Skip to content

Commit a14ef42

Browse files
Randgalttzolov
authored andcommitted
If a handler throws McpError, use its values for the RPC error
Handlers should be able to throw RPC errors and `McpError` is the right exception for that. Improve `DefaultMcpStatelessServerHandler` error handler to check if the exception is `McpError` and, if so, use it to build the RPC error result instead of re-writing as `INTERNAL_ERROR`.
1 parent 4532b61 commit a14ef42

File tree

2 files changed

+82
-25
lines changed

2 files changed

+82
-25
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ public Mono<McpSchema.JSONRPCResponse> handleRequest(McpTransportContext transpo
3535
}
3636
return requestHandler.handle(transportContext, request.params())
3737
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
38-
.onErrorResume(t -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
39-
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, t.getMessage(),
40-
null))));
38+
.onErrorResume(t -> {
39+
McpSchema.JSONRPCResponse.JSONRPCError error;
40+
if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
41+
error = mcpError.getJsonRpcError();
42+
}
43+
else {
44+
error = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
45+
t.getMessage(), null);
46+
}
47+
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, error));
48+
});
4149
}
4250

4351
@Override

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

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,13 @@
44

55
package io.modelcontextprotocol.server;
66

7-
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
8-
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
9-
import static org.assertj.core.api.Assertions.assertThat;
10-
import static org.awaitility.Awaitility.await;
11-
12-
import java.time.Duration;
13-
import java.util.List;
14-
import java.util.Map;
15-
import java.util.concurrent.ConcurrentHashMap;
16-
import java.util.concurrent.atomic.AtomicReference;
17-
import java.util.function.BiFunction;
18-
19-
import org.apache.catalina.LifecycleException;
20-
import org.apache.catalina.LifecycleState;
21-
import org.apache.catalina.startup.Tomcat;
22-
import org.junit.jupiter.api.AfterEach;
23-
import org.junit.jupiter.api.BeforeEach;
24-
import org.junit.jupiter.params.ParameterizedTest;
25-
import org.junit.jupiter.params.provider.ValueSource;
26-
import org.springframework.web.client.RestClient;
27-
287
import com.fasterxml.jackson.databind.ObjectMapper;
29-
308
import io.modelcontextprotocol.client.McpClient;
319
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
3210
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
3311
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
12+
import io.modelcontextprotocol.spec.HttpHeaders;
13+
import io.modelcontextprotocol.spec.McpError;
3414
import io.modelcontextprotocol.spec.McpSchema;
3515
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
3616
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
@@ -41,7 +21,33 @@
4121
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
4222
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
4323
import io.modelcontextprotocol.spec.McpSchema.Tool;
24+
import io.modelcontextprotocol.spec.ProtocolVersions;
4425
import net.javacrumbs.jsonunit.core.Option;
26+
import org.apache.catalina.LifecycleException;
27+
import org.apache.catalina.LifecycleState;
28+
import org.apache.catalina.startup.Tomcat;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.params.ParameterizedTest;
33+
import org.junit.jupiter.params.provider.ValueSource;
34+
import org.springframework.mock.web.MockHttpServletRequest;
35+
import org.springframework.mock.web.MockHttpServletResponse;
36+
import org.springframework.web.client.RestClient;
37+
38+
import java.time.Duration;
39+
import java.util.List;
40+
import java.util.Map;
41+
import java.util.concurrent.ConcurrentHashMap;
42+
import java.util.concurrent.atomic.AtomicReference;
43+
import java.util.function.BiFunction;
44+
45+
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
46+
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
47+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
48+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
49+
import static org.assertj.core.api.Assertions.assertThat;
50+
import static org.awaitility.Awaitility.await;
4551

4652
class HttpServletStatelessIntegrationTests {
4753

@@ -460,6 +466,49 @@ void testStructuredOutputRuntimeToolAddition(String clientType) {
460466
mcpServer.close();
461467
}
462468

469+
@Test
470+
void testThrownMcpError() throws Exception {
471+
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
472+
.serverInfo("test-server", "1.0.0")
473+
.capabilities(ServerCapabilities.builder().tools(true).build())
474+
.build();
475+
476+
Tool testTool = Tool.builder().name("test").description("test").build();
477+
478+
McpStatelessServerFeatures.SyncToolSpecification toolSpec = new McpStatelessServerFeatures.SyncToolSpecification(
479+
testTool, (transportContext, request) -> {
480+
throw new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(12345, "testing", Map.of("a", "b")));
481+
});
482+
483+
mcpServer.addTool(toolSpec);
484+
485+
McpSchema.CallToolRequest callToolRequest = new McpSchema.CallToolRequest("test", Map.of());
486+
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
487+
McpSchema.METHOD_TOOLS_CALL, "test", callToolRequest);
488+
489+
MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT);
490+
MockHttpServletResponse response = new MockHttpServletResponse();
491+
492+
byte[] content = new ObjectMapper().writeValueAsBytes(jsonrpcRequest);
493+
request.setContent(content);
494+
request.addHeader("Content-Type", "application/json");
495+
request.addHeader("Content-Length", Integer.toString(content.length));
496+
request.addHeader("Content-Length", Integer.toString(content.length));
497+
request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM);
498+
request.addHeader("Content-Type", APPLICATION_JSON);
499+
request.addHeader("Cache-Control", "no-cache");
500+
request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26);
501+
mcpStatelessServerTransport.service(request, response);
502+
503+
McpSchema.JSONRPCResponse jsonrpcResponse = new ObjectMapper().readValue(response.getContentAsByteArray(),
504+
McpSchema.JSONRPCResponse.class);
505+
506+
assertThat(jsonrpcResponse.error())
507+
.isEqualTo(new McpSchema.JSONRPCResponse.JSONRPCError(12345, "testing", Map.of("a", "b")));
508+
509+
mcpServer.close();
510+
}
511+
463512
private double evaluateExpression(String expression) {
464513
// Simple expression evaluator for testing
465514
return switch (expression) {

0 commit comments

Comments
 (0)