Context
Security review of PR #238 (typed handler params) flagged that create_tool_caller now runs Model.model_validate on every tool call with no input-size or depth cap at the SDK layer. Upstream transports (FastMCP streamable-HTTP, a2a-python) don't impose a uniform request-body cap, so a hostile caller can submit arbitrarily large JSON and force the server to parse it.
Pydantic 2's pydantic-core is Rust-native and resilient to stack depth, but:
- Deeply-nested JSON with
extra="allow" still allocates per-level Python objects.
- Large string fields without a
max_length constraint (e.g. promoted_offering on GetProductsRequest) become validator-free bulk.
- CPU per-request rises non-trivially under adversarial input.
Pre-existing transport-level concern that PR #238 meaningfully increases because typed dispatch runs validation unconditionally when a model is declared.
Proposal
One of:
-
Starlette middleware at bind time. _run_mcp_http / A2A bind sites install a max_body_size guard (Starlette has no built-in; needs a small custom ASGI middleware that reads Content-Length or buffers + caps). Default to a generous 10 MB, expose as a serve(..., max_request_size=) kwarg. Same knob for both transports.
-
SDK-level len(json.dumps(params)) > N shortcut inside create_tool_caller before calling model_validate. Simpler, but re-serialises what the transport already parsed.
Prefer (1). It kicks in before parsing (cheaper) and covers non-typed handlers too.
Acceptance
- Size cap on request bodies, enforced at the transport bind point.
- Default documented in
docs/handler-authoring.md.
- Test: request exceeding the cap returns an appropriate transport error before dispatch runs.
Severity
Medium. Not a merge-blocker for anything shipped. Adversarial-load resilience for adopters running public-facing endpoints.
Context
Security review of PR #238 (typed handler params) flagged that
create_tool_callernow runsModel.model_validateon every tool call with no input-size or depth cap at the SDK layer. Upstream transports (FastMCP streamable-HTTP, a2a-python) don't impose a uniform request-body cap, so a hostile caller can submit arbitrarily large JSON and force the server to parse it.Pydantic 2's pydantic-core is Rust-native and resilient to stack depth, but:
extra="allow"still allocates per-level Python objects.max_lengthconstraint (e.g.promoted_offeringonGetProductsRequest) become validator-free bulk.Pre-existing transport-level concern that PR #238 meaningfully increases because typed dispatch runs validation unconditionally when a model is declared.
Proposal
One of:
Starlette middleware at bind time.
_run_mcp_http/ A2A bind sites install amax_body_sizeguard (Starlette has no built-in; needs a small custom ASGI middleware that reads Content-Length or buffers + caps). Default to a generous 10 MB, expose as aserve(..., max_request_size=)kwarg. Same knob for both transports.SDK-level
len(json.dumps(params)) > Nshortcut insidecreate_tool_callerbefore callingmodel_validate. Simpler, but re-serialises what the transport already parsed.Prefer (1). It kicks in before parsing (cheaper) and covers non-typed handlers too.
Acceptance
docs/handler-authoring.md.Severity
Medium. Not a merge-blocker for anything shipped. Adversarial-load resilience for adopters running public-facing endpoints.