From 088017c3d53a3b753cf76e2d032f483188f9cf64 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Wed, 24 Jun 2026 15:35:41 +0200 Subject: [PATCH 1/2] Add tests for completion requests targeting missing prompt or resource Signed-off-by: Daniel Garnier-Moiroux --- .../HttpServletStatelessIntegrationTests.java | 63 ++++++++++++++++++- .../server/McpCompletionTests.java | 59 ++++++++++++++++- 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index f52709ad9..e6ef833bb 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -4,8 +4,6 @@ package io.modelcontextprotocol.server; -import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; - import java.time.Duration; import java.util.List; import java.util.Map; @@ -19,6 +17,7 @@ import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.spec.HttpHeaders; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; @@ -53,9 +52,12 @@ import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON; import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM; import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; +import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.awaitility.Awaitility.await; @Timeout(15) @@ -337,6 +339,63 @@ void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult(Stri } } + @ParameterizedTest(name = "{0} : Completion call for non-existent prompt") + @ValueSource(strings = { "httpclient" }) + void testCompletionForNonExistentPromptReturnsInvalidParams(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + + try (var mcpClient = clientBuilder.build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CompleteRequest request = CompleteRequest + .builder(new PromptReference("nonexistent-prompt"), new CompleteRequest.CompleteArgument("arg", "val")) + .build(); + + assertThatThrownBy(() -> mcpClient.completeCompletion(request)).isInstanceOf(McpError.class) + .asInstanceOf(type(McpError.class)) + .extracting(McpError::getJsonRpcError) + .extracting(McpSchema.JSONRPCResponse.JSONRPCError::code) + .isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND); + } + finally { + mcpServer.close(); + } + } + + @ParameterizedTest(name = "{0} : Completion call for non-existent resource") + @ValueSource(strings = { "httpclient" }) + void testCompletionForNonExistentResourceReturnsResourceNotFound(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + + try (var mcpClient = clientBuilder.build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CompleteRequest request = CompleteRequest + .builder(new ResourceReference("test://nonexistent/{param}"), + new CompleteRequest.CompleteArgument("param", "val")) + .build(); + + assertThatThrownBy(() -> mcpClient.completeCompletion(request)).isInstanceOf(McpError.class) + .asInstanceOf(type(McpError.class)) + .extracting(McpError::getJsonRpcError) + .extracting(McpSchema.JSONRPCResponse.JSONRPCError::code) + .isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND); + } + finally { + mcpServer.close(); + } + } + // --------------------------------------- // Tool Structured Output Schema Tests // --------------------------------------- diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java index 9a68318dc..482085ec1 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java @@ -21,6 +21,7 @@ import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; import io.modelcontextprotocol.spec.McpSchema.CompleteResult; @@ -28,16 +29,17 @@ import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.Prompt; import io.modelcontextprotocol.spec.McpSchema.PromptArgument; +import io.modelcontextprotocol.spec.McpSchema.PromptReference; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.Resource; import io.modelcontextprotocol.spec.McpSchema.ResourceReference; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; -import io.modelcontextprotocol.spec.McpSchema.PromptReference; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpError; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; /** * Tests for completion functionality with context support. @@ -273,6 +275,59 @@ void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult() { mcpServer.close(); } + @Test + void testCompletionForNonExistentPromptReturnsInvalidParams() { + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + + try (var mcpClient = clientBuilder + .clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build()) + .build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CompleteRequest request = CompleteRequest + .builder(new PromptReference("nonexistent-prompt"), new CompleteRequest.CompleteArgument("arg", "val")) + .build(); + + assertThatThrownBy(() -> mcpClient.completeCompletion(request)).isInstanceOf(McpError.class) + .asInstanceOf(type(McpError.class)) + .extracting(McpError::getJsonRpcError) + .extracting(McpSchema.JSONRPCResponse.JSONRPCError::code) + .isEqualTo(ErrorCodes.INVALID_PARAMS); + } + + mcpServer.close(); + } + + @Test + void testCompletionForNonExistentResourceReturnsResourceNotFound() { + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + + try (var mcpClient = clientBuilder + .clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build()) + .build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CompleteRequest request = CompleteRequest + .builder(new ResourceReference("test://nonexistent/{param}"), + new CompleteRequest.CompleteArgument("param", "val")) + .build(); + + assertThatThrownBy(() -> mcpClient.completeCompletion(request)).isInstanceOf(McpError.class) + .asInstanceOf(type(McpError.class)) + .extracting(McpError::getJsonRpcError) + .extracting(McpSchema.JSONRPCResponse.JSONRPCError::code) + .isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND); + } + + mcpServer.close(); + } + @Test void testDependentCompletionScenario() { BiFunction completionHandler = (exchange, request) -> { From 23accdf38f3da11ee6ae94942dcdc36b88cc46e5 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Wed, 24 Jun 2026 15:40:13 +0200 Subject: [PATCH 2/2] Change @ParameterizedTest -> @Test in HttpServletStatelessServerTransport Signed-off-by: Daniel Garnier-Moiroux --- .../HttpServletStatelessIntegrationTests.java | 111 ++++++------------ 1 file changed, 33 insertions(+), 78 deletions(-) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index e6ef833bb..a792ff5e0 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -42,8 +41,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -69,7 +66,12 @@ class HttpServletStatelessIntegrationTests { private HttpServletStatelessServerTransport mcpStatelessServerTransport; - ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>(); + private final McpClient.SyncSpec clientBuilder = McpClient + .sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) + .endpoint(CUSTOM_MESSAGE_ENDPOINT) + .build()) + .initializationTimeout(Duration.ofHours(10)) + .requestTimeout(Duration.ofHours(10)); private Tomcat tomcat; @@ -87,12 +89,6 @@ public void before() { catch (Exception e) { throw new RuntimeException("Failed to start Tomcat", e); } - - clientBuilders - .put("httpclient", - McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) - .endpoint(CUSTOM_MESSAGE_ENDPOINT) - .build()).initializationTimeout(Duration.ofHours(10)).requestTimeout(Duration.ofHours(10))); } @AfterEach @@ -114,12 +110,8 @@ public void after() { // --------------------------------------- // Tools Tests // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testToolCallSuccess(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testToolCallSuccess() { var callResponse = CallToolResult.builder() .content(List.of(McpSchema.TextContent.builder("CALL RESPONSE").build())) .isError(false) @@ -160,12 +152,8 @@ void testToolCallSuccess(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testInitialize(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testInitialize() { var mcpServer = McpServer.sync(mcpStatelessServerTransport).build(); try (var mcpClient = clientBuilder.build()) { @@ -180,11 +168,8 @@ void testInitialize(String clientType) { // --------------------------------------- // Completion Tests // --------------------------------------- - @ParameterizedTest(name = "{0} : Completion call") - @ValueSource(strings = { "httpclient" }) - void testCompletionShouldReturnExpectedSuggestions(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testCompletionShouldReturnExpectedSuggestions() { var expectedValues = List.of("python", "pytorch", "pyside"); var completionResponse = new CompleteResult(new CompleteResult.CompleteCompletion(expectedValues, 10, // total true // hasMore @@ -235,11 +220,8 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { } } - @ParameterizedTest(name = "{0} : Completion call without matching handler") - @ValueSource(strings = { "httpclient" }) - void testCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testCompletionWithoutMatchingHandlerReturnsEmptyResult() { BiFunction completionHandler = (transportContext, request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false)); @@ -288,11 +270,8 @@ void testCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) { } } - @ParameterizedTest(name = "{0} : Resource template completion call without matching handler") - @ValueSource(strings = { "httpclient" }) - void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult() { BiFunction completionHandler = (transportContext, request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false)); @@ -339,11 +318,8 @@ void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult(Stri } } - @ParameterizedTest(name = "{0} : Completion call for non-existent prompt") - @ValueSource(strings = { "httpclient" }) - void testCompletionForNonExistentPromptReturnsInvalidParams(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testCompletionForNonExistentPromptReturnsInvalidParams() { var mcpServer = McpServer.sync(mcpStatelessServerTransport) .capabilities(ServerCapabilities.builder().completions().build()) .build(); @@ -360,18 +336,15 @@ void testCompletionForNonExistentPromptReturnsInvalidParams(String clientType) { .asInstanceOf(type(McpError.class)) .extracting(McpError::getJsonRpcError) .extracting(McpSchema.JSONRPCResponse.JSONRPCError::code) - .isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND); + .isEqualTo(ErrorCodes.INVALID_PARAMS); } finally { mcpServer.close(); } } - @ParameterizedTest(name = "{0} : Completion call for non-existent resource") - @ValueSource(strings = { "httpclient" }) - void testCompletionForNonExistentResourceReturnsResourceNotFound(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testCompletionForNonExistentResourceReturnsResourceNotFound() { var mcpServer = McpServer.sync(mcpStatelessServerTransport) .capabilities(ServerCapabilities.builder().completions().build()) .build(); @@ -399,11 +372,8 @@ void testCompletionForNonExistentResourceReturnsResourceNotFound(String clientTy // --------------------------------------- // Tool Structured Output Schema Tests // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputValidationSuccess(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputValidationSuccess() { // Create a tool with output schema Map outputSchema = Map.of( "type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", @@ -468,11 +438,8 @@ void testStructuredOutputValidationSuccess(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputOfObjectArrayValidationSuccess() { // Create a tool with output schema that returns an array of objects Map outputSchema = Map .of( // @formatter:off @@ -529,11 +496,8 @@ void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputWithInHandlerError(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputWithInHandlerError() { // Create a tool with output schema Map outputSchema = Map.of( "type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", @@ -587,11 +551,8 @@ void testStructuredOutputWithInHandlerError(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputValidationFailure(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputValidationFailure() { // Create a tool with output schema Map outputSchema = Map.of("type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", Map.of("type", "string")), "required", @@ -639,11 +600,8 @@ void testStructuredOutputValidationFailure(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputMissingStructuredContent(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputMissingStructuredContent() { // Create a tool with output schema Map outputSchema = Map.of("type", "object", "properties", Map.of("result", Map.of("type", "number")), "required", List.of("result")); @@ -688,11 +646,8 @@ void testStructuredOutputMissingStructuredContent(String clientType) { } } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient" }) - void testStructuredOutputRuntimeToolAddition(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - + @Test + void testStructuredOutputRuntimeToolAddition() { // Start server without tools var mcpServer = McpServer.sync(mcpStatelessServerTransport) .serverInfo("test-server", "1.0.0")