Skip to content

Bound the initialize request a session retains - #583

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

koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:bound_the_initialize_request_a_session_retains

Conversation

@koic

@koic koic commented Sep 29, 2026

Copy link
Copy Markdown
Member

Motivation and Context

A stateful session keeps the clientInfo and capabilities of the initialize request that created it, for as long as the session lives, and initialize is validated for shape only. The session count and the idle timeout bound how many sessions exist, and max_request_bytes bounds one request, but nothing bounded what each session retained, so the sessions together could retain up to max_sessions times max_request_bytes of client-supplied data. The documentation described the session limits as bounding what sessions retain, which held for their number and not for their data. The TypeScript and Python SDKs keep the initialize data whole and bound neither.

The body of an initialize request in stateful mode is now bounded by a new max_initialize_request_bytes: keyword, 64 KiB by default, which leaves room for large experimental capabilities while an ordinary initialize is under 2 KiB. A larger body is rejected with HTTP 413 before any session is created, so nothing over the bound is ever retained; nil removes the bound, and stateless mode, which retains nothing, ignores it.

A byte bound alone does not tightly bound the memory a session keeps, because the parsed objects, not the bytes, are what it keeps: a body made of many small values parses into far more objects than its size suggests. The params of an initialize are therefore also limited to 1024 JSON values, counted in a walk that stops at the limit, and a request over it is rejected the same way; an ordinary initialize holds a few dozen. Like MAX_JSON_NESTING, this structural limit applies in stateful mode whatever max_initialize_request_bytes is, including nil, so removing the byte bound does not reopen the object count; a keyword can follow if a real client ever needs more. Together the two bound both the serialized input and the number of parsed values a session can retain, and the documentation now states that budget instead of the earlier claim.

How Has This Been Tested?

New tests in test/mcp/server/transports/streamable_http_transport_test.rb send an initialize over the bound, one exactly at it and one a byte over, one under nil, and one in stateless mode, and check the 413, that no session exists afterwards, and that a regular request larger than the bound is still governed by max_request_bytes alone. Further tests send params over the value bound while under the byte bound, exactly at it and one value over, a large number of values within it, the same over-bound params under nil, and in stateless mode, and check that object keys are counted and that nested shapes are refused. Against the previous library an initialize just under max_request_bytes creates a session that keeps it. One more sends a representative initialize with several icons, every capability, and an experimental object of many members, and checks that it is accepted well inside both bounds.

Breaking Changes

Requests that earlier releases accepted are now refused at the new bounds: an initialize request larger than 64 KiB is refused in stateful mode unless max_initialize_request_bytes: is raised or set to nil, and one whose params hold more than 1024 JSON values is refused in stateful mode.

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 stateful session keeps the `clientInfo` and `capabilities` of the `initialize` request that created it,
for as long as the session lives, and `initialize` is validated for shape only. The session count and
the idle timeout bound how many sessions exist, and `max_request_bytes` bounds one request, but nothing bounded
what each session retained, so the sessions together could retain up to `max_sessions` times `max_request_bytes`
of client-supplied data. The documentation described the session limits as bounding what sessions retain,
which held for their number and not for their data. The TypeScript and Python SDKs keep the initialize data
whole and bound neither.

The body of an `initialize` request in stateful mode is now bounded by a new `max_initialize_request_bytes:` keyword,
64 KiB by default, which leaves room for large `experimental` capabilities while an ordinary `initialize` is under 2 KiB.
A larger body is rejected with HTTP 413 before any session is created, so nothing over the bound is ever retained;
`nil` removes the bound, and stateless mode, which retains nothing, ignores it.

A byte bound alone does not tightly bound the memory a session keeps, because the parsed objects, not the bytes,
are what it keeps: a body made of many small values parses into far more objects than its size suggests.
The `params` of an `initialize` are therefore also limited to 1024 JSON values, counted in a walk that stops at the limit,
and a request over it is rejected the same way; an ordinary `initialize` holds a few dozen. Like `MAX_JSON_NESTING`,
this structural limit applies in stateful mode whatever `max_initialize_request_bytes` is, including `nil`,
so removing the byte bound does not reopen the object count; a keyword can follow if a real client ever needs more.
Together the two bound both the serialized input and the number of parsed values a session can retain,
and the documentation now states that budget instead of the earlier claim.

## How Has This Been Tested?

New tests in `test/mcp/server/transports/streamable_http_transport_test.rb` send an `initialize` over the bound,
one exactly at it and one a byte over, one under `nil`, and one in stateless mode, and check the 413,
that no session exists afterwards, and that a regular request larger than the bound is still governed by `max_request_bytes` alone.
Further tests send `params` over the value bound while under the byte bound, exactly at it and one value over,
a large number of values within it, the same over-bound `params` under `nil`, and in stateless mode,
and check that object keys are counted and that nested shapes are refused.
Against the previous library an `initialize` just under `max_request_bytes` creates a session that keeps it.
One more sends a representative `initialize` with several icons, every capability, and an `experimental` object of many members,
and checks that it is accepted well inside both bounds.

## Breaking Changes

Requests that earlier releases accepted are now refused at the new bounds: an `initialize` request larger than
64 KiB is refused in stateful mode unless `max_initialize_request_bytes:` is raised or set to `nil`, and one whose
`params` hold more than 1024 JSON values is refused in stateful mode.

This branch has not been deployed

No deployments
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