Skip to content

sdk(server): bound request input size before Pydantic model_validate #239

@bokelley

Description

@bokelley

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions