Conversation
## 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
A stateful session keeps the
clientInfoandcapabilitiesof theinitializerequest that created it, for as long as the session lives, andinitializeis validated for shape only. The session count and the idle timeout bound how many sessions exist, andmax_request_bytesbounds one request, but nothing bounded what each session retained, so the sessions together could retain up tomax_sessionstimesmax_request_bytesof 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
initializerequest in stateful mode is now bounded by a newmax_initialize_request_bytes:keyword, 64 KiB by default, which leaves room for largeexperimentalcapabilities while an ordinaryinitializeis under 2 KiB. A larger body is rejected with HTTP 413 before any session is created, so nothing over the bound is ever retained;nilremoves 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
paramsof aninitializeare 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 ordinaryinitializeholds a few dozen. LikeMAX_JSON_NESTING, this structural limit applies in stateful mode whatevermax_initialize_request_bytesis, includingnil, 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.rbsend aninitializeover the bound, one exactly at it and one a byte over, one undernil, 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 bymax_request_bytesalone. Further tests sendparamsover 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-boundparamsundernil, and in stateless mode, and check that object keys are counted and that nested shapes are refused. Against the previous library aninitializejust undermax_request_bytescreates a session that keeps it. One more sends a representativeinitializewith several icons, every capability, and anexperimentalobject 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
initializerequest larger than 64 KiB is refused in stateful mode unlessmax_initialize_request_bytes:is raised or set tonil, and one whoseparamshold more than 1024 JSON values is refused in stateful mode.Types of changes
Checklist