From 63b1687508e0f4d1f913922681ea70be6ad20380 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 18 Aug 2026 02:47:41 +0200 Subject: [PATCH 1/4] [Schema] Stop rejecting elicitation schemas the specification allows The specification makes `title` optional on every elicitation schema definition. AbstractSchemaDefinition required it, so a client reading a conformant server's elicitation/create refused the request - and a server building one could not omit it either. `title` is now nullable on all seven definitions, omitted from the serialized form when absent, and rejected only when present and not a string. --- CHANGELOG.md | 1 + .../Elicitation/AbstractSchemaDefinition.php | 23 +++++++++++-------- .../Elicitation/BooleanSchemaDefinition.php | 10 ++++---- .../Elicitation/EnumSchemaDefinition.php | 20 ++++++++-------- .../MultiSelectEnumSchemaDefinition.php | 6 ++--- .../Elicitation/NumberSchemaDefinition.php | 8 +++---- .../Elicitation/StringSchemaDefinition.php | 10 ++++---- .../TitledEnumSchemaDefinition.php | 8 +++---- .../TitledMultiSelectEnumSchemaDefinition.php | 8 +++---- .../BooleanSchemaDefinitionTest.php | 16 ++++++++++--- .../Elicitation/EnumSchemaDefinitionTest.php | 16 ++++++++++--- .../MultiSelectEnumSchemaDefinitionTest.php | 17 ++++++++++++-- .../NumberSchemaDefinitionTest.php | 16 ++++++++++--- .../StringSchemaDefinitionTest.php | 16 ++++++++++--- .../TitledEnumSchemaDefinitionTest.php | 16 ++++++++++--- ...ledMultiSelectEnumSchemaDefinitionTest.php | 17 ++++++++++++-- 16 files changed, 146 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c02faf30..b732c041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to `mcp/sdk` will be documented in this file. 0.8.0 ----- +* Fix elicitation schema definitions requiring `title`, which the specification makes optional — a client reading a conformant server's `elicitation/create` refused the request outright. `title` is now nullable on all seven definitions and omitted from the serialized form when absent. * [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name` — the spec allows any string (its own examples use `main.rs` and `Project Files`). URI/URI-template validation is unchanged. * Add `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, and `ClientCapabilities::withExtensions()` so clients can negotiate and check protocol extensions (e.g. MCP Apps) the same way servers already do. [BC Break] `ServerExtensionInterface` is replaced by the side-agnostic `Mcp\Schema\Extension\ExtensionInterface`. * Deprecate Roots, Sampling and Logging per SEP-2577 (protocol revision `2026-07-28`, earliest removal `2027-07-28`). They keep working but using them now triggers a deprecation notice — migrate to tool arguments/resource URIs, a direct LLM provider API, and stderr/OpenTelemetry respectively. diff --git a/src/Schema/Elicitation/AbstractSchemaDefinition.php b/src/Schema/Elicitation/AbstractSchemaDefinition.php index 63fdc219..547d44a5 100644 --- a/src/Schema/Elicitation/AbstractSchemaDefinition.php +++ b/src/Schema/Elicitation/AbstractSchemaDefinition.php @@ -21,13 +21,17 @@ abstract class AbstractSchemaDefinition implements \JsonSerializable { public function __construct( - public readonly string $title, + public readonly ?string $title = null, public readonly ?string $description = null, ) { } /** - * Validate that title exists and is a string in the data array. + * Reject a title that is present but not a string. + * + * The specification makes `title` optional on every elicitation schema, so + * its absence is not an error — and treating it as one would have this + * client refuse to read a conformant server's request. * * @param array $data * @@ -35,22 +39,23 @@ public function __construct( */ protected static function validateTitle(array $data, string $schemaType): void { - if (!isset($data['title']) || !\is_string($data['title'])) { - throw new InvalidArgumentException(\sprintf('Missing or invalid "title" for %s schema definition.', $schemaType)); + if (\array_key_exists('title', $data) && null !== $data['title'] && !\is_string($data['title'])) { + throw new InvalidArgumentException(\sprintf('Invalid "title" for %s schema definition.', $schemaType)); } } /** - * Build the base JSON structure with type, title, and optional description. + * Build the base JSON structure with type, optional title and description. * * @return array */ protected function buildBaseJson(string $type): array { - $data = [ - 'type' => $type, - 'title' => $this->title, - ]; + $data = ['type' => $type]; + + if (null !== $this->title) { + $data['title'] = $this->title; + } if (null !== $this->description) { $data['description'] = $this->description; diff --git a/src/Schema/Elicitation/BooleanSchemaDefinition.php b/src/Schema/Elicitation/BooleanSchemaDefinition.php index 9cfaa8a3..ad2a3b8c 100644 --- a/src/Schema/Elicitation/BooleanSchemaDefinition.php +++ b/src/Schema/Elicitation/BooleanSchemaDefinition.php @@ -19,12 +19,12 @@ final class BooleanSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param string|null $description Optional description/help text * @param bool|null $default Optional default value */ public function __construct( - string $title, + ?string $title, ?string $description = null, public readonly ?bool $default = null, ) { @@ -33,7 +33,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * description?: string, * default?: bool, * } $data @@ -43,7 +43,7 @@ public static function fromArray(array $data): self self::validateTitle($data, 'boolean'); return new self( - title: $data['title'], + title: $data['title'] ?? null, description: $data['description'] ?? null, default: isset($data['default']) ? (bool) $data['default'] : null, ); @@ -52,7 +52,7 @@ public static function fromArray(array $data): self /** * @return array{ * type: string, - * title: string, + * title?: string, * description?: string, * default?: bool, * } diff --git a/src/Schema/Elicitation/EnumSchemaDefinition.php b/src/Schema/Elicitation/EnumSchemaDefinition.php index 003bf354..3f9c636c 100644 --- a/src/Schema/Elicitation/EnumSchemaDefinition.php +++ b/src/Schema/Elicitation/EnumSchemaDefinition.php @@ -30,7 +30,7 @@ final class EnumSchemaDefinition extends AbstractSchemaDefinition * @param string[]|null $enumNames Optional human-readable labels for each enum value */ public function __construct( - string $title, + ?string $title, public readonly array $enum, ?string $description = null, public readonly ?string $default = null, @@ -59,7 +59,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * enum: string[], * description?: string, * default?: string, @@ -75,7 +75,7 @@ public static function fromArray(array $data): self } return new self( - title: $data['title'], + title: $data['title'] ?? null, enum: $data['enum'], description: $data['description'] ?? null, default: $data['default'] ?? null, @@ -86,7 +86,7 @@ enumNames: $data['enumNames'] ?? null, /** * @return array{ * type: string, - * title: string, + * title?: string, * enum: string[], * description?: string, * default?: string, @@ -95,11 +95,13 @@ enumNames: $data['enumNames'] ?? null, */ public function jsonSerialize(): array { - $data = [ - 'type' => 'string', - 'title' => $this->title, - 'enum' => $this->enum, - ]; + $data = ['type' => 'string']; + + if (null !== $this->title) { + $data['title'] = $this->title; + } + + $data['enum'] = $this->enum; if (null !== $this->description) { $data['description'] = $this->description; diff --git a/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php b/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php index 28046bcf..b02e7591 100644 --- a/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php +++ b/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php @@ -31,7 +31,7 @@ final class MultiSelectEnumSchemaDefinition extends AbstractSchemaDefinition * @param int|null $maxItems Optional maximum number of selections */ public function __construct( - string $title, + ?string $title, public readonly array $enum, ?string $description = null, public readonly ?array $default = null, @@ -73,7 +73,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * items: array{type: string, enum: string[]}, * description?: string, * default?: string[], @@ -90,7 +90,7 @@ public static function fromArray(array $data): self } return new self( - title: $data['title'], + title: $data['title'] ?? null, enum: $data['items']['enum'], description: $data['description'] ?? null, default: $data['default'] ?? null, diff --git a/src/Schema/Elicitation/NumberSchemaDefinition.php b/src/Schema/Elicitation/NumberSchemaDefinition.php index 0ad32771..149745b5 100644 --- a/src/Schema/Elicitation/NumberSchemaDefinition.php +++ b/src/Schema/Elicitation/NumberSchemaDefinition.php @@ -31,7 +31,7 @@ final class NumberSchemaDefinition extends AbstractSchemaDefinition * @param int|float|null $maximum Optional maximum value (inclusive) */ public function __construct( - string $title, + ?string $title, public readonly bool $integerOnly = false, ?string $description = null, public readonly int|float|null $default = null, @@ -60,7 +60,7 @@ public function __construct( /** * @param array{ * type: string, - * title: string, + * title?: string, * description?: string, * default?: int|float, * minimum?: int|float, @@ -75,7 +75,7 @@ public static function fromArray(array $data): self $integerOnly = 'integer' === $type; return new self( - title: $data['title'], + title: $data['title'] ?? null, integerOnly: $integerOnly, description: $data['description'] ?? null, default: $data['default'] ?? null, @@ -87,7 +87,7 @@ public static function fromArray(array $data): self /** * @return array{ * type: string, - * title: string, + * title?: string, * description?: string, * default?: int|float, * minimum?: int|float, diff --git a/src/Schema/Elicitation/StringSchemaDefinition.php b/src/Schema/Elicitation/StringSchemaDefinition.php index 76319261..59435e43 100644 --- a/src/Schema/Elicitation/StringSchemaDefinition.php +++ b/src/Schema/Elicitation/StringSchemaDefinition.php @@ -25,7 +25,7 @@ final class StringSchemaDefinition extends AbstractSchemaDefinition private const VALID_FORMATS = ['date', 'date-time', 'email', 'uri']; /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param string|null $description Optional description/help text * @param string|null $default Optional default value * @param string|null $format Optional format constraint (date, date-time, email, uri) @@ -33,7 +33,7 @@ final class StringSchemaDefinition extends AbstractSchemaDefinition * @param int|null $maxLength Optional maximum string length */ public function __construct( - string $title, + ?string $title, ?string $description = null, public readonly ?string $default = null, public readonly ?string $format = null, @@ -61,7 +61,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * description?: string, * default?: string, * format?: string, @@ -74,7 +74,7 @@ public static function fromArray(array $data): self self::validateTitle($data, 'string'); return new self( - title: $data['title'], + title: $data['title'] ?? null, description: $data['description'] ?? null, default: $data['default'] ?? null, format: $data['format'] ?? null, @@ -86,7 +86,7 @@ public static function fromArray(array $data): self /** * @return array{ * type: string, - * title: string, + * title?: string, * description?: string, * default?: string, * format?: string, diff --git a/src/Schema/Elicitation/TitledEnumSchemaDefinition.php b/src/Schema/Elicitation/TitledEnumSchemaDefinition.php index 54eb568b..f6c47d4f 100644 --- a/src/Schema/Elicitation/TitledEnumSchemaDefinition.php +++ b/src/Schema/Elicitation/TitledEnumSchemaDefinition.php @@ -24,13 +24,13 @@ final class TitledEnumSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param list $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, + ?string $title, public readonly array $oneOf, ?string $description = null, public readonly ?string $default = null, @@ -59,7 +59,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * oneOf: list, * description?: string, * default?: string, @@ -74,7 +74,7 @@ public static function fromArray(array $data): self } return new self( - title: $data['title'], + title: $data['title'] ?? null, oneOf: $data['oneOf'], description: $data['description'] ?? null, default: $data['default'] ?? null, diff --git a/src/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinition.php b/src/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinition.php index baab9b0b..ee49a053 100644 --- a/src/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinition.php +++ b/src/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinition.php @@ -23,7 +23,7 @@ final class TitledMultiSelectEnumSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param list $anyOf Array of const/title pairs * @param string|null $description Optional description/help text * @param string[]|null $default Optional default selected values (must be subset of anyOf consts) @@ -31,7 +31,7 @@ final class TitledMultiSelectEnumSchemaDefinition extends AbstractSchemaDefiniti * @param int|null $maxItems Optional maximum number of selections */ public function __construct( - string $title, + ?string $title, public readonly array $anyOf, ?string $description = null, public readonly ?array $default = null, @@ -78,7 +78,7 @@ public function __construct( /** * @param array{ - * title: string, + * title?: string, * items: array{anyOf: list}, * description?: string, * default?: string[], @@ -95,7 +95,7 @@ public static function fromArray(array $data): self } return new self( - title: $data['title'], + title: $data['title'] ?? null, anyOf: $data['items']['anyOf'], description: $data['description'] ?? null, default: $data['default'] ?? null, diff --git a/tests/Unit/Schema/Elicitation/BooleanSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/BooleanSchemaDefinitionTest.php index fefea478..a9e11732 100644 --- a/tests/Unit/Schema/Elicitation/BooleanSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/BooleanSchemaDefinitionTest.php @@ -71,13 +71,23 @@ public function testFromArrayWithAllParams(): void $this->assertTrue($schema->default); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = BooleanSchemaDefinition::fromArray([]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for boolean schema definition.'); /* @phpstan-ignore argument.type */ - BooleanSchemaDefinition::fromArray([]); + BooleanSchemaDefinition::fromArray(['title' => 42]); } public function testJsonSerializeWithMinimalParams(): void diff --git a/tests/Unit/Schema/Elicitation/EnumSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/EnumSchemaDefinitionTest.php index 1bd55ccd..ea200a86 100644 --- a/tests/Unit/Schema/Elicitation/EnumSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/EnumSchemaDefinitionTest.php @@ -114,13 +114,23 @@ public function testFromArrayWithAllParams(): void $this->assertSame(['Poor', 'Fair', 'Good'], $schema->enumNames); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = EnumSchemaDefinition::fromArray(['enum' => ['a', 'b']]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for enum schema definition.'); /* @phpstan-ignore argument.type */ - EnumSchemaDefinition::fromArray(['enum' => ['a', 'b']]); + EnumSchemaDefinition::fromArray(['title' => 42]); } public function testFromArrayWithMissingEnum(): void diff --git a/tests/Unit/Schema/Elicitation/MultiSelectEnumSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/MultiSelectEnumSchemaDefinitionTest.php index 167ad08e..208f5d2c 100644 --- a/tests/Unit/Schema/Elicitation/MultiSelectEnumSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/MultiSelectEnumSchemaDefinitionTest.php @@ -137,13 +137,26 @@ public function testFromArrayWithAllParams(): void $this->assertSame(3, $schema->maxItems); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = MultiSelectEnumSchemaDefinition::fromArray([ + 'items' => ['type' => 'string', 'enum' => ['a']], + ]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for multi-select enum schema definition.'); /* @phpstan-ignore argument.type */ MultiSelectEnumSchemaDefinition::fromArray([ + 'title' => 42, 'items' => ['type' => 'string', 'enum' => ['a']], ]); } diff --git a/tests/Unit/Schema/Elicitation/NumberSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/NumberSchemaDefinitionTest.php index 8ebdee74..ea040382 100644 --- a/tests/Unit/Schema/Elicitation/NumberSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/NumberSchemaDefinitionTest.php @@ -133,13 +133,23 @@ public function testFromArrayWithAllParams(): void $this->assertSame(10, $schema->maximum); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = NumberSchemaDefinition::fromArray(['type' => 'integer']); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for number schema definition.'); /* @phpstan-ignore argument.type */ - NumberSchemaDefinition::fromArray(['type' => 'integer']); + NumberSchemaDefinition::fromArray(['title' => 42]); } public function testJsonSerializeAsInteger(): void diff --git a/tests/Unit/Schema/Elicitation/StringSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/StringSchemaDefinitionTest.php index dbb3277d..02998cce 100644 --- a/tests/Unit/Schema/Elicitation/StringSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/StringSchemaDefinitionTest.php @@ -114,13 +114,23 @@ public function testFromArrayWithAllParams(): void $this->assertSame(100, $schema->maxLength); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = StringSchemaDefinition::fromArray([]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for string schema definition.'); /* @phpstan-ignore argument.type */ - StringSchemaDefinition::fromArray([]); + StringSchemaDefinition::fromArray(['title' => 42]); } public function testJsonSerializeWithMinimalParams(): void diff --git a/tests/Unit/Schema/Elicitation/TitledEnumSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/TitledEnumSchemaDefinitionTest.php index f8c706f0..26502832 100644 --- a/tests/Unit/Schema/Elicitation/TitledEnumSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/TitledEnumSchemaDefinitionTest.php @@ -124,13 +124,23 @@ public function testFromArrayWithAllParams(): void $this->assertSame('b', $schema->default); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = TitledEnumSchemaDefinition::fromArray(['oneOf' => [['const' => 'a', 'title' => 'A']]]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for titled enum schema definition.'); /* @phpstan-ignore argument.type */ - TitledEnumSchemaDefinition::fromArray(['oneOf' => [['const' => 'a', 'title' => 'A']]]); + TitledEnumSchemaDefinition::fromArray(['title' => 42]); } public function testFromArrayWithMissingOneOf(): void diff --git a/tests/Unit/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinitionTest.php b/tests/Unit/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinitionTest.php index 6c1b776e..170893fb 100644 --- a/tests/Unit/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinitionTest.php +++ b/tests/Unit/Schema/Elicitation/TitledMultiSelectEnumSchemaDefinitionTest.php @@ -166,13 +166,26 @@ public function testFromArrayWithAllParams(): void $this->assertSame(2, $schema->maxItems); } - public function testFromArrayWithMissingTitle(): void + public function testFromArrayWithoutTitleIsAccepted(): void + { + // `title` is optional in the specification, so a server that omits it + // must still be readable. + $schema = TitledMultiSelectEnumSchemaDefinition::fromArray([ + 'items' => ['anyOf' => [['const' => 'a', 'title' => 'A']]], + ]); + + $this->assertNull($schema->title); + $this->assertArrayNotHasKey('title', $schema->jsonSerialize()); + } + + public function testFromArrayRejectsNonStringTitle(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing or invalid "title"'); + $this->expectExceptionMessage('Invalid "title" for titled multi-select enum schema definition.'); /* @phpstan-ignore argument.type */ TitledMultiSelectEnumSchemaDefinition::fromArray([ + 'title' => 42, 'items' => ['anyOf' => [['const' => 'a', 'title' => 'A']]], ]); } From 3546d94f8dacc6640168007a07e6ea31f592101f Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 18 Aug 2026 09:23:04 +0200 Subject: [PATCH 2/4] Fix stale title docblocks; allow omitting title where safe --- src/Schema/Elicitation/BooleanSchemaDefinition.php | 2 +- src/Schema/Elicitation/EnumSchemaDefinition.php | 2 +- src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php | 2 +- src/Schema/Elicitation/NumberSchemaDefinition.php | 2 +- src/Schema/Elicitation/StringSchemaDefinition.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Schema/Elicitation/BooleanSchemaDefinition.php b/src/Schema/Elicitation/BooleanSchemaDefinition.php index ad2a3b8c..39766858 100644 --- a/src/Schema/Elicitation/BooleanSchemaDefinition.php +++ b/src/Schema/Elicitation/BooleanSchemaDefinition.php @@ -24,7 +24,7 @@ final class BooleanSchemaDefinition extends AbstractSchemaDefinition * @param bool|null $default Optional default value */ public function __construct( - ?string $title, + ?string $title = null, ?string $description = null, public readonly ?bool $default = null, ) { diff --git a/src/Schema/Elicitation/EnumSchemaDefinition.php b/src/Schema/Elicitation/EnumSchemaDefinition.php index 3f9c636c..1eaef8b5 100644 --- a/src/Schema/Elicitation/EnumSchemaDefinition.php +++ b/src/Schema/Elicitation/EnumSchemaDefinition.php @@ -23,7 +23,7 @@ final class EnumSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param string[] $enum Array of allowed string values * @param string|null $description Optional description/help text * @param string|null $default Optional default value (must be in enum) diff --git a/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php b/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php index b02e7591..ce2ab08e 100644 --- a/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php +++ b/src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php @@ -23,7 +23,7 @@ final class MultiSelectEnumSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param string[] $enum Array of allowed string values * @param string|null $description Optional description/help text * @param string[]|null $default Optional default selected values (must be subset of enum) diff --git a/src/Schema/Elicitation/NumberSchemaDefinition.php b/src/Schema/Elicitation/NumberSchemaDefinition.php index 149745b5..d6b03758 100644 --- a/src/Schema/Elicitation/NumberSchemaDefinition.php +++ b/src/Schema/Elicitation/NumberSchemaDefinition.php @@ -23,7 +23,7 @@ final class NumberSchemaDefinition extends AbstractSchemaDefinition { /** - * @param string $title Human-readable title for the field + * @param ?string $title Optional human-readable title for the field * @param bool $integerOnly Whether to restrict to integer values only * @param string|null $description Optional description/help text * @param int|float|null $default Optional default value diff --git a/src/Schema/Elicitation/StringSchemaDefinition.php b/src/Schema/Elicitation/StringSchemaDefinition.php index 59435e43..36c5e447 100644 --- a/src/Schema/Elicitation/StringSchemaDefinition.php +++ b/src/Schema/Elicitation/StringSchemaDefinition.php @@ -33,7 +33,7 @@ final class StringSchemaDefinition extends AbstractSchemaDefinition * @param int|null $maxLength Optional maximum string length */ public function __construct( - ?string $title, + ?string $title = null, ?string $description = null, public readonly ?string $default = null, public readonly ?string $format = null, From c93326239434c8adbf0164c850bb2203a1e34be1 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 18 Aug 2026 09:36:17 +0200 Subject: [PATCH 3/4] Give Number's title param a default too --- src/Schema/Elicitation/NumberSchemaDefinition.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Elicitation/NumberSchemaDefinition.php b/src/Schema/Elicitation/NumberSchemaDefinition.php index d6b03758..d2bf540c 100644 --- a/src/Schema/Elicitation/NumberSchemaDefinition.php +++ b/src/Schema/Elicitation/NumberSchemaDefinition.php @@ -31,7 +31,7 @@ final class NumberSchemaDefinition extends AbstractSchemaDefinition * @param int|float|null $maximum Optional maximum value (inclusive) */ public function __construct( - ?string $title, + ?string $title = null, public readonly bool $integerOnly = false, ?string $description = null, public readonly int|float|null $default = null, From 5f853daca27eb14334ba316fc30ca9f8d92bbd13 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 18 Aug 2026 18:51:30 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Christopher Hertel --- CHANGELOG.md | 1 - src/Schema/Elicitation/AbstractSchemaDefinition.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b732c041..c02faf30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ All notable changes to `mcp/sdk` will be documented in this file. 0.8.0 ----- -* Fix elicitation schema definitions requiring `title`, which the specification makes optional — a client reading a conformant server's `elicitation/create` refused the request outright. `title` is now nullable on all seven definitions and omitted from the serialized form when absent. * [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name` — the spec allows any string (its own examples use `main.rs` and `Project Files`). URI/URI-template validation is unchanged. * Add `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, and `ClientCapabilities::withExtensions()` so clients can negotiate and check protocol extensions (e.g. MCP Apps) the same way servers already do. [BC Break] `ServerExtensionInterface` is replaced by the side-agnostic `Mcp\Schema\Extension\ExtensionInterface`. * Deprecate Roots, Sampling and Logging per SEP-2577 (protocol revision `2026-07-28`, earliest removal `2027-07-28`). They keep working but using them now triggers a deprecation notice — migrate to tool arguments/resource URIs, a direct LLM provider API, and stderr/OpenTelemetry respectively. diff --git a/src/Schema/Elicitation/AbstractSchemaDefinition.php b/src/Schema/Elicitation/AbstractSchemaDefinition.php index 547d44a5..a05135f1 100644 --- a/src/Schema/Elicitation/AbstractSchemaDefinition.php +++ b/src/Schema/Elicitation/AbstractSchemaDefinition.php @@ -29,10 +29,6 @@ public function __construct( /** * Reject a title that is present but not a string. * - * The specification makes `title` optional on every elicitation schema, so - * its absence is not an error — and treating it as one would have this - * client refuse to read a conformant server's request. - * * @param array $data * * @throws InvalidArgumentException