From 90e24361f4bc43ab4cf010952f4729bd6e14a98e Mon Sep 17 00:00:00 2001 From: Pratham Goyal Date: Sat, 24 Oct 2026 07:53:47 +0000 Subject: [PATCH] fix: use the referenced resource template for completion/complete A completion/complete request for a resource template looked the template up by matching its URI template against every registered template's pattern. Another template whose pattern happens to match that string, such as test://{category}/{id} for test://items/{itemId}, could be checked first, and the request then failed with "Argument not found" for a valid argument. Look the template up by its URI template first, in both McpAsyncServer and McpStatelessAsyncServer. --- .../server/McpAsyncServer.java | 6 +- .../server/McpStatelessAsyncServer.java | 6 +- .../ResourceTemplateManagementTests.java | 96 +++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 32e76264e..60dd6ff22 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -1094,7 +1094,11 @@ private McpRequestHandler completionCompleteRequestHan } } else { - var templateSpec = this.findResourceTemplateSpecification(resourceReference.uri()).orElse(null); + // The reference names a template: matching it as a URI could pick + // another template whose pattern also matches it + var templateSpec = Optional.ofNullable(this.resourceTemplates.get(resourceReference.uri())) + .or(() -> this.findResourceTemplateSpecification(resourceReference.uri())) + .orElse(null); if (templateSpec != null) { if (!uriTemplateManagerFactory.create(templateSpec.resourceTemplate().uriTemplate()) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 95b694e7f..be142b746 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -831,7 +831,11 @@ private McpStatelessRequestHandler completionCompleteR } } else { - var templateSpec = this.findResourceTemplateSpecification(resourceReference.uri()).orElse(null); + // The reference names a template: matching it as a URI could pick + // another template whose pattern also matches it + var templateSpec = Optional.ofNullable(this.resourceTemplates.get(resourceReference.uri())) + .or(() -> this.findResourceTemplateSpecification(resourceReference.uri())) + .orElse(null); if (templateSpec != null) { if (!uriTemplateManagerFactory.create(templateSpec.resourceTemplate().uriTemplate()) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java index 927398441..9b4d92b78 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java @@ -6,14 +6,23 @@ import java.time.Duration; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import io.modelcontextprotocol.MockMcpServerTransport; import io.modelcontextprotocol.MockMcpServerTransportProvider; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonDefaults; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; +import io.modelcontextprotocol.spec.McpSchema.CompleteResult; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.spec.McpSchema.ResourceReference; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.spec.McpStatelessServerTransport; +import io.modelcontextprotocol.spec.ProtocolVersions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -57,6 +66,93 @@ void tearDown() { // Async Resource Template Tests // --------------------------------------- + @Test + void testCompletionUsesTheReferencedResourceTemplate() { + // The referenced template also matches the generic one, which is checked first + String itemTemplate = "test://items/{itemId}"; + String genericTemplate = "test://{category}/{id}"; + McpServerFeatures.AsyncResourceTemplateSpecification item = new McpServerFeatures.AsyncResourceTemplateSpecification( + ResourceTemplate.builder(itemTemplate, "item").build(), + (exchange, req) -> Mono.just(ReadResourceResult.builder(List.of()).build())); + McpServerFeatures.AsyncResourceTemplateSpecification generic = new McpServerFeatures.AsyncResourceTemplateSpecification( + ResourceTemplate.builder(genericTemplate, "generic").build(), + (exchange, req) -> Mono.just(ReadResourceResult.builder(List.of()).build())); + ResourceReference itemReference = new ResourceReference(itemTemplate); + + mcpAsyncServer = McpServer.async(mockTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().resources(false, false).completions().build()) + .resourceTemplates(generic, item) + .completions(new McpServerFeatures.AsyncCompletionSpecification(itemReference, (exchange, + req) -> Mono.just(new CompleteResult( + new CompleteResult.CompleteCompletion(List.of(req.argument().value() + "1"), 1, false))))) + .build(); + + MockMcpServerTransport transport = mockTransportProvider.getTransport(); + mockTransportProvider.simulateIncomingMessage(new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "init", + McpSchema.InitializeRequest + .builder(ProtocolVersions.MCP_2025_11_25, McpSchema.ClientCapabilities.builder().build(), + McpSchema.Implementation.builder("test-client", "1.0.0").build()) + .build())); + mockTransportProvider + .simulateIncomingMessage(new McpSchema.JSONRPCNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED)); + mockTransportProvider.simulateIncomingMessage(new McpSchema.JSONRPCRequest(McpSchema.METHOD_COMPLETION_COMPLETE, + "complete", + CompleteRequest.builder(itemReference, new CompleteRequest.CompleteArgument("itemId", "4")).build())); + + McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) transport.getLastSentMessage(); + assertThat(response.error()).isNull(); + CompleteResult result = transport.unmarshalFrom(response.result(), new TypeRef() { + }); + assertThat(result.completion().values()).containsExactly("41"); + } + + @Test + void testStatelessCompletionUsesTheReferencedResourceTemplate() { + String itemTemplate = "test://items/{itemId}"; + String genericTemplate = "test://{category}/{id}"; + ResourceReference itemReference = new ResourceReference(itemTemplate); + AtomicReference handler = new AtomicReference<>(); + McpStatelessServerTransport transport = new McpStatelessServerTransport() { + @Override + public void setMcpHandler(McpStatelessServerHandler mcpHandler) { + handler.set(mcpHandler); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + }; + + McpStatelessAsyncServer server = McpServer.async(transport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().resources(false, false).completions().build()) + .resourceTemplates( + new McpStatelessServerFeatures.AsyncResourceTemplateSpecification( + ResourceTemplate.builder(genericTemplate, "generic").build(), + (context, req) -> Mono.just(ReadResourceResult.builder(List.of()).build())), + new McpStatelessServerFeatures.AsyncResourceTemplateSpecification( + ResourceTemplate.builder(itemTemplate, "item").build(), + (context, req) -> Mono.just(ReadResourceResult.builder(List.of()).build()))) + .completions(new McpStatelessServerFeatures.AsyncCompletionSpecification(itemReference, (context, + req) -> Mono.just(new CompleteResult( + new CompleteResult.CompleteCompletion(List.of(req.argument().value() + "1"), 1, false))))) + .build(); + + McpSchema.JSONRPCResponse response = handler.get() + .handleRequest(McpTransportContext.EMPTY, + new McpSchema.JSONRPCRequest(McpSchema.METHOD_COMPLETION_COMPLETE, "complete", + CompleteRequest.builder(itemReference, new CompleteRequest.CompleteArgument("itemId", "4")) + .build())) + .block(Duration.ofSeconds(10)); + + assertThat(response.error()).isNull(); + CompleteResult result = McpJsonDefaults.getMapper().convertValue(response.result(), CompleteResult.class); + assertThat(result.completion().values()).containsExactly("41"); + server.closeGracefully().block(Duration.ofSeconds(10)); + } + @Test void testAddResourceTemplate() { mcpAsyncServer = McpServer.async(mockTransportProvider)