Skip to content

Commit 5d3dece

Browse files
authored
Fix error behavior on unregistered handlers in stateless server handler (#800)
Instead of throwing an McpError, it now returns with a JSON-RPC method not found error (-32601), aligned with the stateful implementation. Closes #784
1 parent dea53fd commit 5d3dece

3 files changed

Lines changed: 89 additions & 4 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +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(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND)
36-
.message("Missing handler for request type: " + request.method())
37-
.build());
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)));
3838
}
3939
return requestHandler.handle(transportContext, request.params())
4040
.map(result -> McpSchema.JSONRPCResponse.result(request.id(), result))
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: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Map;
1010
import java.util.concurrent.atomic.AtomicReference;
1111
import java.util.function.BiFunction;
12+
import java.util.function.Function;
1213

1314
import io.modelcontextprotocol.client.McpClient;
1415
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
@@ -45,6 +46,8 @@
4546
import org.springframework.mock.web.MockHttpServletRequest;
4647
import org.springframework.mock.web.MockHttpServletResponse;
4748
import org.springframework.web.client.RestClient;
49+
import reactor.core.publisher.Mono;
50+
import reactor.test.StepVerifier;
4851

4952
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
5053
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
@@ -448,7 +451,7 @@ void testStructuredOutputOfObjectArrayValidationSuccess() {
448451
"type", "object",
449452
"properties", Map.of(
450453
"name", Map.of("type", "string"),
451-
"age", Map.of("type", "number")),
454+
"age", Map.of("type", "number")),
452455
"required", List.of("name", "age"))); // @formatter:on
453456

454457
Tool calculatorTool = Tool.builder("getMembers")
@@ -765,6 +768,48 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception {
765768
mcpServer.close();
766769
}
767770

771+
@Test
772+
void testMissingHandlerReturnsMethodNotFoundError() {
773+
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
774+
.serverInfo("test-server", "1.0.0")
775+
.capabilities(ServerCapabilities.builder().build())
776+
.build();
777+
var clientTransport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
778+
.endpoint(CUSTOM_MESSAGE_ENDPOINT)
779+
.build();
780+
781+
try (var mcpClient = McpClient.sync(clientTransport).build()) {
782+
// Create a session using an MCP client
783+
McpSchema.InitializeResult initResult = mcpClient.initialize();
784+
assertThat(initResult).isNotNull();
785+
786+
// Override the response handler in the client to capture responses
787+
AtomicReference<McpSchema.JSONRPCResponse> response = new AtomicReference<>();
788+
var handler = (Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>>) (
789+
message) -> message.doOnNext(r -> {
790+
if (r instanceof McpSchema.JSONRPCResponse resp) {
791+
response.set(resp);
792+
}
793+
});
794+
StepVerifier.create(clientTransport.connect(handler)).verifyComplete();
795+
796+
// Send a request for a non-existent method through the transport, bypassing
797+
// the client's capability checks
798+
StepVerifier
799+
.create(clientTransport.sendMessage(new McpSchema.JSONRPCRequest("foo/bar", "test-request-123")))
800+
.verifyComplete();
801+
802+
// Wait until we've received the response
803+
await().atMost(Duration.ofSeconds(1)).until(() -> response.get() != null);
804+
805+
assertThat(response.get().error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND);
806+
assertThat(response.get().error().message()).isEqualTo("Method not found: foo/bar");
807+
}
808+
finally {
809+
mcpServer.closeGracefully();
810+
}
811+
}
812+
768813
private double evaluateExpression(String expression) {
769814
// Simple expression evaluator for testing
770815
return switch (expression) {

0 commit comments

Comments
 (0)