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\MetaObject;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18: use Nexus\Mcp\Core\Schema\ClientCapabilities;
19: use Nexus\Mcp\Core\Schema\Enum\LoggingLevel;
20: use Nexus\Mcp\Core\Schema\Implementation;
21: use Nexus\Mcp\Core\Schema\MetaObject;
22: use Nexus\Mcp\Core\Schema\ProgressToken;
23: use Nexus\Mcp\Core\Schema\ProtocolVersion;
24:
25: /**
26: * Extends `MetaObject` with additional request-specific fields. All key naming rules from `MetaObject` apply.
27: *
28: * @extends MetaObject<array{
29: * 'io.modelcontextprotocol/protocolVersion': non-empty-string,
30: * 'io.modelcontextprotocol/clientInfo'?: template-type<Implementation, Arrayable, 'T'>,
31: * 'io.modelcontextprotocol/clientCapabilities': template-type<ClientCapabilities, Arrayable, 'T'>,
32: * 'io.modelcontextprotocol/logLevel'?: value-of<LoggingLevel>,
33: * progressToken?: int|non-empty-string,
34: * ...<array-key, mixed>,
35: * }>
36: *
37: * @see https://modelcontextprotocol.io/specification/2026-07-28/schema#requestmetaobject
38: */
39: final readonly class RequestMetaObject extends MetaObject
40: {
41: public const string PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion';
42: public const string CLIENT_INFO_KEY = 'io.modelcontextprotocol/clientInfo';
43: public const string CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities';
44: public const string LOG_LEVEL_KEY = 'io.modelcontextprotocol/logLevel';
45:
46: /**
47: * @param array<array-key, mixed> $extras
48: */
49: public function __construct(
50: public ProtocolVersion $protocolVersion,
51: public ClientCapabilities $clientCapabilities,
52: public ?Implementation $clientInfo = null,
53: public ?LoggingLevel $logLevel = null,
54: public ?ProgressToken $progressToken = null,
55: array $extras = [],
56: ) {
57: parent::__construct(extras: $extras);
58: }
59:
60: #[\Override]
61: public static function fromArray(array $data): static
62: {
63: Assert::that($data)->hasOffset(self::PROTOCOL_VERSION_KEY, '"_meta" is missing the required "{key}" key.');
64: $protocolVersion = $data[self::PROTOCOL_VERSION_KEY];
65: Assert::that($protocolVersion)->isNonEmptyString(\sprintf('"_meta.%s" must be a non-empty string, {type} given.', self::PROTOCOL_VERSION_KEY));
66: unset($data[self::PROTOCOL_VERSION_KEY]);
67:
68: Assert::that($data)->hasOffset(self::CLIENT_CAPABILITIES_KEY, '"_meta" is missing the required "{key}" key.');
69: Assert::that($data[self::CLIENT_CAPABILITIES_KEY])
70: ->isArray(\sprintf('"_meta.%s" must be an object, {type} given.', self::CLIENT_CAPABILITIES_KEY))
71: ->isMap(\sprintf('"_meta.%s" must be a string-keyed object.', self::CLIENT_CAPABILITIES_KEY))
72: ;
73: $clientCapabilities = ClientCapabilities::fromArray($data[self::CLIENT_CAPABILITIES_KEY]);
74: unset($data[self::CLIENT_CAPABILITIES_KEY]);
75:
76: $clientInfo = null;
77:
78: if (\array_key_exists(self::CLIENT_INFO_KEY, $data)) {
79: Assert::that($data[self::CLIENT_INFO_KEY])
80: ->isArray(\sprintf('"_meta.%s" must be an object, {type} given.', self::CLIENT_INFO_KEY))
81: ->isMap(\sprintf('"_meta.%s" must be a string-keyed object.', self::CLIENT_INFO_KEY))
82: ;
83: $clientInfo = Implementation::fromArray($data[self::CLIENT_INFO_KEY]);
84: unset($data[self::CLIENT_INFO_KEY]);
85: }
86:
87: $logLevel = null;
88:
89: if (\array_key_exists(self::LOG_LEVEL_KEY, $data)) {
90: Assert::that($data[self::LOG_LEVEL_KEY])->isOneOf(
91: array_column(LoggingLevel::cases(), 'value'),
92: \sprintf('"_meta.%s" must be one of {choices}, {value} given.', self::LOG_LEVEL_KEY),
93: );
94: $logLevel = LoggingLevel::from($data[self::LOG_LEVEL_KEY]);
95: unset($data[self::LOG_LEVEL_KEY]);
96: }
97:
98: $progressToken = null;
99:
100: if (\array_key_exists('progressToken', $data)) {
101: $raw = $data['progressToken'];
102: Assert::that($raw)->isIntOrNonEmptyString('"_meta.progressToken" must be an int or non-empty string, {type} given.');
103:
104: $progressToken = new ProgressToken(token: $raw);
105: unset($data['progressToken']);
106: }
107:
108: return new self(
109: protocolVersion: new ProtocolVersion(version: $protocolVersion),
110: clientCapabilities: $clientCapabilities,
111: clientInfo: $clientInfo,
112: logLevel: $logLevel,
113: progressToken: $progressToken,
114: extras: $data,
115: );
116: }
117:
118: #[\Override]
119: public function toArray(): array
120: {
121: $out = [];
122:
123: $out[self::PROTOCOL_VERSION_KEY] = $this->protocolVersion->version;
124:
125: if (null !== $this->clientInfo) {
126: $out[self::CLIENT_INFO_KEY] = $this->clientInfo->toArray();
127: }
128:
129: $out[self::CLIENT_CAPABILITIES_KEY] = $this->clientCapabilities->toArray();
130:
131: if (null !== $this->logLevel) {
132: $out[self::LOG_LEVEL_KEY] = $this->logLevel->value;
133: }
134:
135: if (null !== $this->progressToken) {
136: $out['progressToken'] = $this->progressToken->token;
137: }
138:
139: $out += $this->extras;
140:
141: return $out;
142: }
143:
144: #[\Override]
145: public function jsonSerialize(): array
146: {
147: $out = $this->toArray();
148: $out[self::CLIENT_CAPABILITIES_KEY] = $this->clientCapabilities->jsonSerialize();
149:
150: return $out;
151: }
152: }
153: