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\Result;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18: use Nexus\Mcp\Core\Schema\Enum\CacheScope;
19: use Nexus\Mcp\Core\Schema\Enum\ResultType;
20: use Nexus\Mcp\Core\Schema\Implementation;
21: use Nexus\Mcp\Core\Schema\MetaObject;
22: use Nexus\Mcp\Core\Schema\ServerCapabilities;
23: use Nexus\Mcp\Core\Validation\EnumValueValidator;
24:
25: /**
26: * The result returned by the server for a `server/discover` request.
27: *
28: * @extends CacheableResult<array{
29: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
30: * resultType: non-empty-string,
31: * supportedVersions: list<non-empty-string>,
32: * capabilities: template-type<ServerCapabilities, Arrayable, 'T'>,
33: * serverInfo: template-type<Implementation, Arrayable, 'T'>,
34: * instructions?: non-empty-string,
35: * ttlMs: int,
36: * cacheScope: value-of<CacheScope>,
37: * }>
38: *
39: * @see https://modelcontextprotocol.io/specification/draft/schema#discoverresult
40: */
41: final readonly class DiscoverResult extends CacheableResult implements ServerResult
42: {
43: /**
44: * @var list<non-empty-string>
45: */
46: public array $supportedVersions;
47:
48: /**
49: * @var null|non-empty-string
50: */
51: public ?string $instructions;
52:
53: /**
54: * @param list<string> $supportedVersions
55: */
56: public function __construct(
57: array $supportedVersions,
58: public ServerCapabilities $capabilities,
59: public Implementation $serverInfo,
60: int $ttlMs,
61: CacheScope $cacheScope,
62: ?string $instructions = null,
63: MetaObject $meta = new MetaObject(),
64: ) {
65: Assert::that($supportedVersions)
66: ->isList('"result.supportedVersions" must be a list, non-list array given.')
67: ->values()->isNonEmptyString('each "result.supportedVersions" must be a non-empty string.')
68: ;
69: Assert::that($instructions)->nullOr()->isNonEmptyString('"result.instructions" must be a non-empty string or null.');
70:
71: $this->supportedVersions = $supportedVersions;
72: $this->instructions = $instructions;
73:
74: parent::__construct(ttlMs: $ttlMs, cacheScope: $cacheScope, meta: $meta);
75: }
76:
77: #[\Override]
78: public static function fromArray(array $data): static
79: {
80: Assert::that($data)->hasOffset('supportedVersions', '"result" is missing the required "supportedVersions" key.');
81: Assert::that($data['supportedVersions'])
82: ->isList('"result.supportedVersions" must be a list, {type} given.')
83: ->values()->isString('each "result.supportedVersions" must be a string, {type} given.')
84: ;
85: $supportedVersions = $data['supportedVersions'];
86:
87: Assert::that($data)->hasOffset('capabilities', '"result" is missing the required "capabilities" key.');
88: Assert::that($data['capabilities'])
89: ->isArray('"result.capabilities" must be an object, {type} given.')
90: ->isMap('"result.capabilities" must be a string-keyed object.')
91: ;
92: $capabilities = ServerCapabilities::fromArray($data['capabilities']);
93:
94: Assert::that($data)->hasOffset('serverInfo', '"result" is missing the required "serverInfo" key.');
95: Assert::that($data['serverInfo'])
96: ->isArray('"result.serverInfo" must be an object, {type} given.')
97: ->isMap('"result.serverInfo" must be a string-keyed object.')
98: ;
99: $serverInfo = Implementation::fromArray($data['serverInfo']);
100:
101: Assert::that($data)->hasOffset('ttlMs', '"result" is missing the required "ttlMs" key.');
102: $ttlMs = $data['ttlMs'];
103: Assert::that($ttlMs)->isInt('"result.ttlMs" must be an integer, {type} given.');
104:
105: Assert::that($data)->hasOffset('cacheScope', '"result" is missing the required "cacheScope" key.');
106: $cacheScope = EnumValueValidator::parse(CacheScope::class, $data['cacheScope'], '"result.cacheScope"');
107:
108: $instructions = null;
109:
110: if (\array_key_exists('instructions', $data)) {
111: $raw = $data['instructions'];
112: Assert::that($raw)->isString('"result.instructions" must be a string, {type} given.');
113: $instructions = $raw;
114: }
115:
116: $meta = new MetaObject();
117:
118: if (\array_key_exists('_meta', $data)) {
119: Assert::that($data['_meta'])
120: ->isArray('"result._meta" must be an object, {type} given.')
121: ->isMap('"result._meta" must be a string-keyed object.')
122: ;
123: $meta = MetaObject::fromArray($data['_meta']);
124: }
125:
126: return new self(
127: supportedVersions: $supportedVersions,
128: capabilities: $capabilities,
129: serverInfo: $serverInfo,
130: ttlMs: $ttlMs,
131: cacheScope: $cacheScope,
132: instructions: $instructions,
133: meta: $meta,
134: );
135: }
136:
137: #[\Override]
138: public function toArray(): array
139: {
140: $data = [];
141: $meta = $this->meta->toArray();
142:
143: if ([] !== $meta) {
144: $data['_meta'] = $meta;
145: }
146:
147: $data['resultType'] = self::getResultType();
148: $data['supportedVersions'] = $this->supportedVersions;
149: $data['capabilities'] = $this->capabilities->toArray();
150: $data['serverInfo'] = $this->serverInfo->toArray();
151:
152: if (null !== $this->instructions) {
153: $data['instructions'] = $this->instructions;
154: }
155:
156: $data['ttlMs'] = $this->ttlMs;
157: $data['cacheScope'] = $this->cacheScope->value;
158:
159: return $data;
160: }
161:
162: #[\Override]
163: public function jsonSerialize(): array
164: {
165: $data = $this->toArray();
166: $data['capabilities'] = $this->capabilities->jsonSerialize();
167:
168: return $data;
169: }
170:
171: #[\Override]
172: protected function getResultType(): string
173: {
174: return ResultType::Complete->value;
175: }
176: }
177: