Skip to content

Limit HTTP request bodies before MCP middleware parsing - #3111

Merged
SamMorrowDrums merged 6 commits into
mainfrom
sammorrowdrums-issue-3102-limit-http-request-bodies-before-mcp-mid-d0a507
Aug 19, 2026
Merged

Limit HTTP request bodies before MCP middleware parsing#3111
SamMorrowDrums merged 6 commits into
mainfrom
sammorrowdrums-issue-3102-limit-http-request-bodies-before-mcp-mid-d0a507

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an application-level HTTP request-body size limit that is enforced before any middleware or the MCP SDK reads or buffers the body.

Previously WithMCPParse and the WithScopeChallenge fallback path called io.ReadAll(r.Body) on an unbounded body with no size guard anywhere ahead of them in the HTTP stack. A large payload would be fully buffered in memory by those middlewares before the SDK's own guard in servePOST had a chance to apply.

Changes

  • New middleware.WithMaxBodySize(maxBytes) (pkg/http/middleware/body_limit.go): wraps the request body with http.MaxBytesReader, rejecting immediately via a known Content-Length fast path, or erroring on read once the limit is hit for chunked/unknown-length bodies. Registered as the first middleware in Handler.RegisterMiddleware, ahead of ExtractUserToken, WithMCPParse, WithPATScopes, and WithScopeChallenge.
  • WithMCPParse and WithScopeChallenge now detect the resulting *http.MaxBytesError and return a clear 413 Request Entity Too Large ("request body too large") response, following the existing http.Error(w, ..., statusCode) convention used elsewhere in this package, instead of silently falling through.
  • ServerConfig.MaxRequestBodyBytes (new, optional) lets operators override the default.
  • The effective limit is passed to both enforcement points: the early middleware and mcp.StreamableHTTPOptions.MaxRequestBodyBytes. This is required rather than defensive — see below.
  • Request-body replay for downstream handlers (including the MCP SDK) is preserved for all requests within the limit.

Why 5 MiB

The limit applies to the total HTTP request, not to the content carried within it. The JSON-RPC frame, the tools/call envelope, per-item arrays, paths and other argument metadata, and JSON string escaping all consume part of the budget, so the usable content is meaningfully smaller than the limit.

The MCP SDK already enforced 4 MiB on every request. Spending that entire budget on tool content leaves nothing for the envelope, so the default here is 5 MiB — a modest 1 MiB of headroom over the SDK default, chosen so that envelope overhead does not eat into the practical payload size. The intent is to keep the effective request ceiling in the same range as before while moving rejection earlier, not to raise the practical payload ceiling.

Because 5 MiB is above the SDK's own default, passing it to StreamableHTTPOptions.MaxRequestBodyBytes is load-bearing: leaving the SDK on its default would silently cap requests at 4 MiB and the headroom would not exist. A test sends a request sized between the two limits to prove this.

push_files is the main tool that can approach the limit, since it batches every file into a single JSON-RPC request. Note that this is a bound on total request size, not a guarantee that any particular file size will fit. Larger uploads are better served by pushing over Git directly, by Git LFS for large binaries, or by the release asset APIs, none of which route through the MCP JSON-RPC endpoint.

Operators who need a different bound can set ServerConfig.MaxRequestBodyBytes, and that value applies at both enforcement points.

Tests

  • pkg/http/middleware/body_limit_test.go: allowed request, boundary size (exact limit), oversized with known Content-Length (rejected before next runs), oversized with unknown length (rejected on downstream read).
  • pkg/http/middleware/mcp_parse_test.go: composition of WithMaxBodySize + WithMCPParse — oversized body never reaches parsing/next handler; boundary-size body still parses and preserves the body.
  • pkg/http/middleware/scope_challenge_test.go: composition of WithMaxBodySize + WithScopeChallenge's fallback body-read path.
  • pkg/http/handler_test.go: the default exceeds mcp.DefaultMaxRequestBodyBytes; an oversized request through RegisterMiddleware/RegisterRoutes never constructs the MCP server; a boundary-size request succeeds; a configured override reaches the SDK layer (verified by the SDK reporting the configured limit rather than its own default); and an unconfigured handler accepts a request sized between the SDK default and the 5 MiB default, which fails if the SDK option is dropped.

Verification

  • script/lint — 0 issues
  • script/test — full suite passes
  • No MCP tool schema changes — no toolsnap or docs regeneration needed.

Fixes #3102

Acknowledgments

Thanks @EQSTLab, @sondt99, @manus-use, and @YuvalElbar6 for the reports that led to this hardening.

Add WithMaxBodySize middleware that bounds the request body via
http.MaxBytesReader (with a fast Content-Length rejection when known),
registered first in RegisterMiddleware so it runs before any other
middleware or the MCP SDK reads or buffers the body.

WithMCPParse and WithScopeChallenge now return a clear 413 "request
body too large" response when their body read hits the limit, instead
of silently continuing.

Defaults to 10 MiB, overridable via ServerConfig.MaxRequestBodyBytes.

Fixes #3102

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 19, 2026 12:18
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner August 19, 2026 12:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an early, configurable HTTP request-body limit to prevent unbounded buffering.

Changes:

  • Adds WithMaxBodySize with a 10 MiB default.
  • Returns HTTP 413 for oversized bodies during middleware parsing.
  • Adds unit and integration coverage for size limits and body replay.
Show a summary per file
File Description
pkg/http/server.go Adds request-size configuration.
pkg/http/handler.go Registers the limiter first.
pkg/http/handler_test.go Tests handler-level enforcement.
pkg/http/middleware/body_limit.go Implements bounded request bodies.
pkg/http/middleware/body_limit_test.go Tests limit boundaries and lengths.
pkg/http/middleware/mcp_parse.go Handles limit errors with 413.
pkg/http/middleware/mcp_parse_test.go Tests parser composition.
pkg/http/middleware/scope_challenge.go Handles limit errors in fallback parsing.
pkg/http/middleware/scope_challenge_test.go Tests scope fallback composition.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/http/middleware/scope_challenge_test.go Outdated
Comment thread pkg/http/middleware/mcp_parse_test.go Outdated
SamMorrowDrums and others added 5 commits August 19, 2026 14:51
WithMCPParse and WithScopeChallenge tests for oversized requests were
using strings.NewReader, which gives httptest.NewRequest a known
Content-Length. That let WithMaxBodySize reject the request in its
fast path before the request ever reached the middleware's own
io.ReadAll/isMaxBytesError handling, leaving those branches untested.

Reuse the existing unknownLengthBody helper (body_limit_test.go) so
these tests actually reach the fallback read path and cover the
*http.MaxBytesError handling added in WithMCPParse and
WithScopeChallenge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The middleware default was an arbitrary 10 MiB, above the 4 MiB the SDK
already enforces, so it never changed which requests were accepted.
Alias mcp.DefaultMaxRequestBodyBytes instead, making the earlier
enforcement point behaviour-preserving by construction.

Also pass the effective limit to StreamableHTTPOptions. Previously the
SDK kept its own 4 MiB default, so a larger configured
MaxRequestBodyBytes was silently capped; both layers now agree.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bounds the total HTTP request, so allow modest headroom over the MCP
SDK's 4 MiB default for JSON-RPC and tool-call envelope overhead rather
than spending the whole budget on tool content.

Because the limit now exceeds the SDK default, passing it to
StreamableHTTPOptions is load-bearing: without it the SDK would cap
requests at 4 MiB and the headroom would not exist. Covered by a test
that sends a request between the two limits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

Limit HTTP request bodies before MCP middleware parsing

2 participants