From d5351759d7cd071aa04ff25a405f69116227f888 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 18 Aug 2026 02:09:07 +0200 Subject: [PATCH] [Schema] Drop SDK-only name pattern on resources and resource templates (#430) * [Schema] Drop SDK-only name pattern on resources and resource templates * Update src/Schema/ResourceDefinition.php * Update src/Schema/ResourceTemplate.php --- CHANGELOG.md | 1 + src/Schema/ResourceDefinition.php | 8 ------- src/Schema/ResourceTemplate.php | 8 ------- tests/Unit/Schema/ResourceDefinitionTest.php | 24 ++++++++++++++++++++ tests/Unit/Schema/ResourceTemplateTest.php | 24 ++++++++++++++++++++ 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c5324..c980e05 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 ----- +* [BC Break] Drop the SDK-only `[a-zA-Z0-9_-]+` restriction on `ResourceDefinition::$name` and `ResourceTemplate::$name`: the specification puts no pattern on `name` (its own examples use `main.rs` and `Project Files`), and the classes also parse what a peer sent, so the rule rejected conformant servers. Any string is now accepted; the URI / URI template validation is unchanged. * Add `ClientGateway::supportsExtension()` to check whether the client negotiated a protocol extension (e.g. `McpApps::EXTENSION_ID`) before offering UI-linked tools, plus `Client\Builder::enableExtension()` and `ClientCapabilities::withExtensions()` so hosts advertise extensions the same way servers do. [BC Break] `ServerExtensionInterface` is replaced by the side-agnostic `Mcp\Schema\Extension\ExtensionInterface`. * Deprecate the Roots, Sampling and Logging features per SEP-2577 (protocol revision `2026-07-28`, earliest removal `2027-07-28`): the schema, client and server classes that make them up are marked `@deprecated` with the suggested migration (tool arguments or resource URIs instead of roots; an LLM provider's API instead of sampling; stderr or OpenTelemetry instead of logging), and exercising them — `ClientGateway::log()` / `sample()` / `listRoots()`, or registering a client-side `SamplingRequestHandler`, `ListRootsRequestHandler` or `LoggingNotificationHandler` — triggers a silenced `E_USER_DEPRECATED` via `symfony/deprecation-contracts`. Everything remains fully functional until removal. * Always emit `{}` for empty tool schemas: `Tool` recursively normalizes every empty sub-schema — `properties`, `items`, `additionalProperties`, `$defs`, combinators and the other draft-07 to 2020-12 schema keywords — in the constructor, for both `inputSchema` and `outputSchema`, so an object position is never serialized as `[]`. diff --git a/src/Schema/ResourceDefinition.php b/src/Schema/ResourceDefinition.php index ca9d0e6..69c44d5 100644 --- a/src/Schema/ResourceDefinition.php +++ b/src/Schema/ResourceDefinition.php @@ -35,11 +35,6 @@ use Mcp\Exception\InvalidArgumentException; */ class ResourceDefinition implements \JsonSerializable { - /** - * Resource name pattern regex - must contain only alphanumeric characters, underscores, and hyphens. - */ - private const RESOURCE_NAME_PATTERN = '/^[a-zA-Z0-9_-]+$/'; - /** * URI pattern regex - requires a valid scheme followed by colon and optional path (RFC 3986). * Example patterns: file://path, db://table, urn:isbn:123, config:key, etc. @@ -68,9 +63,6 @@ class ResourceDefinition implements \JsonSerializable public readonly ?array $icons = null, public readonly ?array $meta = null, ) { - if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) { - throw new InvalidArgumentException(\sprintf('Invalid resource name "%s": must contain only alphanumeric characters, underscores, and hyphens.', $name)); - } if (!preg_match(self::URI_PATTERN, $uri)) { throw new InvalidArgumentException(\sprintf('Invalid resource URI: "%s" must be a valid URI with a scheme and optional path.', $uri)); } diff --git a/src/Schema/ResourceTemplate.php b/src/Schema/ResourceTemplate.php index a26e8b9..4603397 100644 --- a/src/Schema/ResourceTemplate.php +++ b/src/Schema/ResourceTemplate.php @@ -32,11 +32,6 @@ use Mcp\Exception\InvalidArgumentException; */ class ResourceTemplate implements \JsonSerializable { - /** - * Resource name pattern regex - must contain only alphanumeric characters, underscores, and hyphens. - */ - private const RESOURCE_NAME_PATTERN = '/^[a-zA-Z0-9_-]+$/'; - /** * URI Template pattern regex - requires a valid scheme followed by colon and path with at least one placeholder (RFC 3986). * Example patterns: file://{path}/contents.txt, db://{table}/{id}, config:{key}, etc. @@ -61,9 +56,6 @@ class ResourceTemplate implements \JsonSerializable public readonly ?Annotations $annotations = null, public readonly ?array $meta = null, ) { - if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) { - throw new InvalidArgumentException(\sprintf('Invalid resource name "%s": must contain only alphanumeric characters, underscores, and hyphens.', $name)); - } if (!preg_match(self::URI_TEMPLATE_PATTERN, $uriTemplate)) { throw new InvalidArgumentException(\sprintf('Invalid URI template : "%s" must be a valid URI template with at least one placeholder.', $uriTemplate)); } diff --git a/tests/Unit/Schema/ResourceDefinitionTest.php b/tests/Unit/Schema/ResourceDefinitionTest.php index ab39f41..e6bbe56 100644 --- a/tests/Unit/Schema/ResourceDefinitionTest.php +++ b/tests/Unit/Schema/ResourceDefinitionTest.php @@ -116,6 +116,30 @@ class ResourceDefinitionTest extends TestCase $this->assertArrayNotHasKey('title', $data); } + /** + * The specification puts no pattern on `name`; its own examples use + * names such as "main.rs" and "Project Files". + */ + #[DataProvider('provideSpecExampleNames')] + public function testConstructorAcceptsAnyName(string $name): void + { + $resource = new ResourceDefinition( + uri: self::VALID_URI, + name: $name, + ); + + $this->assertSame($name, $resource->name); + } + + public static function provideSpecExampleNames(): iterable + { + yield 'file name with dot' => ['main.rs']; + yield 'display name with space' => ['Project Files']; + yield 'uppercase file name' => ['README.md']; + yield 'namespaced' => ['acme/reports:quarterly']; + yield 'unicode' => ['Ünïcödé 名前']; + } + #[DataProvider('provideInvalidResources')] public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void { diff --git a/tests/Unit/Schema/ResourceTemplateTest.php b/tests/Unit/Schema/ResourceTemplateTest.php index 3e2908d..b99c9d0 100644 --- a/tests/Unit/Schema/ResourceTemplateTest.php +++ b/tests/Unit/Schema/ResourceTemplateTest.php @@ -114,6 +114,30 @@ class ResourceTemplateTest extends TestCase $this->assertArrayNotHasKey('title', $data); } + /** + * The specification puts no pattern on `name`; its own examples use + * names such as "main.rs" and "Project Files". + */ + #[DataProvider('provideSpecExampleNames')] + public function testConstructorAcceptsAnyName(string $name): void + { + $resource = new ResourceTemplate( + uriTemplate: self::VALID_URI, + name: $name, + ); + + $this->assertSame($name, $resource->name); + } + + public static function provideSpecExampleNames(): iterable + { + yield 'file name with dot' => ['main.rs']; + yield 'display name with space' => ['Project Files']; + yield 'uppercase file name' => ['README.md']; + yield 'namespaced' => ['acme/reports:quarterly']; + yield 'unicode' => ['Ünïcödé 名前']; + } + #[DataProvider('provideInvalidResources')] public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void {