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
This commit is contained in:
parent
dea53fd14c
commit
5d3decebd6
3 changed files with 89 additions and 4 deletions
|
|
@ -32,9 +32,9 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {
|
|||
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))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ 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;
|
||||
|
|
@ -45,6 +46,8 @@ import org.junit.jupiter.api.Timeout;
|
|||
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;
|
||||
|
|
@ -448,7 +451,7 @@ class HttpServletStatelessIntegrationTests {
|
|||
"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 +768,48 @@ class HttpServletStatelessIntegrationTests {
|
|||
mcpServer.close();
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
try (var mcpClient = McpClient.sync(clientTransport).build()) {
|
||||
// Create a session using an MCP client
|
||||
McpSchema.InitializeResult initResult = mcpClient.initialize();
|
||||
assertThat(initResult).isNotNull();
|
||||
|
||||
// Override the response handler in the client to capture responses
|
||||
AtomicReference<McpSchema.JSONRPCResponse> response = new AtomicReference<>();
|
||||
var handler = (Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>>) (
|
||||
message) -> message.doOnNext(r -> {
|
||||
if (r instanceof McpSchema.JSONRPCResponse resp) {
|
||||
response.set(resp);
|
||||
}
|
||||
});
|
||||
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();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
private double evaluateExpression(String expression) {
|
||||
// Simple expression evaluator for testing
|
||||
return switch (expression) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue