Backport #800: fix error behavior on unregistered handlers in stateless server handler - #1104
Conversation
Backport of modelcontextprotocol#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 modelcontextprotocol#800 and an integration test that asserts both the HTTP status and the JSON-RPC error body, since the status is what regressed. See modelcontextprotocol#1085
eashwar-mp
left a comment
There was a problem hiding this comment.
Confirmed the underlying bug from the reporter side in #1085 (0.17.0 via spring-ai 1.1.4, WebMvc stateless transport). I also verified this exact change applies byte-for-byte to 0.17.x: the early-return and all McpSchema APIs it touches are identical there.
The fix reads correctly, it restores parity with the stateful handler (unknown method returns HTTP 200 with a -32601 body) and unblocks the OpenAI connector's server/discover probe. Leaving the HTTP 404 status change to #1083 as a separate concern is the right call for a backport. LGTM.
|
@eashwar-mp It seems the MR is missing workflows being approved, to then merge, and it seems only Maintainers can do that. Any chance you're one, or know who we can ping? This is a much needed change for my team. Thanks! |
|
@JHTosas I'm not a maintainer, my review is a community approval with read-only permissions, so it won't satisfy the merge gate or approve the CI workflows. This needs someone with write access. Looking at who merges here, @Kehrlann is the most active maintainer (nearly all recent merges are his), and @tzolov triaged this one on #1085 (labeled it P1 and tagged it for confirmation). Either is probably a good ping to approve the workflows and take it from there. The bug is now confirmed on three lines (0.17.0, 0.18.3, 0.18.4) and the change is three lines plus tests, so hopefully a quick one. @JHTosas If it's breaking your prod today or this week, I would recommend adding a servlet filter which intercepts only a server/discover request (carrying an id) and returns a clean -32601 at HTTP 200. That should be a good intermediary fix, while the maintainers push this one out and publish the artifact. |
Backport of #800 to the 0.18.x line. See #1085.
Why
DefaultMcpStatelessServerHandler.handleRequestreturnsMono.error(...)for a method with no registered handler. That is an early return, so the error never reaches theonErrorResumea few lines below it that maps errors onto a JSON-RPC error response — it escapes to the transport instead, and every stateless transport maps an escaping handler error to HTTP 500:HttpServletStatelessServerTransportWebMvcStatelessServerTransport(io.modelcontextprotocol.sdk:mcp-spring-webmvc)WebFluxStatelessServerTransport(io.modelcontextprotocol.sdk:mcp-spring-webflux)Requests for registered methods are unaffected —
tools/callwith an unknown tool name already returns a proper-32602, because that path does run throughonErrorResume.The practical impact is the one described in #1072: OpenAI's hosted MCP connector sends
server/discoverbeforetools/list, so a healthy server answers a routine capability probe with a 5xx. On our side that madePOST /mcpthe only 5xx source in the service and burned the availability SLO budget; OpenAI surfaces the same 500 as HTTP 424external_connector_error, which aborts the whole response.Why a backport
The fix is on
mainand shipped inmcp-core2.0.1, but 2.0.x is not reachable for Spring AI users on Spring Boot 3:io.modelcontextprotocol.sdk:mcp-spring-webmvcdoes not exist at 2.0.x — the Spring transports moved toorg.springframework.ai— somcp-corecannot be bumped on its own.spring-ai2.0'sspring-ai-starter-mcp-server-webmvcpullsspring-boot-starter-web:4.1.0, making the upgrade a Spring Framework 7 migration rather than a version bump.spring-ai1.1.x pins the 0.18.x line, and 0.18.4 (the latest published) still has the early return.That is the situation #1085 describes, with two other reporters confirming it on 0.17.0 and 0.18.3.
Change
Three lines in
DefaultMcpStatelessServerHandler, identical in effect to #800: return aJSONRPCResponsecarrying-32601 Method not found: <method>instead of failing theMono.Tests
DefaultMcpStatelessServerHandlerTests— the unit test from Fix error behavior on unregistered handlers in stateless server handler #800, verbatim.HttpServletStatelessIntegrationTests#testMissingHandlerReturnsMethodNotFoundError— asserts the HTTP status is 200 as well as the JSON-RPC error body. Fix error behavior on unregistered handlers in stateless server handler #800's integration test goes through an MCP client and so only sees the JSON-RPC layer; since the regression people actually hit is the status code, this one drives the transport directly withMockHttpServletRequest(the same style as the neighbouringtestThrownMcpErrorAndJsonRpcError) so a future transport-level regression is caught too../mvnw -pl mcp-core test -Dtest=DefaultMcpStatelessServerHandlerTests→ 1/1 green../mvnw -pl mcp-test test -Dtest=HttpServletStatelessIntegrationTests→ 13/13 green.Note on HTTP status
This restores parity with the stateful session handler and with 2.0.x: unknown method → HTTP 200 with a JSON-RPC
-32601body. The 2026-07-28 Streamable HTTP revision asks for HTTP 404 with-32601, which is what #1083 adds forHttpServletStatelessServerTransporton 2.0.x. That status change is deliberately left out here — it is a separate concern from the 500, the Spring transports each carry their own copy of the status logic and would need the same treatment, and 200 +-32601is already enough for clients to fall back to the legacyinitializeflow.Happy to open the equivalent PRs against
1.0.xand1.1.x— both carry the same early return (viaMcpError.builder(...)), and I have the same change prepared and green on both.