Skip to content

Send JSON-RPC error response when a request handler completes empty - #1099

Open
ZYZ666-RGB wants to merge 1 commit into
modelcontextprotocol:mainfrom
ZYZ666-RGB:fix/1081-empty-handler-response
Open

Send JSON-RPC error response when a request handler completes empty#1099
ZYZ666-RGB wants to merge 1 commit into
modelcontextprotocol:mainfrom
ZYZ666-RGB:fix/1081-empty-handler-response

Conversation

@ZYZ666-RGB

@ZYZ666-RGB ZYZ666-RGB commented Aug 18, 2026

Copy link
Copy Markdown

When a server-side request handler returns a Mono that completes empty, the SDK previously sent no response at all for that request, leaving the client waiting indefinitely. This violates JSON-RPC 2.0's one-response-per-request contract.

Motivation and Context

MCP rides on JSON-RPC 2.0, which requires that every request carrying an id receives exactly one response (a result or an error). Two server-side dispatch paths had the gap:

  • McpServerSession.handleIncomingRequest (stateful servers): the empty Mono flowed through .map(...) (no emission) and .onErrorResume(...) (no error), so handle()'s .flatMap(this.transport::sendMessage) was never invoked and no bytes went over the wire.
  • DefaultMcpStatelessServerHandler.handleRequest (stateless servers): same empty propagation; callers doing .block() received null instead of a JSON-RPC response.

An empty completion is a common Reactor idiom (e.g. a reactive repository's get(id) completing empty when nothing matches), so this is a realistic failure mode, not a theoretical one. Fixes #1081.

Both paths now append .switchIfEmpty(...): an empty completion produces exactly one JSON-RPC error response (-32603 Internal error) naming the method, plus a WARN log. Normal result and error behavior is unchanged — switchIfEmpty only fires on empty completion.

How Has This Been Tested?

  • Added regression tests covering all three paths in both dispatch layers:
    • McpServerSessionTests (new): empty completion → exactly one error response with matching id; normal result → result response; failing handler → error response.
    • DefaultMcpStatelessServerHandlerTests (extended): empty handler result → INTERNAL_ERROR response; failing handler → error response.
  • Verified locally with mvn -pl mcp-core test — 6/6 new tests pass, module builds clean with spring-javaformat validation.
  • Full-suite integration tests require Docker and are covered by CI on GitHub.

Breaking Changes

None. This is an additive fix for a previously-broken path (empty handler completion previously produced no response; it now produces an error response). Existing successful/error behavior is byte-for-byte unchanged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Root cause: Mono.map() does not emit for an empty source and onErrorResume does not trigger without an error, so the empty completion propagated silently through both dispatch chains. The fix converts that silent empty completion into a contract-compliant error response.


When a server-side request handler returns a Mono that completes empty, McpServerSession.handleIncomingRequest and DefaultMcpStatelessServerHandler.handleRequest propagated the empty completion, so no JSON-RPC response was ever sent for the request. This violates JSON-RPC 2.0's one-response-per-request contract and leaves the client waiting indefinitely.

Both dispatch paths now complete with an internal error (-32603) response when the handler completes without a result, and log a warning naming the method. Normal result and error behavior is unchanged.

Adds regression tests for the empty-completion, normal-result and error paths in both the stateful (McpServerSession) and stateless (DefaultMcpStatelessServerHandler) dispatch layers.

Fixes modelcontextprotocol#1081

Signed-off-by: zhaoyuzhe <1991039819@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

McpServerSession never sends a response when a request handler's Mono completes empty — violates JSON-RPC 2.0's one-response-per-request contract

1 participant