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:
18: /**
19: * Schema for multiple-selection enumeration without display titles for options.
20: *
21: * @implements MultiSelectEnumSchema<array{
22: * type: 'array',
23: * items: array{type: 'string', enum: list<non-empty-string>},
24: * title?: non-empty-string,
25: * description?: non-empty-string,
26: * minItems?: int<0, max>,
27: * maxItems?: int<0, max>,
28: * default?: list<string>,
29: * }>
30: *
31: * @see https://modelcontextprotocol.io/specification/draft/schema#untitledmultiselectenumschema
32: */
33: final readonly class UntitledMultiSelectEnumSchema implements MultiSelectEnumSchema
34: {
35: public const string TYPE = 'array';
36: public const string ITEMS_TYPE = 'string';
37:
38: /**
39: * @var list<non-empty-string>
40: */
41: public array $items;
42:
43: /**
44: * @var null|non-empty-string
45: */
46: public ?string $title;
47:
48: /**
49: * @var null|non-empty-string
50: */
51: public ?string $description;
52:
53: /**
54: * @var null|list<string>
55: */
56: public ?array $default;
57:
58: /**
59: * @var null|int<0, max>
60: */
61: public ?int $minItems;
62:
63: /**
64: * @var null|int<0, max>
65: */
66: public ?int $maxItems;
67:
68: /**
69: * @param list<string> $items The inner `enum` values
70: * @param null|list<string> $default
71: */
72: public function __construct(
73: array $items,
74: ?string $title = null,
75: ?string $description = null,
76: ?int $minItems = null,
77: ?int $maxItems = null,
78: ?array $default = null,
79: ) {
80: Assert::that($items)
81: ->isList('untitled multi-select enum schema "items" must be a list, non-list array given.')
82: ->values()->isNonEmptyString('each untitled multi-select enum schema "items" must be a non-empty string.')
83: ;
84: Assert::that($title)
85: ->nullOr()
86: ->isNonEmptyString('untitled multi-select enum schema "title" must be a non-empty string or null.')
87: ;
88: Assert::that($description)
89: ->nullOr()
90: ->isNonEmptyString('untitled multi-select enum schema "description" must be a non-empty string or null.')
91: ;
92: Assert::that($minItems)
93: ->nullOr()
94: ->isNaturalInt('untitled multi-select enum schema "minItems" must be a non-negative integer or null.')
95: ;
96: Assert::that($maxItems)
97: ->nullOr()
98: ->isNaturalInt('untitled multi-select enum schema "maxItems" must be a non-negative integer or null.')
99: ;
100:
101: if (null !== $default) {
102: Assert::that($default)
103: ->isList('untitled multi-select enum schema "default" must be a list, non-list array given.')
104: ->values()->isString('each untitled multi-select enum schema "default" must be a string.')
105: ;
106: }
107:
108: $this->items = $items;
109: $this->title = $title;
110: $this->description = $description;
111: $this->default = $default;
112: $this->minItems = $minItems;
113: $this->maxItems = $maxItems;
114: }
115:
116: #[\Override]
117: public static function fromArray(array $data): static
118: {
119: Assert::that($data)->hasOffset('type', 'untitled multi-select enum schema is missing the required "type" key.');
120: $type = $data['type'];
121: Assert::that($type)->isIdentical(self::TYPE, 'untitled multi-select enum schema "type" must be {other}, {value} given.');
122:
123: Assert::that($data)->hasOffset('items', 'untitled multi-select enum schema is missing the required "items" key.');
124: Assert::that($data['items'])
125: ->isArray('untitled multi-select enum schema "items" must be an object, {type} given.')
126: ->isMap('untitled multi-select enum schema "items" must be a string-keyed object.')
127: ;
128:
129: $itemsType = $data['items']['type'] ?? null;
130: Assert::that($itemsType)->isIdentical(self::ITEMS_TYPE, 'untitled multi-select enum schema "items.type" must be {other}, {value} given.');
131:
132: $itemsEnum = $data['items']['enum'] ?? null;
133: Assert::that($itemsEnum)
134: ->isArray('untitled multi-select enum schema "items.enum" must be a list, {type} given.')
135: ->isList('untitled multi-select enum schema "items.enum" must be a list, non-list array given.')
136: ->values()->isString('each untitled multi-select enum schema "items.enum" must be a string, {type} given.')
137: ;
138:
139: $title = $data['title'] ?? null;
140: Assert::that($title)->nullOr()->isString('untitled multi-select enum schema "title" must be a string or null, {type} given.');
141:
142: $description = $data['description'] ?? null;
143: Assert::that($description)->nullOr()->isString('untitled multi-select enum schema "description" must be a string or null, {type} given.');
144:
145: $minItems = $data['minItems'] ?? null;
146: Assert::that($minItems)->nullOr()->isInt('untitled multi-select enum schema "minItems" must be an int or null, {type} given.');
147:
148: $maxItems = $data['maxItems'] ?? null;
149: Assert::that($maxItems)->nullOr()->isInt('untitled multi-select enum schema "maxItems" must be an int or null, {type} given.');
150:
151: $default = null;
152:
153: if (isset($data['default'])) {
154: Assert::that($data['default'])
155: ->isList('untitled multi-select enum schema "default" must be a list, non-list array given.')
156: ->values()->isString('each untitled multi-select enum schema "default" must be a string, {type} given.')
157: ;
158: $default = $data['default'];
159: }
160:
161: return new self(
162: items: $itemsEnum,
163: title: $title,
164: description: $description,
165: minItems: $minItems,
166: maxItems: $maxItems,
167: default: $default,
168: );
169: }
170:
171: #[\Override]
172: public function toArray(): array
173: {
174: $data = [
175: 'type' => self::TYPE,
176: 'items' => ['type' => self::ITEMS_TYPE, 'enum' => $this->items],
177: ];
178:
179: if (null !== $this->title) {
180: $data['title'] = $this->title;
181: }
182:
183: if (null !== $this->description) {
184: $data['description'] = $this->description;
185: }
186:
187: if (null !== $this->minItems) {
188: $data['minItems'] = $this->minItems;
189: }
190:
191: if (null !== $this->maxItems) {
192: $data['maxItems'] = $this->maxItems;
193: }
194:
195: if (null !== $this->default) {
196: $data['default'] = $this->default;
197: }
198:
199: return $data;
200: }
201:
202: #[\Override]
203: public function jsonSerialize(): array
204: {
205: return $this->toArray();
206: }
207: }
208: