Repository navigation
Reject server messages that are not JSON-RPC 2.0 in the client transports - #590
Merged
koic merged 1 commit intoOct 6, 2026
Conversation
…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.
atesgoral
approved these changes
Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
Issue #589 reports that
MCP::Client#call_toolreturns normally when the server's response omitsjsonrpcor sets it to"1.0". The bundled transports never read that member of an incoming message, while the server side of this SDK answers-32600to such a request and the TypeScript, Python, Go, and C# SDKs validate every incoming message against a schema whosejsonrpcis the literal"2.0".MCP::Client::StdioandMCP::Client::HTTPnow check it. A response that is not a JSON object carrying"jsonrpc": "2.0"fails the request withRequestHandlerErrorinstead 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 itsread_timeoutdefaults 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: apingis not answered and a handler is not called. Frames for other ids keep being skipped, the body answering a notification is not checked, and undermode: :autoa rejectedserver/discoveranswer falls back to the legacy handshake like any other probe failure. An empty ornullJSON body of a 200 is still handed over asnil, 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, andMCP::Clientover a custom transport or a test double is unaffected.Types of changes
Checklist
Additional context