Repository navigation
Bound peer JSON nesting and request size (Fixes #63) - #65
Merged
Merged
Conversation
Peer json::parse had no depth or size limit, so a deeply nested stdio line or unauthenticated POST could overflow the stack. Match the Ruby MCP SDK: nesting depth 64 and a 4 MiB frame/body cap, and keep serving with a JSON-RPC error instead of crashing. Fixes #63 Co-authored-by: Zihong Zhang <hkr04@users.noreply.github.com>
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.
Fixes #63
Peer
json::parseof stdio lines and HTTP bodies had no nesting or size bound. nlohmannordered_jsoncopies nested values recursively when an object grows, so a message nested tens of thousands of levels deep overflows the stack (SIGSEGV / ASan stack-overflow). Streamable HTTP parsed the body before the session lookup, so one unauthenticatedPOST /mcpcould kill the process.This follows the official Ruby MCP SDK transport limits. RFC 8259 §9 allows an implementation to cap nesting; MCP itself does not mandate a depth. Ruby and the TypeScript SDK both default to a 4 MiB body, and Ruby caps nesting at 64.
Alignment with the Ruby SDK
MAX_JSON_NESTING = 64DEFAULT_MAX_REQUEST_BYTES = 4 * 1024 * 1024(stdiomax_line_bytesis the same 4 MiB)Source of truth:
MAX_JSON_NESTING = 64,DEFAULT_MAX_REQUEST_BYTES = 4 * 1024 * 1024,parse_request_body→JSON.parse(..., max_nesting: MAX_JSON_NESTING))max_line_bytes)What changed
parse_peer_jsonrejects an input larger than 4 MiB before parsing, and aborts from nlohmann's parser callback once another array or object would make the document deeper than 64. The callback runs before that container is built, so a deep DOM is never constructed.Call sites:
server::start_stdio— bounded frame read (discards the rest of an over-long line so the next frame stays aligned) andparse_peer_json. Oversize → JSON-RPC-32600; bad JSON or over-depth →-32700. The server keeps reading.server::handle_jsonrpc— parse the body before session lookup. HTTP 400 /-32700for bad or too-deep JSON; HTTP 413 /-32600for an oversized body.server::handle_mcp_post— same, still before theMcp-Session-Idcheck. A declaredContent-Lengthabove 4 MiB is rejected by cpp-httplib (set_payload_max_length) before the route handler, with a JSON-RPC 413 body.tools/callwhenargumentsis a JSON string —parse_peer_json; failure is JSON-RPC-32602.parse_peer_json, and the stdio client drops a frame that grows past 4 MiB instead of buffering it forever.paramsisstd::moved out of the parsed value on the server paths above. That only removes the extra deep copy; the depth cap is what stops the overflow insidejson::parse.Test plan
cmake -S . -B build -DMCP_BUILD_TESTS=ON && cmake --build build -j --target mcp_tests && ./build/test/mcp_tests— 30 tests passedpingsucceeds; a depth-65 line returns-32700and a followingpingstill succeeds-32600and a followingpingstill succeeds-32602; a shallow string argument and a laterpingstill workPOST /mcpwith depth 64initializereturns 200; depth 65 returns 400 /-32700(not the missing-session error) before session lookup; a body over 4 MiB returns 413 /-32600; a later request is still servedPOST /messagewith depth 65 returns 400 /-32700, then apingreturns 202