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\Elicitation;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * The `requestedSchema` shape carried by an `ElicitRequestFormParams`.
21: *
22: * @implements Arrayable<array{
23: * type: 'object',
24: * properties: array<int|non-empty-string, template-type<PrimitiveSchemaDefinition, Arrayable, 'T'>>,
25: * required?: list<non-empty-string>,
26: * '$schema'?: non-empty-string,
27: * }>
28: *
29: * @see https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts
30: */
31: final readonly class ElicitRequestedSchema implements Arrayable
32: {
33: public const string TYPE = 'object';
34:
35: /**
36: * @param array<int|non-empty-string, PrimitiveSchemaDefinition> $properties
37: * @param null|list<non-empty-string> $required
38: * @param null|non-empty-string $schema
39: */
40: public function __construct(
41: public array $properties,
42: public ?array $required = null,
43: public ?string $schema = null,
44: ) {
45: Assert::that($properties)->keys()->isIntOrNonEmptyString('each "requestedSchema.properties" key must be an int or non-empty string.');
46: Assert::that($properties)->values()->isInstanceOf(PrimitiveSchemaDefinition::class);
47:
48: if (null !== $required) {
49: Assert::that($required)
50: ->isList('"requestedSchema.required" must be a list, non-list array given.')
51: ->values()->isNonEmptyString('each "requestedSchema.required" must be a non-empty string.')
52: ;
53: }
54:
55: Assert::that($schema)->nullOr()->isNonEmptyString('"requestedSchema.$schema" must be a non-empty string or null.');
56: }
57:
58: #[\Override]
59: public static function fromArray(array $data): static
60: {
61: Assert::that($data)->hasOffset('type', '"requestedSchema" is missing the required "type" key.');
62: $type = $data['type'];
63: Assert::that($type)->isIdentical(self::TYPE, '"requestedSchema.type" must be {other}, {value} given.');
64:
65: Assert::that($data)->hasOffset('properties', '"requestedSchema" is missing the required "properties" key.');
66: Assert::that($data['properties'])->isArray('"requestedSchema.properties" must be an object, {type} given.');
67:
68: $properties = [];
69:
70: foreach ($data['properties'] as $name => $shape) {
71: Assert::that($name)->isIntOrNonEmptyString('each "requestedSchema.properties" key must be an int or non-empty string.');
72: Assert::that($shape)
73: ->isArray('"requestedSchema.properties" must be an object, {type} given.')
74: ->isMap('"requestedSchema.properties" must be a string-keyed object.')
75: ;
76:
77: $properties[$name] = self::parsePrimitiveSchema($shape);
78: }
79:
80: $required = null;
81:
82: if (isset($data['required'])) {
83: Assert::that($data['required'])
84: ->isList('"requestedSchema.required" must be a list, non-list array given.')
85: ->values()->isNonEmptyString('each "requestedSchema.required" must be a non-empty string, {type} given.')
86: ;
87: $required = $data['required'];
88: }
89:
90: $schema = $data['$schema'] ?? null;
91: Assert::that($schema)->nullOr()->isNonEmptyString('"requestedSchema.$schema" must be a non-empty string or null, {type} given.');
92:
93: return new self(properties: $properties, required: $required, schema: $schema);
94: }
95:
96: #[\Override]
97: public function toArray(): array
98: {
99: $data = [
100: 'type' => self::TYPE,
101: 'properties' => array_map(
102: static fn(PrimitiveSchemaDefinition $p): array => $p->toArray(),
103: $this->properties,
104: ),
105: ];
106:
107: if (null !== $this->required) {
108: $data['required'] = $this->required;
109: }
110:
111: if (null !== $this->schema) {
112: $data['$schema'] = $this->schema;
113: }
114:
115: return $data;
116: }
117:
118: #[\Override]
119: public function jsonSerialize(): array
120: {
121: $data = $this->toArray();
122:
123: if (array_is_list($this->properties)) {
124: $data['properties'] = (object) $data['properties'];
125: }
126:
127: return $data;
128: }
129:
130: /**
131: * @param array<string, mixed> $data
132: */
133: private static function parsePrimitiveSchema(array $data): PrimitiveSchemaDefinition
134: {
135: $type = $data['type'] ?? null;
136: Assert::that($type)->isString('"requestedSchema.primitiveSchema" must carry a "type" string, {type} given.');
137:
138: return match (true) {
139: BooleanSchema::TYPE === $type => BooleanSchema::fromArray($data),
140: NumberSchema::TYPE === $type, NumberSchema::TYPE_INTEGER === $type => NumberSchema::fromArray($data),
141: UntitledMultiSelectEnumSchema::TYPE === $type => self::parseArraySchema($data),
142: StringSchema::TYPE === $type => self::parseStringSchema($data),
143: default => throw new \InvalidArgumentException(\sprintf(
144: '"requestedSchema.primitiveSchema" has unknown "type" %s.',
145: var_export($type, true),
146: )),
147: };
148: }
149:
150: /**
151: * @param array<string, mixed> $data
152: */
153: private static function parseStringSchema(array $data): PrimitiveSchemaDefinition
154: {
155: return match (true) {
156: isset($data['oneOf']) => TitledSingleSelectEnumSchema::fromArray($data),
157: isset($data['enum']) && isset($data['enumNames']) => LegacyTitledEnumSchema::fromArray($data),
158: isset($data['enum']) => UntitledSingleSelectEnumSchema::fromArray($data),
159: default => StringSchema::fromArray($data),
160: };
161: }
162:
163: /**
164: * @param array<string, mixed> $data
165: */
166: private static function parseArraySchema(array $data): PrimitiveSchemaDefinition
167: {
168: $items = $data['items'] ?? null;
169: Assert::that($items)
170: ->isArray('"requestedSchema.items" must be an object, {type} given.')
171: ->isMap('"requestedSchema" multi-select "items" must be a string-keyed object.')
172: ;
173:
174: return isset($items['anyOf'])
175: ? TitledMultiSelectEnumSchema::fromArray($data)
176: : UntitledMultiSelectEnumSchema::fromArray($data);
177: }
178: }
179: