Skip to content

Handle non-object JSON-RPC messages without raising#448

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:handle_non_object_json_rpc_messages
Open

Handle non-object JSON-RPC messages without raising#448
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:handle_non_object_json_rpc_messages

Conversation

@koic

@koic koic commented Jul 11, 2026

Copy link
Copy Markdown
Member

Motivation and Context

A JSON-RPC message is an object, but two stdio-facing paths assumed that and raised on a non-object value parsed from an incoming frame:

  • JsonRpcHandler.handle maps each element of a batch array through process_request, which reads request[:id] assuming a Hash. A non-object batch element (for example [[]] or [5]) makes [][:id] raise TypeError. handle_json rescued only JSON::ParserError, so the error escaped; over stdio it escaped the StdioTransport#open read loop. The HTTP transport already rejects array bodies before dispatch, so this surfaced only on stdio.
  • MCP::Client::Stdio#read_response parses each frame from the server and calls parsed.key?("id"), which raises NoMethodError when the server emits a non-object frame. Only JSON::ParserError was rescued, so one malformed line from the server escaped.

Both are robustness gaps: a malformed frame should be reported or skipped, not raise an unhandled exception out of the transport.

  • process_request now returns an INVALID_REQUEST error response for a non-object request before touching request[:id], so a malformed batch element becomes a normal per-element error.

  • handle_json gains a last-resort StandardError rescue that returns an INTERNAL_ERROR response, so an unexpected handling error still produces a JSON-RPC reply.

  • read_response skips a non-object frame the same way it skips a frame without an id.

How Has This Been Tested?

New tests in test/json_rpc_handler_test.rb cover non-object batch elements ([[]], [5], ["x"], [nil], [true]), a valid request followed by one non-object element, and handle_json("[[]]") returning an error instead of raising. A test in test/mcp/server/transports/stdio_transport_test.rb drives open with a [[]] line and asserts it emits an error response without raising. A test in test/mcp/client/stdio_test.rb has the server emit non-object frames before the response and asserts the client skips them and still returns the response.

Breaking Changes

None. A well-formed request, batch, or response is handled exactly as before; only a non-object message now yields a JSON-RPC error on the server or is skipped by the client instead of raising.

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

## Motivation and Context

A JSON-RPC message is an object, but two stdio-facing paths assumed that and raised
on a non-object value parsed from an incoming frame:

- `JsonRpcHandler.handle` maps each element of a batch array through `process_request`,
  which reads `request[:id]` assuming a Hash. A non-object batch element (for example `[[]]`
  or `[5]`) makes `[][:id]` raise `TypeError`. `handle_json` rescued only `JSON::ParserError`,
  so the error escaped; over stdio it escaped the `StdioTransport#open` read loop.
  The HTTP transport already rejects array bodies before dispatch, so this surfaced only on stdio.
- `MCP::Client::Stdio#read_response` parses each frame from the server and calls `parsed.key?("id")`,
  which raises `NoMethodError` when the server emits a non-object frame.
  Only `JSON::ParserError` was rescued, so one malformed line from the server escaped.

Both are robustness gaps: a malformed frame should be reported or skipped, not raise
an unhandled exception out of the transport.

- `process_request` now returns an `INVALID_REQUEST` error response for a non-object request
  before touching `request[:id]`, so a malformed batch element becomes a normal per-element error.

- `handle_json` gains a last-resort `StandardError` rescue that returns an `INTERNAL_ERROR` response,
  so an unexpected handling error still produces a JSON-RPC reply.
- `read_response` skips a non-object frame the same way it skips a frame without an id.

## How Has This Been Tested?

New tests in `test/json_rpc_handler_test.rb` cover non-object batch elements
(`[[]]`, `[5]`, `["x"]`, `[nil]`, `[true]`), a valid request followed by one non-object element,
and `handle_json("[[]]")` returning an error instead of raising. A test in
`test/mcp/server/transports/stdio_transport_test.rb` drives `open` with a `[[]]` line and
asserts it emits an error response without raising. A test in `test/mcp/client/stdio_test.rb`
has the server emit non-object frames before the response and asserts the client skips them
and still returns the response.

## Breaking Changes

None. A well-formed request, batch, or response is handled exactly as before;
only a non-object message now yields a JSON-RPC error on the server or is skipped by the client
instead of raising.
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.

1 participant