From 70655950265950b7ea972e873c8a9b0af6dce3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Thu, 20 Aug 2026 10:49:42 +0200 Subject: [PATCH] Fix error behavior on unregistered handlers in stateless server handler Backport of #800 to the 0.18.x line. Instead of returning Mono.error with an McpError, DefaultMcpStatelessServerHandler now returns a JSON-RPC method not found error (-32601), aligned with the stateful server session handler. The early return meant the error never reached the onErrorResume below it, so it escaped to the transport. HttpServletStatelessServerTransport and the Spring WebMvc/WebFlux stateless transports all map an escaping handler error to HTTP 500, so any request for a method with no registered handler was answered with a server error rather than a JSON-RPC error response. Clients probing for optional methods -- OpenAI's hosted connector sends server/discover before tools/list -- therefore drove 5xx traffic against otherwise healthy servers. Adds the unit test from #800 and an integration test that asserts both the HTTP status and the JSON-RPC error body, since the status is what regressed. See #1085 --- .../DefaultMcpStatelessServerHandler.java | 4 +- ...DefaultMcpStatelessServerHandlerTests.java | 40 +++++++++++++++++++ .../HttpServletStatelessIntegrationTests.java | 40 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) 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 d1b55f594..58c85bd3b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java @@ -32,7 +32,9 @@ public Mono handleRequest(McpTransportContext transpo McpSchema.JSONRPCRequest request) { McpStatelessRequestHandler requestHandler = this.requestHandlers.get(request.method()); if (requestHandler == null) { - return Mono.error(new McpError("Missing handler for request type: " + request.method())); + 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 -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)) 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 92b32dfd9..ce74e43f3 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -650,6 +650,46 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } + @Test + void testMissingHandlerReturnsMethodNotFoundError() throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .build(); + + // "server/discover" has no registered handler, and neither do the capabilities + // this server does not advertise + McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, + "server/discover", "test", null); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); + MockHttpServletResponse response = new MockHttpServletResponse(); + + byte[] content = JSON_MAPPER.writeValueAsBytes(jsonrpcRequest); + request.setContent(content); + request.addHeader("Content-Length", Integer.toString(content.length)); + request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM); + request.addHeader("Content-Type", APPLICATION_JSON); + request.addHeader("Cache-Control", "no-cache"); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + + mcpStatelessServerTransport.service(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + + McpSchema.JSONRPCResponse jsonrpcResponse = JSON_MAPPER.readValue(response.getContentAsByteArray(), + McpSchema.JSONRPCResponse.class); + + assertThat(jsonrpcResponse).isNotNull(); + assertThat(jsonrpcResponse.id()).isEqualTo("test"); + assertThat(jsonrpcResponse.result()).isNull(); + assertThat(jsonrpcResponse.error()).isNotNull(); + assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.METHOD_NOT_FOUND); + assertThat(jsonrpcResponse.error().message()).isEqualTo("Method not found: server/discover"); + + mcpServer.close(); + } + // --------------------------------------- // Bounded read // ---------------------------------------