Skip to content

Commit 7065595

Browse files
committed
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
1 parent 5ba88f6 commit 7065595

3 files changed

Lines changed: 83 additions & 1 deletion

File tree

mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public Mono<McpSchema.JSONRPCResponse> handleRequest(McpTransportContext transpo
3232
McpSchema.JSONRPCRequest request) {
3333
McpStatelessRequestHandler<?> requestHandler = this.requestHandlers.get(request.method());
3434
if (requestHandler == null) {
35-
return Mono.error(new McpError("Missing handler for request type: " + request.method()));
35+
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
36+
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
37+
"Method not found: " + request.method(), null)));
3638
}
3739
return requestHandler.handle(transportContext, request.params())
3840
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server;
6+
7+
import io.modelcontextprotocol.common.McpTransportContext;
8+
import io.modelcontextprotocol.spec.McpSchema;
9+
import org.junit.jupiter.api.Test;
10+
import reactor.test.StepVerifier;
11+
12+
import java.util.Collections;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
class DefaultMcpStatelessServerHandlerTests {
17+
18+
@Test
19+
void testHandleRequestWithUnregisteredMethod() {
20+
// no request/initialization handlers
21+
DefaultMcpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(Collections.emptyMap(),
22+
Collections.emptyMap());
23+
24+
// unregistered method
25+
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "resources/list",
26+
"test-id-123", null);
27+
28+
StepVerifier.create(handler.handleRequest(McpTransportContext.EMPTY, request)).assertNext(response -> {
29+
assertThat(response).isNotNull();
30+
assertThat(response.jsonrpc()).isEqualTo(McpSchema.JSONRPC_VERSION);
31+
assertThat(response.id()).isEqualTo("test-id-123");
32+
assertThat(response.result()).isNull();
33+
34+
assertThat(response.error()).isNotNull();
35+
assertThat(response.error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND);
36+
assertThat(response.error().message()).isEqualTo("Method not found: resources/list");
37+
}).verifyComplete();
38+
}
39+
40+
}

mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,46 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception {
650650
mcpServer.close();
651651
}
652652

653+
@Test
654+
void testMissingHandlerReturnsMethodNotFoundError() throws Exception {
655+
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
656+
.serverInfo("test-server", "1.0.0")
657+
.capabilities(ServerCapabilities.builder().tools(true).build())
658+
.build();
659+
660+
// "server/discover" has no registered handler, and neither do the capabilities
661+
// this server does not advertise
662+
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
663+
"server/discover", "test", null);
664+
665+
MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT);
666+
MockHttpServletResponse response = new MockHttpServletResponse();
667+
668+
byte[] content = JSON_MAPPER.writeValueAsBytes(jsonrpcRequest);
669+
request.setContent(content);
670+
request.addHeader("Content-Length", Integer.toString(content.length));
671+
request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM);
672+
request.addHeader("Content-Type", APPLICATION_JSON);
673+
request.addHeader("Cache-Control", "no-cache");
674+
request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26);
675+
676+
mcpStatelessServerTransport.service(request, response);
677+
678+
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
679+
680+
McpSchema.JSONRPCResponse jsonrpcResponse = JSON_MAPPER.readValue(response.getContentAsByteArray(),
681+
McpSchema.JSONRPCResponse.class);
682+
683+
assertThat(jsonrpcResponse).isNotNull();
684+
assertThat(jsonrpcResponse.id()).isEqualTo("test");
685+
assertThat(jsonrpcResponse.result()).isNull();
686+
assertThat(jsonrpcResponse.error()).isNotNull();
687+
assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.METHOD_NOT_FOUND);
688+
assertThat(jsonrpcResponse.error().message()).isEqualTo("Method not found: server/discover");
689+
690+
mcpServer.close();
691+
}
692+
653693
// ---------------------------------------
654694
// Bounded read
655695
// ---------------------------------------

0 commit comments

Comments
 (0)