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: * Schema for multiple-selection enumeration with display titles for each option.
21: *
22: * @implements MultiSelectEnumSchema<array{
23: * type: 'array',
24: * items: array{anyOf: list<template-type<EnumOption, Arrayable, 'T'>>},
25: * title?: non-empty-string,
26: * description?: non-empty-string,
27: * minItems?: int<0, max>,
28: * maxItems?: int<0, max>,
29: * default?: list<string>,
30: * }>
31: *
32: * @see https://modelcontextprotocol.io/specification/draft/schema#titledmultiselectenumschema
33: */
34: final readonly class TitledMultiSelectEnumSchema implements MultiSelectEnumSchema
35: {
36: public const string TYPE = 'array';
37:
38: /**
39: * @var list<EnumOption>
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<EnumOption> $items The inner `anyOf` list of `{const, title}` pairs
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('titled multi-select enum schema "items" must be a list, non-list array given.')
82: ->values()
83: ->isInstanceOf(EnumOption::class, 'each titled multi-select enum schema "items" must be an enum option, {type} given.')
84: ;
85: Assert::that($title)
86: ->nullOr()
87: ->isNonEmptyString('titled multi-select enum schema "title" must be a non-empty string or null.')
88: ;
89: Assert::that($description)
90: ->nullOr()
91: ->isNonEmptyString('titled multi-select enum schema "description" must be a non-empty string or null.')
92: ;
93: Assert::that($minItems)
94: ->nullOr()
95: ->isNaturalInt('titled multi-select enum schema "minItems" must be a non-negative integer or null.')
96: ;
97: Assert::that($maxItems)
98: ->nullOr()
99: ->isNaturalInt('titled multi-select enum schema "maxItems" must be a non-negative integer or null.')
100: ;
101:
102: if (null !== $default) {
103: Assert::that($default)
104: ->isList('titled multi-select enum schema "default" must be a list, non-list array given.')
105: ->values()->isString('each titled multi-select enum schema "default" must be a string.')
106: ;
107: }
108:
109: $this->items = $items;
110: $this->title = $title;
111: $this->description = $description;
112: $this->default = $default;
113: $this->minItems = $minItems;
114: $this->maxItems = $maxItems;
115: }
116:
117: #[\Override]
118: public static function fromArray(array $data): static
119: {
120: Assert::that($data)->hasOffset('type', 'titled multi-select enum schema is missing the required "type" key.');
121: $type = $data['type'];
122: Assert::that($type)->isIdentical(self::TYPE, 'titled multi-select enum schema "type" must be {other}, {value} given.');
123:
124: Assert::that($data)->hasOffset('items', 'titled multi-select enum schema is missing the required "items" key.');
125: Assert::that($data['items'])
126: ->isArray('titled multi-select enum schema "items" must be an object, {type} given.')
127: ->isMap('titled multi-select enum schema "items" must be a string-keyed object.')
128: ;
129:
130: $anyOf = $data['items']['anyOf'] ?? null;
131: Assert::that($anyOf)
132: ->isArray('titled multi-select enum schema "items.anyOf" must be a list, {type} given.')
133: ->isList('titled multi-select enum schema "items.anyOf" must be a list, non-list array given.')
134: ->values()
135: ->isArray('each titled multi-select enum schema "items.anyOf" must be an object, {type} given.')
136: ->isMap('each titled multi-select enum schema "items.anyOf" must be a string-keyed object.')
137: ;
138: $items = array_map(EnumOption::fromArray(...), $anyOf);
139:
140: $title = $data['title'] ?? null;
141: Assert::that($title)->nullOr()->isString('titled multi-select enum schema "title" must be a string or null, {type} given.');
142:
143: $description = $data['description'] ?? null;
144: Assert::that($description)->nullOr()->isString('titled multi-select enum schema "description" must be a string or null, {type} given.');
145:
146: $minItems = $data['minItems'] ?? null;
147: Assert::that($minItems)->nullOr()->isInt('titled multi-select enum schema "minItems" must be an int or null, {type} given.');
148:
149: $maxItems = $data['maxItems'] ?? null;
150: Assert::that($maxItems)->nullOr()->isInt('titled multi-select enum schema "maxItems" must be an int or null, {type} given.');
151:
152: $default = null;
153:
154: if (isset($data['default'])) {
155: Assert::that($data['default'])
156: ->isList('titled multi-select enum schema "default" must be a list, non-list array given.')
157: ->values()->isString('each titled multi-select enum schema "default" must be a string, {type} given.')
158: ;
159: $default = $data['default'];
160: }
161:
162: return new self(
163: items: $items,
164: title: $title,
165: description: $description,
166: minItems: $minItems,
167: maxItems: $maxItems,
168: default: $default,
169: );
170: }
171:
172: #[\Override]
173: public function toArray(): array
174: {
175: $data = [
176: 'type' => self::TYPE,
177: 'items' => [
178: 'anyOf' => array_map(static fn(EnumOption $o): array => $o->toArray(), $this->items),
179: ],
180: ];
181:
182: if (null !== $this->title) {
183: $data['title'] = $this->title;
184: }
185:
186: if (null !== $this->description) {
187: $data['description'] = $this->description;
188: }
189:
190: if (null !== $this->minItems) {
191: $data['minItems'] = $this->minItems;
192: }
193:
194: if (null !== $this->maxItems) {
195: $data['maxItems'] = $this->maxItems;
196: }
197:
198: if (null !== $this->default) {
199: $data['default'] = $this->default;
200: }
201:
202: return $data;
203: }
204:
205: #[\Override]
206: public function jsonSerialize(): array
207: {
208: return $this->toArray();
209: }
210: }
211: