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\RequestId;
23:
24: /**
25: * A response to a request that indicates an error occurred.
26: *
27: * @implements Arrayable<array{
28: * jsonrpc: '2.0',
29: * id?: int|non-empty-string,
30: * error: template-type<Error, Arrayable, 'T'>,
31: * }>
32: *
33: * @see https://modelcontextprotocol.io/specification/draft/schema#jsonrpcerrorresponse
34: */
35: final readonly class JsonRpcErrorResponse implements Arrayable, JsonRpcResponse
36: {
37: public function __construct(public ?RequestId $id, public Error $error)
38: {
39: }
40:
41: #[\Override]
42: public static function fromArray(array $data): static
43: {
44: $id = $data['id'] ?? null;
45: Assert::that($id)
46: ->nullOr()
47: ->isArrayKey('"id" must be an int, string, or null, {type} given.')
48: ;
49:
50: $error = $data['error'] ?? [];
51: Assert::that($error)
52: ->isArray('"error" must be an object, {type} given.')
53: ->isMap('"error" must be a string-keyed object.')
54: ;
55:
56: return new self(
57: id: null === $id ? null : new RequestId(id: $id),
58: error: self::parseError($error),
59: );
60: }
61:
62: #[\Override]
63: public function toArray(): array
64: {
65: $envelope = ['jsonrpc' => self::JSONRPC_VERSION];
66:
67: if (null !== $this->id) {
68: $envelope['id'] = $this->id->id;
69: }
70:
71: $envelope['error'] = $this->error->toArray();
72:
73: return $envelope;
74: }
75:
76: #[\Override]
77: public function jsonSerialize(): array
78: {
79: $envelope = ['jsonrpc' => self::JSONRPC_VERSION];
80:
81: if (null !== $this->id) {
82: $envelope['id'] = $this->id->id;
83: }
84:
85: $envelope['error'] = $this->error->jsonSerialize();
86:
87: return $envelope;
88: }
89:
90: /**
91: * @param array<string, mixed> $data
92: */
93: private static function parseError(array $data): Error
94: {
95: Assert::that($data)->hasOffset('code', '"error" is missing the required "code" key.');
96: Assert::that($data['code'])->isInt('"error.code" must be an integer, {type} given.');
97: $code = $data['code'];
98:
99: Assert::that($data)->hasOffset('message', '"error" is missing the required "message" key.');
100: Assert::that($data['message'])->isString('"error.message" must be a string, {type} given.');
101: $message = $data['message'];
102:
103: $extra = $data['data'] ?? null;
104:
105: $resolved = ProtocolErrorCode::tryFrom($code);
106:
107: if (null === $resolved) {
108: return new UnknownProtocolError(code: $code, message: $message, data: $extra);
109: }
110:
111: return ErrorFactory::create($resolved, $message, $extra);
112: }
113: }
114: