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