Skip to content

Reject non-Hash JSON-RPC bodies in StreamableHTTPTransport#354

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

Reject non-Hash JSON-RPC bodies in StreamableHTTPTransport#354
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:reject_non_hash_post_body_in_streamable_http_transport

Conversation

@koic
Copy link
Copy Markdown
Member

@koic koic commented May 17, 2026

Motivation and Context

MCP 2025-11-25 does not support JSON-RPC batch (batch support was removed in 2025-06-18), so request bodies are expected to be a single JSON-RPC message object. The previous code path:

body = parse_request_body(body_string)
return body unless body.is_a?(Hash)

returned the parsed JSON value as if it were a Rack response when the body was a JSON array (former batch shape), a string, a number, or any other non-Hash JSON value. This produced a malformed Rack response instead of a proper HTTP 400.

This was a pre-existing bug exposed during follow-up review of #347 (MCP-Protocol-Version header validation), which surfaced it but did not fix the broken response shape for non-Hash bodies sent with a valid header.

Behavior

  • parse_request_body now raises a private InvalidJsonError on parse failure instead of returning a Rack response tuple. The overloaded return type and the parse_error_tuple? discriminator are gone.
  • handle_post rescues InvalidJsonError and returns the same plain JSON 400 response as before ({"error":"Invalid JSON"}), so parse error handling is observably unchanged.
  • Non-Hash parsed bodies (arrays, strings, numbers, booleans, null) now return HTTP 400 with a plain JSON body explaining that the body must be a single JSON-RPC message object, instead of producing a malformed Rack response.

The error response shape stays in the existing plain JSON style for consistency with validate_content_type, not_acceptable_response, and the other transport-level error helpers. Unifying all of these to a JSON-RPC error envelope (matching the Python and TypeScript SDKs) is deferred to a separate follow-up.

SDK Comparison

  • Python SDK (src/mcp/server/streamable_http.py): Already rejects non-Hash bodies (Array, primitive) with HTTP 400. Uses a JSON-RPC error envelope with INVALID_PARAMS.
  • TypeScript SDK (packages/server/src/server/streamableHttp.ts): As of main, still processes JSON arrays as JSON-RPC batches and has not yet caught up with the 2025-06-18 batch removal. Primitives fail schema validation and return 400 with a JSON-RPC error envelope.

This change brings the Ruby SDK in line with the current MCP spec (no batch). The Ruby SDK is now closer to the spec than the TypeScript SDK's main branch on this specific point.

How Has This Been Tested?

Existing regression test test "handles POST request with invalid JSON" continues to pass unchanged (plain JSON response preserved). Added new regression tests:

  • POST with a JSON array body returns 400 with a clear error message
  • POST with a non-object JSON body (e.g. "foo") returns 400

bundle exec rake test and bundle exec rake rubocop both pass.

Breaking Changes

None for compliant clients sending single JSON-RPC message objects. Clients that were sending JSON arrays (batch requests, no longer supported as of MCP 2025-11-25) or other non-object bodies will now receive a proper HTTP 400 with a JSON error body instead of the previous malformed Rack response; no client could have relied on the broken pre-existing behavior.

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

MCP 2025-11-25 does not support JSON-RPC batch (batch support was removed in 2025-06-18),
so request bodies are expected to be a single JSON-RPC message object. The previous code path:

```ruby
body = parse_request_body(body_string)
return body unless body.is_a?(Hash)
```

returned the parsed JSON value as if it were a Rack response when
the body was a JSON array (former batch shape), a string, a number,
or any other non-Hash JSON value. This produced a malformed Rack response
instead of a proper HTTP 400.

This was a pre-existing bug exposed during follow-up review of modelcontextprotocol#347
(`MCP-Protocol-Version` header validation), which surfaced it but did not fix
the broken response shape for non-Hash bodies sent with a valid header.

### Behavior

- `parse_request_body` now raises a private `InvalidJsonError` on parse
  failure instead of returning a Rack response tuple. The overloaded
  return type and the `parse_error_tuple?` discriminator are gone.
- `handle_post` rescues `InvalidJsonError` and returns the same plain
  JSON 400 response as before (`{"error":"Invalid JSON"}`), so parse
  error handling is observably unchanged.
- Non-Hash parsed bodies (arrays, strings, numbers, booleans, null) now
  return HTTP 400 with a plain JSON body explaining that the body must
  be a single JSON-RPC message object, instead of producing a malformed
  Rack response.

The error response shape stays in the existing plain JSON style for
consistency with `validate_content_type`, `not_acceptable_response`,
and the other transport-level error helpers. Unifying all of these to
a JSON-RPC error envelope (matching the Python and TypeScript SDKs) is
deferred to a separate follow-up.

### SDK Comparison

- Python SDK (`src/mcp/server/streamable_http.py`): Already rejects
  non-Hash bodies (Array, primitive) with HTTP 400. Uses a JSON-RPC
  error envelope with `INVALID_PARAMS`.
- TypeScript SDK (`packages/server/src/server/streamableHttp.ts`):
  As of `main`, still processes JSON arrays as JSON-RPC batches and
  has not yet caught up with the 2025-06-18 batch removal. Primitives
  fail schema validation and return 400 with a JSON-RPC error envelope.

This change brings the Ruby SDK in line with the current MCP spec
(no batch). The Ruby SDK is now closer to the spec than the TypeScript
SDK's `main` branch on this specific point.

## How Has This Been Tested?

Existing regression test `test "handles POST request with invalid
JSON"` continues to pass unchanged (plain JSON response preserved).
Added new regression tests:

- POST with a JSON array body returns 400 with a clear error message
- POST with a non-object JSON body (e.g. `"foo"`) returns 400

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

None for compliant clients sending single JSON-RPC message objects.
Clients that were sending JSON arrays (batch requests, no longer
supported as of MCP 2025-11-25) or other non-object bodies will now
receive a proper HTTP 400 with a JSON error body instead of
the previous malformed Rack response; no client could have relied on
the broken pre-existing behavior.
@koic koic force-pushed the reject_non_hash_post_body_in_streamable_http_transport branch from 7c60956 to f62d813 Compare May 17, 2026 15:56
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.

2 participants