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/2026-07-28/schema#titledmultiselectenumschema
33: */
34: final readonly class TitledMultiSelectEnumSchema implements MultiSelectEnumSchema
35: {
36: public const string TYPE = 'array';
37:
38: /**
39: * @param list<EnumOption> $items The inner `anyOf` list of `{const, title}` pairs
40: * @param null|non-empty-string $title
41: * @param null|non-empty-string $description
42: * @param null|int<0, max> $minItems
43: * @param null|int<0, max> $maxItems
44: * @param null|list<string> $default
45: */
46: public function __construct(
47: public array $items,
48: public ?string $title = null,
49: public ?string $description = null,
50: public ?int $minItems = null,
51: public ?int $maxItems = null,
52: public ?array $default = null,
53: ) {
54: Assert::that($items)
55: ->isList('titled multi-select enum schema "items" must be a list, non-list array given.')
56: ->values()
57: ->isInstanceOf(EnumOption::class, 'each titled multi-select enum schema "items" must be an enum option, {type} given.')
58: ;
59: Assert::that($title)
60: ->nullOr()
61: ->isNonEmptyString('titled multi-select enum schema "title" must be a non-empty string or null.')
62: ;
63: Assert::that($description)
64: ->nullOr()
65: ->isNonEmptyString('titled multi-select enum schema "description" must be a non-empty string or null.')
66: ;
67: Assert::that($minItems)
68: ->nullOr()
69: ->isNaturalInt('titled multi-select enum schema "minItems" must be a non-negative integer or null.')
70: ;
71: Assert::that($maxItems)
72: ->nullOr()
73: ->isNaturalInt('titled multi-select enum schema "maxItems" must be a non-negative integer or null.')
74: ;
75:
76: if (null !== $default) {
77: Assert::that($default)
78: ->isList('titled multi-select enum schema "default" must be a list, non-list array given.')
79: ->values()->isString('each titled multi-select enum schema "default" must be a string.')
80: ;
81: }
82: }
83:
84: #[\Override]
85: public static function fromArray(array $data): static
86: {
87: Assert::that($data)->hasOffset('type', 'titled multi-select enum schema is missing the required "type" key.');
88: $type = $data['type'];
89: Assert::that($type)->isIdentical(self::TYPE, 'titled multi-select enum schema "type" must be {other}, {value} given.');
90:
91: Assert::that($data)->hasOffset('items', 'titled multi-select enum schema is missing the required "items" key.');
92: Assert::that($data['items'])
93: ->isArray('titled multi-select enum schema "items" must be an object, {type} given.')
94: ->isMap('titled multi-select enum schema "items" must be a string-keyed object.')
95: ;
96:
97: $anyOf = $data['items']['anyOf'] ?? null;
98: Assert::that($anyOf)
99: ->isArray('titled multi-select enum schema "items.anyOf" must be a list, {type} given.')
100: ->isList('titled multi-select enum schema "items.anyOf" must be a list, non-list array given.')
101: ->values()
102: ->isArray('each titled multi-select enum schema "items.anyOf" must be an object, {type} given.')
103: ->isMap('each titled multi-select enum schema "items.anyOf" must be a string-keyed object.')
104: ;
105: $items = array_map(EnumOption::fromArray(...), $anyOf);
106:
107: $title = $data['title'] ?? null;
108: Assert::that($title)->nullOr()->isNonEmptyString('titled multi-select enum schema "title" must be a non-empty string or null, {type} given.');
109:
110: $description = $data['description'] ?? null;
111: Assert::that($description)->nullOr()->isNonEmptyString('titled multi-select enum schema "description" must be a non-empty string or null, {type} given.');
112:
113: $minItems = $data['minItems'] ?? null;
114: Assert::that($minItems)->nullOr()->isNaturalInt('titled multi-select enum schema "minItems" must be a non-negative integer or null, {type} given.');
115:
116: $maxItems = $data['maxItems'] ?? null;
117: Assert::that($maxItems)->nullOr()->isNaturalInt('titled multi-select enum schema "maxItems" must be a non-negative integer or null, {type} given.');
118:
119: $default = null;
120:
121: if (isset($data['default'])) {
122: Assert::that($data['default'])
123: ->isList('titled multi-select enum schema "default" must be a list, non-list array given.')
124: ->values()->isString('each titled multi-select enum schema "default" must be a string, {type} given.')
125: ;
126: $default = $data['default'];
127: }
128:
129: return new self(
130: items: $items,
131: title: $title,
132: description: $description,
133: minItems: $minItems,
134: maxItems: $maxItems,
135: default: $default,
136: );
137: }
138:
139: #[\Override]
140: public function toArray(): array
141: {
142: $data = [
143: 'type' => self::TYPE,
144: 'items' => [
145: 'anyOf' => array_map(static fn(EnumOption $o): array => $o->toArray(), $this->items),
146: ],
147: ];
148:
149: if (null !== $this->title) {
150: $data['title'] = $this->title;
151: }
152:
153: if (null !== $this->description) {
154: $data['description'] = $this->description;
155: }
156:
157: if (null !== $this->minItems) {
158: $data['minItems'] = $this->minItems;
159: }
160:
161: if (null !== $this->maxItems) {
162: $data['maxItems'] = $this->maxItems;
163: }
164:
165: if (null !== $this->default) {
166: $data['default'] = $this->default;
167: }
168:
169: return $data;
170: }
171:
172: #[\Override]
173: public function jsonSerialize(): array
174: {
175: return $this->toArray();
176: }
177: }
178: