From a6d0363ff818561886187d49e4a9e5e1214fe9b2 Mon Sep 17 00:00:00 2001 From: Andrei Fedoseev Date: Wed, 11 Nov 2026 15:08:34 +0100 Subject: [PATCH] [Server] Complete the resource template a completion reference names --- CHANGELOG.md | 1 + .../Request/CompletionCompleteHandler.php | 9 ++++++++ .../Request/CompletionCompleteHandlerTest.php | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb0b7062f..cb6e3d98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Fix a resource whose text is not valid UTF-8 failing `resources/read`: `ResourceResultFormatter` sends it as a blob with the same MIME type. * Encode arrays and objects in results and `TextContent` as compact JSON instead of pretty-printed JSON. * Fix `EnumCompletionProvider` and `ListCompletionProvider` offering all values for the prefix `"0"`. + * Fix `completion/complete` for a resource template completing the arguments of another template whose pattern matches the referenced URI template, e.g. `weather://{city}` for `weather://{city},{country}`: the template named by the reference is used. * Log the name of a called tool at info level; its arguments stay at debug level. * Discovery and registration * [BC Break] `Builder::build()` and `Builder::buildStateless()` throw a `RuntimeException` when `setDiscovery()` is used without `symfony/finder` and without a custom discoverer, instead of logging a warning and discovering nothing. diff --git a/src/Server/Handler/Request/CompletionCompleteHandler.php b/src/Server/Handler/Request/CompletionCompleteHandler.php index aa708703b..9fa6fc5a3 100644 --- a/src/Server/Handler/Request/CompletionCompleteHandler.php +++ b/src/Server/Handler/Request/CompletionCompleteHandler.php @@ -105,6 +105,15 @@ public function handle(Request $request, SessionInterface $session): Response|Er */ private function resourceCompletionProviders(string $uri): array { + // Clients name a template by its URI template, which another template's pattern may match too + if (str_contains($uri, '{')) { + try { + return $this->registry->getResourceTemplate($uri)->completionProviders; + } catch (ResourceNotFoundException) { + // Not a registered template: matched below like any other URI. + } + } + $reference = $this->registry->getResource($uri); return $reference instanceof ResourceTemplateReference ? $reference->completionProviders : []; diff --git a/tests/Unit/Server/Handler/Request/CompletionCompleteHandlerTest.php b/tests/Unit/Server/Handler/Request/CompletionCompleteHandlerTest.php index 69f923744..8d3efccff 100644 --- a/tests/Unit/Server/Handler/Request/CompletionCompleteHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/CompletionCompleteHandlerTest.php @@ -16,7 +16,9 @@ use Mcp\Schema\Request\CompletionCompleteRequest; use Mcp\Schema\ResourceTemplate; use Mcp\Schema\Result\CompletionCompleteResult; +use Mcp\Server\Capability\Completion\ListCompletionProvider; use Mcp\Server\Capability\Completion\ProviderInterface; +use Mcp\Server\Capability\Registry; use Mcp\Server\Capability\Registry\ResourceReference; use Mcp\Server\Capability\Registry\ResourceTemplateReference; use Mcp\Server\Capability\RegistryInterface; @@ -82,6 +84,26 @@ public function testReturnsCompletionsForResourceTemplate(): void $this->assertEquals(new CompletionCompleteResult(['alpha'], 1, false), $response->result); } + public function testCompletesTheTemplateTheReferenceNames(): void + { + $registry = new Registry(); + $registry->registerResourceTemplate(new ResourceTemplate('weather://{city}', 'city'), static fn () => null, [ + 'city' => new ListCompletionProvider(['Paris', 'Madrid']), + ]); + $registry->registerResourceTemplate(new ResourceTemplate('weather://{city},{country}', 'city_in_country'), static fn () => null, [ + 'city' => new ListCompletionProvider(['Paris', 'Madrid']), + 'country' => new ListCompletionProvider(['France', 'Spain']), + ]); + + $handler = new CompletionCompleteHandler($registry); + $request = $this->createCompletionRequest('weather://{city},{country}', ['name' => 'country', 'value' => '']); + + $response = $handler->handle($request, $this->session); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(new CompletionCompleteResult(['France', 'Spain'], 2, false), $response->result); + } + public function testLogsProviderFailureBeforeReturningInternalError(): void { $uri = 'file://users/alice';