From 67747c22419412c802a2ea78f63290d8a17c8d27 Mon Sep 17 00:00:00 2001 From: Mathias Geat Date: Mon, 16 Feb 2026 17:55:42 +0100 Subject: [PATCH 1/2] Fix error behavior on unregistered handlers in stateless server handler Instead of throwing an McpError, it now returns with a JSON-RPC method not found error (-32601), aligned with the stateful implementation. --- .../DefaultMcpStatelessServerHandler.java | 6 +-- ...DefaultMcpStatelessServerHandlerTests.java | 40 ++++++++++++++++++ .../HttpServletStatelessIntegrationTests.java | 42 ++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandlerTests.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java index def40d58d..5cd5de7ad 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java @@ -32,9 +32,9 @@ public Mono handleRequest(McpTransportContext transpo McpSchema.JSONRPCRequest request) { McpStatelessRequestHandler requestHandler = this.requestHandlers.get(request.method()); if (requestHandler == null) { - return Mono.error(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND) - .message("Missing handler for request type: " + request.method()) - .build()); + return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, + new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND, + "Method not found: " + request.method(), null))); } return requestHandler.handle(transportContext, request.params()) .map(result -> McpSchema.JSONRPCResponse.result(request.id(), result)) diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandlerTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandlerTests.java new file mode 100644 index 000000000..267aca504 --- /dev/null +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandlerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.server; + +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultMcpStatelessServerHandlerTests { + + @Test + void testHandleRequestWithUnregisteredMethod() { + // no request/initialization handlers + DefaultMcpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(Collections.emptyMap(), + Collections.emptyMap()); + + // unregistered method + McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "resources/list", + "test-id-123", null); + + StepVerifier.create(handler.handleRequest(McpTransportContext.EMPTY, request)).assertNext(response -> { + assertThat(response).isNotNull(); + assertThat(response.jsonrpc()).isEqualTo(McpSchema.JSONRPC_VERSION); + assertThat(response.id()).isEqualTo("test-id-123"); + assertThat(response.result()).isNull(); + + assertThat(response.error()).isNotNull(); + assertThat(response.error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); + assertThat(response.error().message()).isEqualTo("Method not found: resources/list"); + }).verifyComplete(); + } + +} 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 a792ff5e0..e9252fc1a 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -4,6 +4,10 @@ package io.modelcontextprotocol.server; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; import java.util.Map; @@ -448,7 +452,7 @@ void testStructuredOutputOfObjectArrayValidationSuccess() { "type", "object", "properties", Map.of( "name", Map.of("type", "string"), - "age", Map.of("type", "number")), + "age", Map.of("type", "number")), "required", List.of("name", "age"))); // @formatter:on Tool calculatorTool = Tool.builder("getMembers") @@ -765,6 +769,42 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } + @ParameterizedTest + @ValueSource(strings = { "tools/list", "resources/list", "prompts/list" }) + void testMissingHandlerReturnsMethodNotFoundError(String method) throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + HttpResponse response; + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + PORT + CUSTOM_MESSAGE_ENDPOINT)) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/event-stream") + .POST(HttpRequest.BodyPublishers.ofString(""" + { + "jsonrpc": "2.0", + "method": "%s", + "id": "test-request-123", + "params": {} + } + """.formatted(method))) + .build(); + + response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + } + finally { + mcpServer.closeGracefully(); + } + + final var responseBody = response.body(); + assertThatJson(responseBody).inPath("error.code").isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); + assertThatJson(responseBody).inPath("error.message").isEqualTo("Method not found: " + method); + } + private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) { From a446be457cea2a9796ef010dc3c39a3e405fc85e Mon Sep 17 00:00:00 2001 From: Mathias Geat Date: Wed, 8 Jul 2026 14:11:26 +0200 Subject: [PATCH 2/2] Rewrite testMissingHandlerReturnsMethodNotFoundError using a client transport Mirrors the setup on HttpServletStreamableIntegrationTests. --- .../HttpServletStatelessIntegrationTests.java | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 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 e9252fc1a..a5e2e01ff 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -4,15 +4,12 @@ package io.modelcontextprotocol.server; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import java.util.function.Function; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; @@ -49,6 +46,8 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.client.RestClient; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON; import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM; @@ -769,40 +768,46 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } - @ParameterizedTest - @ValueSource(strings = { "tools/list", "resources/list", "prompts/list" }) - void testMissingHandlerReturnsMethodNotFoundError(String method) throws Exception { + @Test + void testMissingHandlerReturnsMethodNotFoundError() { var mcpServer = McpServer.sync(mcpStatelessServerTransport) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().build()) .build(); + var clientTransport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) + .endpoint(CUSTOM_MESSAGE_ENDPOINT) + .build(); - HttpResponse response; + try (var mcpClient = McpClient.sync(clientTransport).build()) { + // Create a session using an MCP client + McpSchema.InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + PORT + CUSTOM_MESSAGE_ENDPOINT)) - .header("Content-Type", "application/json") - .header("Accept", "application/json, text/event-stream") - .POST(HttpRequest.BodyPublishers.ofString(""" - { - "jsonrpc": "2.0", - "method": "%s", - "id": "test-request-123", - "params": {} + // Override the response handler in the client to capture responses + AtomicReference response = new AtomicReference<>(); + var handler = (Function, Mono>) ( + message) -> message.doOnNext(r -> { + if (r instanceof McpSchema.JSONRPCResponse resp) { + response.set(resp); } - """.formatted(method))) - .build(); + }); + StepVerifier.create(clientTransport.connect(handler)).verifyComplete(); + + // Send a request for a non-existent method through the transport, bypassing + // the client's capability checks + StepVerifier + .create(clientTransport.sendMessage(new McpSchema.JSONRPCRequest("foo/bar", "test-request-123"))) + .verifyComplete(); - response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + // Wait until we've received the response + await().atMost(Duration.ofSeconds(1)).until(() -> response.get() != null); + + assertThat(response.get().error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); + assertThat(response.get().error().message()).isEqualTo("Method not found: foo/bar"); } finally { mcpServer.closeGracefully(); } - - final var responseBody = response.body(); - assertThatJson(responseBody).inPath("error.code").isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); - assertThatJson(responseBody).inPath("error.message").isEqualTo("Method not found: " + method); } private double evaluateExpression(String expression) {