1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Core\Schema\JsonRpc;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\JsonRpc\ErrorFactory;
18: use Nexus\Mcp\Core\Schema\Arrayable;
19: use Nexus\Mcp\Core\Schema\Enum\ProtocolErrorCode;
20: use Nexus\Mcp\Core\Schema\Error;
21: use Nexus\Mcp\Core\Schema\Error\UnknownProtocolError;
22: use Nexus\Mcp\Core\Schema\Error\UrlElicitationRequiredErrorPayload;
23: use Nexus\Mcp\Core\Schema\RequestId;
24:
25: /**
26: * A response to a request that indicates an error occurred.
27: *
28: * @implements Arrayable<array{
29: * jsonrpc: '2.0',
30: * id?: int|non-empty-string,
31: * error: template-type<Error, Arrayable, 'T'>,
32: * }>
33: *
34: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#jsonrpcerrorresponse
35: */
36: final readonly class JsonRpcErrorResponse implements Arrayable, JsonRpcResponse
37: {
38: public function __construct(public ?RequestId $id, public Error $error)
39: {
40: }
41:
42: /**
43: * @param array<string, mixed> $data
44: */
45: #[\Override]
46: public static function fromArray(array $data): static
47: {
48: $data += ['id' => null, 'error' => []];
49:
50: $id = $data['id'];
51:
52: Assert::that($id)
53: ->nullOr()
54: ->isArrayKey('JSON-RPC error response id must be an int, string, or null; {type} given.')
55: ;
56:
57: Assert::that($data['error'])
58: ->isArray('JSON-RPC error response "error" must be an object, {type} given.')
59: ->isMap('JSON-RPC error response "error" must be a string-keyed object.')
60: ;
61:
62: return new self(
63: null === $id ? null : new RequestId($id),
64: self::parseError($data['error']),
65: );
66: }
67:
68: #[\Override]
69: public function toArray(): array
70: {
71: $envelope = ['jsonrpc' => self::JSONRPC_VERSION];
72:
73: if (null !== $this->id) {
74: $envelope['id'] = $this->id->id;
75: }
76:
77: $envelope['error'] = $this->error->toArray();
78:
79: return $envelope;
80: }
81:
82: /**
83: * @return array{
84: * jsonrpc: '2.0',
85: * id?: int|non-empty-string,
86: * error: template-type<Error, Arrayable, 'T'>,
87: * }
88: */
89: #[\Override]
90: public function jsonSerialize(): array
91: {
92: return $this->toArray();
93: }
94:
95: /**
96: * @param array<string, mixed> $data
97: */
98: private static function parseError(array $data): Error
99: {
100: Assert::that($data)->hasOffset('code', 'error response missing the required "code" key.');
101: Assert::that($data['code'])->isInt('error response "code" must be an integer, {type} given.');
102: $code = $data['code'];
103:
104: Assert::that($data)->hasOffset('message', 'error response missing the required "message" key.');
105: Assert::that($data['message'])->isString('error response "message" must be a string, {type} given.');
106: $message = $data['message'];
107:
108: $extra = $data['data'] ?? null;
109: $narrow = ['message' => $message];
110:
111: if (null !== $extra) {
112: $narrow['data'] = $extra;
113: }
114:
115: $resolved = ProtocolErrorCode::tryFrom($code);
116:
117: if (ProtocolErrorCode::UrlElicitationRequired === $resolved) {
118: return UrlElicitationRequiredErrorPayload::fromArray($narrow);
119: }
120:
121: if (null === $resolved) {
122: return new UnknownProtocolError($code, $message, $extra);
123: }
124:
125: return ErrorFactory::create($resolved, $message, $extra);
126: }
127: }
128: