-
Notifications
You must be signed in to change notification settings - Fork 123
[Server] Extend elicitation enum schema compliance #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the official PHP MCP SDK. | ||
| * | ||
| * A collaboration between Symfony and the PHP Foundation. | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Mcp\Schema\Elicitation; | ||
|
|
||
| use Mcp\Exception\InvalidArgumentException; | ||
|
|
||
| /** | ||
| * Schema definition for multi-select enum fields without titles (SEP-1330). | ||
| * | ||
| * Produces: {"type": "array", "items": {"type": "string", "enum": [...]}} | ||
| * | ||
| * @see https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330 | ||
| */ | ||
| final class MultiSelectEnumSchemaDefinition extends AbstractSchemaDefinition | ||
| { | ||
| /** | ||
| * @param string $title Human-readable title for the field | ||
| * @param string[] $enum Array of allowed string values | ||
| * @param string|null $description Optional description/help text | ||
| */ | ||
| public function __construct( | ||
| string $title, | ||
| public readonly array $enum, | ||
| ?string $description = null, | ||
| ) { | ||
| parent::__construct($title, $description); | ||
|
|
||
|
Comment on lines
+25
to
+38
|
||
| if ([] === $enum) { | ||
| throw new InvalidArgumentException('enum array must not be empty.'); | ||
| } | ||
|
|
||
| foreach ($enum as $value) { | ||
| if (!\is_string($value)) { | ||
| throw new InvalidArgumentException('All enum values must be strings.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, mixed> | ||
| */ | ||
| public function jsonSerialize(): array | ||
| { | ||
| $data = [ | ||
| 'type' => 'array', | ||
| 'title' => $this->title, | ||
| 'items' => [ | ||
| 'type' => 'string', | ||
| 'enum' => $this->enum, | ||
| ], | ||
| ]; | ||
|
|
||
| if (null !== $this->description) { | ||
| $data['description'] = $this->description; | ||
| } | ||
|
|
||
| return $data; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the official PHP MCP SDK. | ||
| * | ||
| * A collaboration between Symfony and the PHP Foundation. | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Mcp\Schema\Elicitation; | ||
|
|
||
| use Mcp\Exception\InvalidArgumentException; | ||
|
|
||
| /** | ||
| * Schema definition for single-select enum fields with titled options (SEP-1330). | ||
| * | ||
| * Uses the oneOf pattern with const/title pairs instead of enum/enumNames. | ||
| * Produces: {"type": "string", "oneOf": [{"const": "value", "title": "Label"}, ...]} | ||
| * | ||
| * @see https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330 | ||
| */ | ||
| final class TitledEnumSchemaDefinition extends AbstractSchemaDefinition | ||
| { | ||
| /** | ||
| * @param string $title Human-readable title for the field | ||
| * @param list<array{const: string, title: string}> $oneOf Array of const/title pairs | ||
| * @param string|null $description Optional description/help text | ||
| * @param string|null $default Optional default value (must match a const) | ||
| */ | ||
| public function __construct( | ||
| string $title, | ||
| public readonly array $oneOf, | ||
| ?string $description = null, | ||
| public readonly ?string $default = null, | ||
| ) { | ||
| parent::__construct($title, $description); | ||
|
Comment on lines
+28
to
+40
|
||
|
|
||
| if ([] === $oneOf) { | ||
| throw new InvalidArgumentException('oneOf array must not be empty.'); | ||
| } | ||
|
|
||
| $consts = []; | ||
| foreach ($oneOf as $item) { | ||
| if (!isset($item['const']) || !\is_string($item['const'])) { | ||
| throw new InvalidArgumentException('Each oneOf item must have a string "const" property.'); | ||
| } | ||
| if (!isset($item['title']) || !\is_string($item['title'])) { | ||
| throw new InvalidArgumentException('Each oneOf item must have a string "title" property.'); | ||
| } | ||
| $consts[] = $item['const']; | ||
| } | ||
|
|
||
| if (null !== $default && !\in_array($default, $consts, true)) { | ||
| throw new InvalidArgumentException(\sprintf('Default value "%s" is not in the oneOf const values.', $default)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, mixed> | ||
| */ | ||
| public function jsonSerialize(): array | ||
| { | ||
| $data = [ | ||
| 'type' => 'string', | ||
| 'title' => $this->title, | ||
| 'oneOf' => $this->oneOf, | ||
| ]; | ||
|
|
||
| if (null !== $this->description) { | ||
| $data['description'] = $this->description; | ||
| } | ||
|
|
||
| if (null !== $this->default) { | ||
| $data['default'] = $this->default; | ||
| } | ||
|
|
||
| return $data; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the official PHP MCP SDK. | ||
| * | ||
| * A collaboration between Symfony and the PHP Foundation. | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Mcp\Schema\Elicitation; | ||
|
|
||
| use Mcp\Exception\InvalidArgumentException; | ||
|
|
||
| /** | ||
| * Schema definition for multi-select enum fields with titled options (SEP-1330). | ||
| * | ||
| * Produces: {"type": "array", "items": {"anyOf": [{"const": "value", "title": "Label"}, ...]}} | ||
| * | ||
| * @see https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330 | ||
| */ | ||
| final class TitledMultiSelectEnumSchemaDefinition extends AbstractSchemaDefinition | ||
| { | ||
| /** | ||
|
Comment on lines
+23
to
+27
|
||
| * @param string $title Human-readable title for the field | ||
| * @param list<array{const: string, title: string}> $anyOf Array of const/title pairs | ||
| * @param string|null $description Optional description/help text | ||
| */ | ||
| public function __construct( | ||
| string $title, | ||
| public readonly array $anyOf, | ||
| ?string $description = null, | ||
| ) { | ||
| parent::__construct($title, $description); | ||
|
|
||
| if ([] === $anyOf) { | ||
| throw new InvalidArgumentException('anyOf array must not be empty.'); | ||
| } | ||
|
Comment on lines
+25
to
+41
|
||
|
|
||
| foreach ($anyOf as $item) { | ||
| if (!isset($item['const']) || !\is_string($item['const'])) { | ||
| throw new InvalidArgumentException('Each anyOf item must have a string "const" property.'); | ||
| } | ||
| if (!isset($item['title']) || !\is_string($item['title'])) { | ||
| throw new InvalidArgumentException('Each anyOf item must have a string "title" property.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, mixed> | ||
| */ | ||
| public function jsonSerialize(): array | ||
| { | ||
| $data = [ | ||
| 'type' => 'array', | ||
| 'title' => $this->title, | ||
| 'items' => [ | ||
| 'anyOf' => $this->anyOf, | ||
| ], | ||
| ]; | ||
|
|
||
| if (null !== $this->description) { | ||
| $data['description'] = $this->description; | ||
| } | ||
|
|
||
| return $data; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,3 @@ | ||
| server: | ||
| - tools-call-elicitation | ||
| - elicitation-sep1034-defaults | ||
| - elicitation-sep1330-enums | ||
| - dns-rebinding-protection | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor PHPDoc now allows any AbstractSchemaDefinition, but ElicitationSchema::fromArray still delegates to PrimitiveSchemaDefinition::fromArray which currently rejects
type: arrayand cannot recognizeoneOf/anyOfenum shapes. This means a client deserializing anelicitation/createrequest containing SEP-1330 enums (or multi-select arrays) will either throw (array type) or silently downgrade titled enums to a plain StringSchemaDefinition. Consider extending the parsing path (PrimitiveSchemaDefinition + related fromArray implementations) to support these new schema shapes and updating the schema-level tests accordingly.