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