Commit 7c60956
committed
Reject non-Hash JSON-RPC bodies in
## 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 #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.StreamableHTTPTransport
1 parent d6b5392 commit 7c60956
2 files changed
Lines changed: 73 additions & 5 deletions
File tree
- lib/mcp/server/transports
- test/mcp/server/transports
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
339 | 341 | | |
340 | 342 | | |
341 | 343 | | |
342 | | - | |
343 | | - | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
344 | 349 | | |
345 | 350 | | |
346 | 351 | | |
| |||
349 | 354 | | |
350 | 355 | | |
351 | 356 | | |
352 | | - | |
| 357 | + | |
| 358 | + | |
353 | 359 | | |
354 | 360 | | |
355 | 361 | | |
| |||
507 | 513 | | |
508 | 514 | | |
509 | 515 | | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
510 | 520 | | |
511 | 521 | | |
512 | 522 | | |
513 | | - | |
514 | | - | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
515 | 529 | | |
516 | 530 | | |
517 | 531 | | |
| |||
Lines changed: 54 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
99 | 153 | | |
100 | 154 | | |
101 | 155 | | |
| |||
0 commit comments