Skip to content

Reject server messages that are not JSON-RPC 2.0 in the client transports - #590

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:reject_responses_that_are_not_json_rpc_2_0
Oct 6, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:reject_responses_that_are_not_json_rpc_2_0

Conversation

@koic

@koic koic commented Oct 5, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Issue #589 reports that MCP::Client#call_tool returns normally when the server's response omits jsonrpc or sets it to "1.0". The bundled transports never read that member of an incoming message, while the server side of this SDK answers -32600 to such a request and the TypeScript, Python, Go, and C# SDKs validate every incoming message against a schema whose jsonrpc is the literal "2.0".

MCP::Client::Stdio and MCP::Client::HTTP now check it. A response that is not a JSON object carrying "jsonrpc": "2.0" fails the request with RequestHandlerError instead of being returned: over stdio the frame answering the awaited id, over HTTP the JSON body or the first response-shaped SSE event, on a resumed stream as well. Over HTTP that is what the Python client does with the answer to a request, and the TypeScript client with a JSON body. Over stdio both of them report the message as an error and keep reading; this transport reads synchronously and its read_timeout defaults to none, so reading on would wait forever, and the request fails at once instead. A server-to-client request that is not JSON-RPC 2.0 is ignored: a ping is not answered and a handler is not called. Frames for other ids keep being skipped, the body answering a notification is not checked, and under mode: :auto a rejected server/discover answer falls back to the legacy handshake like any other probe failure. An empty or null JSON body of a 200 is still handed over as nil, as before.

The check lives in the transports, not in MCP::Client#request, so a custom transport keeps handing over whatever Hash it builds.

The HTTP client tests stubbed most JSON bodies as { result: ... } without the member, which the transport now rejects,
so those 54 stubs and three whole-response assertions carry jsonrpc: "2.0".

Closes #589.

How Has This Been Tested?

test/mcp/client/stdio_test.rb and test/mcp/client/http_test.rb. Against the library before this change, 20 of the 22 new tests fail; the other two pin behavior that stays the same.

Breaking Changes

A server whose responses lack "jsonrpc": "2.0" no longer works with the bundled transports, and neither does a test that stubs such a response for them: the member has to be sent, and there is no opt-out. Servers built with an MCP SDK send it already, and MCP::Client over a custom transport or a test double is unaffected.

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

…orts

## Motivation and Context

Issue modelcontextprotocol#589 reports that `MCP::Client#call_tool` returns normally when the server's response omits `jsonrpc` or
sets it to `"1.0"`. The bundled transports never read that member of an incoming message, while the server side of
this SDK answers `-32600` to such a request and the TypeScript, Python, Go, and C# SDKs validate every incoming message
against a schema whose `jsonrpc` is the literal `"2.0"`.

`MCP::Client::Stdio` and `MCP::Client::HTTP` now check it. A response that is not a JSON object carrying
`"jsonrpc": "2.0"` fails the request with `RequestHandlerError` instead of being returned: over stdio the frame answering
the awaited id, over HTTP the JSON body or the first response-shaped SSE event, on a resumed stream as well.
Over HTTP that is what the Python client does with the answer to a request, and the TypeScript client with a JSON body.
Over stdio both of them report the message as an error and keep reading; this transport reads synchronously and
its `read_timeout` defaults to none, so reading on would wait forever, and the request fails at once instead.
A server-to-client request that is not JSON-RPC 2.0 is ignored: a `ping` is not answered and a handler is not called.
Frames for other ids keep being skipped, the body answering a notification is not checked, and under `mode: :auto`
a rejected `server/discover` answer falls back to the legacy handshake like any other probe failure.
An empty or `null` JSON body of a 200 is still handed over as `nil`, as before.

The check lives in the transports, not in `MCP::Client#request`, so a custom transport keeps handing over
whatever Hash it builds.

The HTTP client tests stubbed most JSON bodies as `{ result: ... }` without the member, which the transport now rejects,
so those 54 stubs and three whole-response assertions carry `jsonrpc: "2.0"`.

Closes modelcontextprotocol#589.

## How Has This Been Tested?

test/mcp/client/stdio_test.rb and test/mcp/client/http_test.rb. Against the library before this change,
20 of the 22 new tests fail; the other two pin behavior that stays the same.

## Breaking Changes

A server whose responses lack `"jsonrpc": "2.0"` no longer works with the bundled transports, and neither does
a test that stubs such a response for them: the member has to be sent, and there is no opt-out. Servers built
with an MCP SDK send it already, and `MCP::Client` over a custom transport or a test double is unaffected.
@koic
koic merged commit d565e12 into modelcontextprotocol:main Oct 6, 2026
11 checks passed
@koic
koic deleted the reject_responses_that_are_not_json_rpc_2_0 branch October 6, 2026 03:07
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.

Client accepts tool responses with missing or invalid jsonrpc version

2 participants