## Motivation and Context
Both transports read a peer-controlled message fully into memory with no upper bound,
so a single unauthenticated message can allocate gigabytes and OOM the worker:
- `StreamableHTTPTransport#handle_post` did `request.body.read` with no size limit
before `JSON.parse`. A single ~512 MB POST grew a worker from 44 MB to 1.66 GB RSS
in the reporter's measurement. The path runs before session validation,
so it is reachable unauthenticated in both stateful and `stateless: true` modes.
- `StdioTransport` and `MCP::Client::Stdio` read frames with `IO#gets` and no `limit:`,
so a peer that never emits a newline grows a single String until the process is OOM-killed.
This matters when a host pipes an untrusted server's stdout into an unconstrained process.
The spec does not mandate a request size limit, and the reference SDKs vary
(the TypeScript SDK caps at a 4 MB default; Python/Go delegate to the web framework).
A finite default here bounds the allocation at the transport layer regardless of the deployment,
matching the TypeScript SDK's default.
- `StreamableHTTPTransport` gains `max_request_bytes:` (default 4 MiB). `handle_post`
rejects a declared `Content-Length` over the cap before reading, and also bounds
the actual read to one byte past the cap so a missing or spoofed `Content-Length`
(chunked transfer) is still caught, returning HTTP 413. `parse_request_body` now passes
`max_nesting:` (64) so a deeply nested body cannot exhaust the stack (complements the byte cap).
The constructor validates the new option as a positive Integer, since `nil` or a non-positive
value makes the underlying read unbounded and would silently disable the protection.
- The stdio `max_line_bytes:` bound (default 4 MiB) has since been merged upstream through
a separate advisory, so this change carries the remaining piece of the reported stdio fix:
an over-limit frame leaves leftover bytes in the pipe, desyncing the stream beyond recovery,
so `MCP::Client::Stdio` now closes the transport before raising and a later `send_request`
fails cleanly instead of parsing a truncated frame.
## How Has This Been Tested?
New tests in `test/mcp/server/transports/streamable_http_transport_test.rb` cover the 413 rejection
with and without `Content-Length`, the boundary body being accepted, the nesting-depth rejection,
and the constructor validation. The client stdio over-limit test now also asserts the transport
is closed afterward (`refute_predicate(transport, :connected?)`).
`bundle exec rake` (tests, RuboCop, and conformance baseline) passes. Verified end-to-end that
a 256 MB POST with no declared `Content-Length` is rejected with 413 without the transport reading
the whole body (a throttled `rack.input` that raises if read far past the cap is never tripped).
## Breaking Changes
None. The 4 MiB defaults comfortably fit a typical JSON-RPC message (a 4 MiB JSON string decodes to
roughly 3 MiB of base64 payload), and the new keyword arguments are additive and optional.