From 2cc4ccb97e02e9b019de09cf778edce7632f37d1 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 19 Aug 2026 14:18:12 +0200 Subject: [PATCH 1/4] Limit HTTP request bodies before MCP middleware parsing 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> --- pkg/http/handler.go | 12 ++ pkg/http/handler_test.go | 84 ++++++++++++++ pkg/http/middleware/body_limit.go | 54 +++++++++ pkg/http/middleware/body_limit_test.go | 115 ++++++++++++++++++++ pkg/http/middleware/mcp_parse.go | 4 + pkg/http/middleware/mcp_parse_test.go | 65 +++++++++++ pkg/http/middleware/scope_challenge.go | 4 + pkg/http/middleware/scope_challenge_test.go | 79 ++++++++++++++ pkg/http/server.go | 5 + 9 files changed, 422 insertions(+) create mode 100644 pkg/http/middleware/body_limit.go create mode 100644 pkg/http/middleware/body_limit_test.go create mode 100644 pkg/http/middleware/scope_challenge_test.go diff --git a/pkg/http/handler.go b/pkg/http/handler.go index 935bca1c0e..417f32679b 100644 --- a/pkg/http/handler.go +++ b/pkg/http/handler.go @@ -132,6 +132,9 @@ func NewHTTPMcpHandler( func (h *Handler) RegisterMiddleware(r chi.Router) { r.Use( + // Must run first: bounds the request body before any other + // middleware (or the MCP SDK) reads or buffers it. + middleware.WithMaxBodySize(h.maxRequestBodyBytes()), middleware.ExtractUserToken(h.oauthCfg), middleware.WithRequestConfig, middleware.WithMCPParse(), @@ -143,6 +146,15 @@ func (h *Handler) RegisterMiddleware(r chi.Router) { } } +// maxRequestBodyBytes returns the configured request-body size limit, or +// middleware.DefaultMaxRequestBodyBytes if none was configured. +func (h *Handler) maxRequestBodyBytes() int64 { + if h.config != nil && h.config.MaxRequestBodyBytes > 0 { + return h.config.MaxRequestBodyBytes + } + return middleware.DefaultMaxRequestBodyBytes +} + // RegisterRoutes registers the routes for the MCP server // URL-based values take precedence over header-based values func (h *Handler) RegisterRoutes(r chi.Router) { diff --git a/pkg/http/handler_test.go b/pkg/http/handler_test.go index 13e87fc034..b6998d89ad 100644 --- a/pkg/http/handler_test.go +++ b/pkg/http/handler_test.go @@ -1287,3 +1287,87 @@ func TestUIMetaStrippedWhenClientLacksCapability(t *testing.T) { require.Len(t, unknown, 1) require.NotNil(t, unknown[0].Tool.Meta["ui"], "_meta.ui should be preserved when capability is unknown and FF is on") } + +// TestRegisterMiddleware_MaxRequestBodySize verifies that RegisterMiddleware +// wires the body-size limit ahead of the body-consuming middleware, so an +// oversized request never reaches the MCP server, and that requests within +// the configured limit (including exactly at the boundary) still succeed. +func TestRegisterMiddleware_MaxRequestBodySize(t *testing.T) { + const limit = 256 + + apiHost, err := utils.NewAPIHost("https://api.github.com") + require.NoError(t, err) + + buildBody := func(size int) string { + payload := `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"pad":"PADDING"}}` + if len(payload) >= size { + return payload + } + pad := strings.Repeat("x", size-len(payload)) + return strings.Replace(payload, "PADDING", "PADDING"+pad, 1) + } + + newHandler := func(t *testing.T, mcpServerFactoryCalled *bool) http.Handler { + t.Helper() + handler := NewHTTPMcpHandler( + context.Background(), + &ServerConfig{Version: "test", MaxRequestBodyBytes: limit}, + nil, + translations.NullTranslationHelper, + slog.Default(), + apiHost, + WithInventoryFactory(func(_ *http.Request) (*inventory.Inventory, error) { + return inventory.NewBuilder().Build() + }), + WithGitHubMCPServerFactory(func(_ *http.Request, _ github.ToolDependencies, _ *inventory.Inventory, _ *github.MCPServerConfig) (*mcp.Server, error) { + if mcpServerFactoryCalled != nil { + *mcpServerFactoryCalled = true + } + return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil), nil + }), + WithScopeFetcher(allScopesFetcher{}), + ) + + r := chi.NewRouter() + handler.RegisterMiddleware(r) + handler.RegisterRoutes(r) + return r + } + + t.Run("oversized request is rejected before reaching the MCP server", func(t *testing.T) { + var mcpServerFactoryCalled bool + r := newHandler(t, &mcpServerFactoryCalled) + + body := buildBody(limit + 1) + require.Greater(t, len(body), limit) + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + req.Header.Set(headers.AuthorizationHeader, strings.Join([]string{"ghs", "test-token"}, "_")) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + assert.Contains(t, rr.Body.String(), "request body too large") + assert.False(t, mcpServerFactoryCalled, "the MCP server should never be constructed for an oversized request") + }) + + t.Run("boundary-size request at the configured limit succeeds", func(t *testing.T) { + var mcpServerFactoryCalled bool + r := newHandler(t, &mcpServerFactoryCalled) + + body := buildBody(limit) + require.Len(t, body, limit) + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + req.Header.Set(headers.ContentTypeHeader, headers.ContentTypeJSON) + req.Header.Set(headers.AcceptHeader, strings.Join([]string{headers.ContentTypeJSON, headers.ContentTypeEventStream}, ", ")) + req.Header.Set(headers.AuthorizationHeader, strings.Join([]string{"ghs", "test-token"}, "_")) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code, "response body: %s", rr.Body.String()) + assert.True(t, mcpServerFactoryCalled, "the MCP server should be constructed for an allowed request") + }) +} diff --git a/pkg/http/middleware/body_limit.go b/pkg/http/middleware/body_limit.go new file mode 100644 index 0000000000..8d18a878a6 --- /dev/null +++ b/pkg/http/middleware/body_limit.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "errors" + "net/http" +) + +// DefaultMaxRequestBodyBytes bounds the size of HTTP request bodies accepted +// by the MCP endpoints when no explicit limit is configured. +const DefaultMaxRequestBodyBytes int64 = 10 << 20 // 10 MiB + +// WithMaxBodySize returns middleware that bounds the size of the request +// body. It must be registered before any middleware that reads or buffers +// the body (e.g. WithMCPParse, WithScopeChallenge) so that an oversized +// payload is rejected before it is ever fully buffered in memory, rather than +// relying on a size guard applied later by the MCP SDK or a downstream +// handler. +// +// When Content-Length is known and already exceeds maxBytes, the request is +// rejected immediately without touching the body. Otherwise the body is +// wrapped with http.MaxBytesReader, so any subsequent read (including +// chunked or unknown-length bodies) fails with a *http.MaxBytesError once +// maxBytes have been consumed. +func WithMaxBodySize(maxBytes int64) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.ContentLength > maxBytes { + writeRequestTooLarge(w) + return + } + + if r.Body != nil { + r.Body = http.MaxBytesReader(w, r.Body, maxBytes) + } + + next.ServeHTTP(w, r) + }) + } +} + +// writeRequestTooLarge writes the standard "request body too large" response. +// Every middleware that reads the request body should use this so oversized +// requests get a consistent, clear response regardless of which layer +// detects the overflow. +func writeRequestTooLarge(w http.ResponseWriter) { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) +} + +// isMaxBytesError reports whether err resulted from a body exceeding the +// limit applied by WithMaxBodySize, as opposed to some other read failure. +func isMaxBytesError(err error) bool { + var maxBytesErr *http.MaxBytesError + return errors.As(err, &maxBytesErr) +} diff --git a/pkg/http/middleware/body_limit_test.go b/pkg/http/middleware/body_limit_test.go new file mode 100644 index 0000000000..0bcb4746f9 --- /dev/null +++ b/pkg/http/middleware/body_limit_test.go @@ -0,0 +1,115 @@ +package middleware + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// unknownLengthBody wraps a reader without exposing a Len method, so +// httptest.NewRequest cannot infer Content-Length from it. This mirrors a +// chunked-transfer-encoded request, where the body size is unknown upfront. +func unknownLengthBody(s string) io.Reader { + return io.NopCloser(strings.NewReader(s)) +} + +func TestWithMaxBodySize(t *testing.T) { + const limit = 16 + + t.Run("allowed request under the limit passes through", func(t *testing.T) { + var nextCalled bool + var readBody string + var readErr error + + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + nextCalled = true + b, err := io.ReadAll(r.Body) + readBody, readErr = string(b), err + }) + + handler := WithMaxBodySize(limit)(next) + + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader("short")) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.True(t, nextCalled, "next handler should be called for an allowed request") + require.NoError(t, readErr) + assert.Equal(t, "short", readBody) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("boundary size exactly at the limit is allowed", func(t *testing.T) { + body := strings.Repeat("a", limit) + + var nextCalled bool + var readBody string + var readErr error + + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + nextCalled = true + b, err := io.ReadAll(r.Body) + readBody, readErr = string(b), err + }) + + handler := WithMaxBodySize(limit)(next) + + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.True(t, nextCalled, "next handler should be called when the body is exactly at the limit") + require.NoError(t, readErr, "reading exactly maxBytes should not error") + assert.Equal(t, body, readBody, "the full boundary-size body should be readable") + }) + + t.Run("oversized request with known Content-Length is rejected before next runs", func(t *testing.T) { + body := strings.Repeat("a", limit+1) + + var nextCalled bool + next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + nextCalled = true + }) + + handler := WithMaxBodySize(limit)(next) + + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + require.Equal(t, int64(limit+1), req.ContentLength, "test setup: Content-Length should be known") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.False(t, nextCalled, "next handler must not run for an oversized request") + assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + assert.Contains(t, rr.Body.String(), "request body too large") + }) + + t.Run("oversized request with unknown length fails on downstream read", func(t *testing.T) { + body := strings.Repeat("a", limit+1) + + var nextCalled bool + var readErr error + + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + nextCalled = true + _, readErr = io.ReadAll(r.Body) + }) + + handler := WithMaxBodySize(limit)(next) + + req := httptest.NewRequest(http.MethodPost, "/mcp", unknownLengthBody(body)) + require.Equal(t, int64(-1), req.ContentLength, "test setup: Content-Length should be unknown") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.True(t, nextCalled, "next handler still runs; the limit is enforced on read") + require.Error(t, readErr) + assert.True(t, isMaxBytesError(readErr), "expected a *http.MaxBytesError, got %v", readErr) + }) +} diff --git a/pkg/http/middleware/mcp_parse.go b/pkg/http/middleware/mcp_parse.go index c82616b270..21848fdeb4 100644 --- a/pkg/http/middleware/mcp_parse.go +++ b/pkg/http/middleware/mcp_parse.go @@ -54,6 +54,10 @@ func WithMCPParse() func(http.Handler) http.Handler { // Read the request body body, err := io.ReadAll(r.Body) if err != nil { + if isMaxBytesError(err) { + writeRequestTooLarge(w) + return + } // Log but continue - don't block requests on parse errors next.ServeHTTP(w, r) return diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index 5a28a30c3b..682c8116cf 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -189,3 +189,68 @@ func TestWithMCPParse_BodyRestoration(t *testing.T) { assert.Equal(t, originalBody, capturedBody, "body should be restored for downstream handlers") } + +// TestWithMCPParse_WithMaxBodySize composes the body-size limit with +// WithMCPParse, mirroring the production middleware ordering where +// WithMaxBodySize runs first. It verifies that an oversized body is rejected +// with a clear 413 before parsing runs, while requests within the limit +// (including exactly at the boundary) still parse and preserve the body. +func TestWithMCPParse_WithMaxBodySize(t *testing.T) { + const limit = 128 + + buildBody := func(size int) string { + payload := `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"test_tool","arguments":{"pad":"PADDING"}}}` + if len(payload) >= size { + return payload + } + // Pad the JSON with a longer string value so we can hit an exact byte size. + pad := strings.Repeat("x", size-len(payload)) + return strings.Replace(payload, "PADDING", "PADDING"+pad, 1) + } + + t.Run("oversized body is rejected before parsing", func(t *testing.T) { + body := buildBody(limit + 1) + require.Greater(t, len(body), limit) + + var nextCalled bool + nextHandler := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + nextCalled = true + }) + + handler := WithMaxBodySize(limit)(WithMCPParse()(nextHandler)) + + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.False(t, nextCalled, "downstream handler must not run for an oversized request") + assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + assert.Contains(t, rr.Body.String(), "request body too large") + }) + + t.Run("boundary-size body is parsed and preserved", func(t *testing.T) { + body := buildBody(limit) + require.Len(t, body, limit) + + var capturedInfo *ghcontext.MCPMethodInfo + var capturedBody string + nextHandler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + capturedInfo, _ = ghcontext.MCPMethod(r.Context()) + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + capturedBody = string(b) + }) + + handler := WithMaxBodySize(limit)(WithMCPParse()(nextHandler)) + + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + require.NotNil(t, capturedInfo, "MCPMethodInfo should be parsed for an allowed request") + assert.Equal(t, "tools/call", capturedInfo.Method) + assert.Equal(t, "test_tool", capturedInfo.ItemName) + assert.Equal(t, body, capturedBody, "body should be preserved for downstream handlers") + }) +} diff --git a/pkg/http/middleware/scope_challenge.go b/pkg/http/middleware/scope_challenge.go index 1a86bf93ce..4ac3bab906 100644 --- a/pkg/http/middleware/scope_challenge.go +++ b/pkg/http/middleware/scope_challenge.go @@ -54,6 +54,10 @@ func WithScopeChallenge(oauthCfg *oauth.Config, scopeFetcher scopes.FetcherInter // Fallback: parse the request body directly body, err := io.ReadAll(r.Body) if err != nil { + if isMaxBytesError(err) { + writeRequestTooLarge(w) + return + } next.ServeHTTP(w, r) return } diff --git a/pkg/http/middleware/scope_challenge_test.go b/pkg/http/middleware/scope_challenge_test.go new file mode 100644 index 0000000000..1e315892ac --- /dev/null +++ b/pkg/http/middleware/scope_challenge_test.go @@ -0,0 +1,79 @@ +package middleware + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + ghcontext "github.com/github/github-mcp-server/pkg/context" + "github.com/github/github-mcp-server/pkg/http/oauth" + "github.com/github/github-mcp-server/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestWithScopeChallenge_MaxBodySize verifies the fallback body-parsing path +// (used when WithMCPParse has not already populated MCPMethodInfo in +// context) respects the request-body size limit and returns a clear 413 +// instead of silently continuing, when composed with WithMaxBodySize as it +// is in production. +func TestWithScopeChallenge_MaxBodySize(t *testing.T) { + const limit = 64 + oauthCfg := &oauth.Config{} + fetcher := &mockScopeFetcher{scopes: []string{"repo"}} + + newRequest := func(body string) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + ctx := ghcontext.WithTokenInfo(req.Context(), &ghcontext.TokenInfo{ + Token: "******", + TokenType: utils.TokenTypeOAuthAccessToken, + }) + return req.WithContext(ctx) + } + + t.Run("oversized body is rejected before the fallback parse", func(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"` + strings.Repeat("x", limit) + `"}}` + require.Greater(t, len(body), limit) + + var nextCalled bool + next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + nextCalled = true + }) + + handler := WithMaxBodySize(limit)(WithScopeChallenge(oauthCfg, fetcher)(next)) + + req := newRequest(body) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.False(t, nextCalled, "downstream handler must not run for an oversized request") + assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + assert.Contains(t, rr.Body.String(), "request body too large") + }) + + t.Run("allowed body still reaches the fallback parse and next handler", func(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"tools/list"}` + require.LessOrEqual(t, len(body), limit) + + var nextCalled bool + var capturedBody string + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + nextCalled = true + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + capturedBody = string(b) + }) + + handler := WithMaxBodySize(limit)(WithScopeChallenge(oauthCfg, fetcher)(next)) + + req := newRequest(body) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.True(t, nextCalled, "downstream handler should run for an allowed request") + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, body, capturedBody, "body should be preserved for downstream handlers") + }) +} diff --git a/pkg/http/server.go b/pkg/http/server.go index 6bf48a07a7..e68f420c70 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -108,6 +108,11 @@ type ServerConfig struct { // MRTRStateKey is a Base64-encoded 32-byte key used to protect multi-round-trip request state. MRTRStateKey string + // MaxRequestBodyBytes bounds the size of HTTP request bodies accepted by + // the MCP endpoints, enforced before any middleware reads or buffers the + // body. When zero, middleware.DefaultMaxRequestBodyBytes is used. + MaxRequestBodyBytes int64 + disableDeleteRepository bool } From 82b29b917609330cac15aceeab68dee4cc22aaf8 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 19 Aug 2026 14:51:12 +0200 Subject: [PATCH 2/4] test: exercise MaxBytesError branches with unknown-length bodies 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> --- pkg/http/middleware/mcp_parse_test.go | 9 ++++++++- pkg/http/middleware/scope_challenge_test.go | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index 682c8116cf..0f261fc2cf 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -219,7 +219,14 @@ func TestWithMCPParse_WithMaxBodySize(t *testing.T) { handler := WithMaxBodySize(limit)(WithMCPParse()(nextHandler)) - req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + // Use an unknown-length body so WithMaxBodySize can't reject the + // request via its known-Content-Length fast path (already covered by + // body_limit_test.go). This forces the request through to + // WithMCPParse's own io.ReadAll call, exercising its *http.MaxBytesError + // handling. + req := httptest.NewRequest(http.MethodPost, "/mcp", unknownLengthBody(body)) + require.Equal(t, int64(-1), req.ContentLength, "test setup: Content-Length should be unknown") + rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) diff --git a/pkg/http/middleware/scope_challenge_test.go b/pkg/http/middleware/scope_challenge_test.go index 1e315892ac..221e8c19ba 100644 --- a/pkg/http/middleware/scope_challenge_test.go +++ b/pkg/http/middleware/scope_challenge_test.go @@ -24,8 +24,8 @@ func TestWithScopeChallenge_MaxBodySize(t *testing.T) { oauthCfg := &oauth.Config{} fetcher := &mockScopeFetcher{scopes: []string{"repo"}} - newRequest := func(body string) *http.Request { - req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + newRequestWithBody := func(body io.Reader) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/mcp", body) ctx := ghcontext.WithTokenInfo(req.Context(), &ghcontext.TokenInfo{ Token: "******", TokenType: utils.TokenTypeOAuthAccessToken, @@ -33,6 +33,10 @@ func TestWithScopeChallenge_MaxBodySize(t *testing.T) { return req.WithContext(ctx) } + newRequest := func(body string) *http.Request { + return newRequestWithBody(strings.NewReader(body)) + } + t.Run("oversized body is rejected before the fallback parse", func(t *testing.T) { body := `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"` + strings.Repeat("x", limit) + `"}}` require.Greater(t, len(body), limit) @@ -44,7 +48,14 @@ func TestWithScopeChallenge_MaxBodySize(t *testing.T) { handler := WithMaxBodySize(limit)(WithScopeChallenge(oauthCfg, fetcher)(next)) - req := newRequest(body) + // Use an unknown-length body so WithMaxBodySize can't reject the + // request via its known-Content-Length fast path (already covered by + // body_limit_test.go). This forces the request through to + // WithScopeChallenge's fallback io.ReadAll call, exercising its + // *http.MaxBytesError handling. + req := newRequestWithBody(unknownLengthBody(body)) + require.Equal(t, int64(-1), req.ContentLength, "test setup: Content-Length should be unknown") + rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) From 8bdd5798d019e81f8dd94d05cb98a6d63bc80d6d Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 19 Aug 2026 15:57:00 +0200 Subject: [PATCH 3/4] Align request-body limit with the MCP SDK default 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> --- pkg/http/handler.go | 10 +-- pkg/http/handler_test.go | 75 ++++++++++++++------- pkg/http/middleware/body_limit.go | 32 ++++----- pkg/http/middleware/body_limit_test.go | 5 +- pkg/http/middleware/mcp_parse_test.go | 15 ++--- pkg/http/middleware/scope_challenge_test.go | 14 ++-- pkg/http/server.go | 3 +- 7 files changed, 81 insertions(+), 73 deletions(-) diff --git a/pkg/http/handler.go b/pkg/http/handler.go index 417f32679b..a1b02152b5 100644 --- a/pkg/http/handler.go +++ b/pkg/http/handler.go @@ -132,8 +132,7 @@ func NewHTTPMcpHandler( func (h *Handler) RegisterMiddleware(r chi.Router) { r.Use( - // Must run first: bounds the request body before any other - // middleware (or the MCP SDK) reads or buffers it. + // Must run first: bounds the body before anything downstream reads it. middleware.WithMaxBodySize(h.maxRequestBodyBytes()), middleware.ExtractUserToken(h.oauthCfg), middleware.WithRequestConfig, @@ -146,8 +145,8 @@ func (h *Handler) RegisterMiddleware(r chi.Router) { } } -// maxRequestBodyBytes returns the configured request-body size limit, or -// middleware.DefaultMaxRequestBodyBytes if none was configured. +// maxRequestBodyBytes returns the effective request-body size limit, applied +// both by the early middleware and by the MCP SDK handler. func (h *Handler) maxRequestBodyBytes() int64 { if h.config != nil && h.config.MaxRequestBodyBytes > 0 { return h.config.MaxRequestBodyBytes @@ -251,6 +250,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return ghServer }, &mcp.StreamableHTTPOptions{ Stateless: true, + // Keep the SDK's own guard in step with the middleware, otherwise its + // default would silently cap a larger configured limit. + MaxRequestBodyBytes: h.maxRequestBodyBytes(), }) mcpHandler.ServeHTTP(w, r) diff --git a/pkg/http/handler_test.go b/pkg/http/handler_test.go index b6998d89ad..d959f04007 100644 --- a/pkg/http/handler_test.go +++ b/pkg/http/handler_test.go @@ -3,6 +3,7 @@ package http import ( "context" "encoding/json" + "fmt" "log/slog" "net/http" "net/http/httptest" @@ -1288,11 +1289,23 @@ func TestUIMetaStrippedWhenClientLacksCapability(t *testing.T) { require.NotNil(t, unknown[0].Tool.Meta["ui"], "_meta.ui should be preserved when capability is unknown and FF is on") } -// TestRegisterMiddleware_MaxRequestBodySize verifies that RegisterMiddleware -// wires the body-size limit ahead of the body-consuming middleware, so an -// oversized request never reaches the MCP server, and that requests within -// the configured limit (including exactly at the boundary) still succeed. -func TestRegisterMiddleware_MaxRequestBodySize(t *testing.T) { +// TestMaxRequestBodyBytes checks the effective limit tracks the MCP SDK +// default and honours an operator override. +func TestMaxRequestBodyBytes(t *testing.T) { + t.Run("defaults to the MCP SDK limit", func(t *testing.T) { + h := &Handler{config: &ServerConfig{}} + assert.Equal(t, int64(mcp.DefaultMaxRequestBodyBytes), h.maxRequestBodyBytes()) + }) + + t.Run("configured value overrides the default", func(t *testing.T) { + h := &Handler{config: &ServerConfig{MaxRequestBodyBytes: 1234}} + assert.Equal(t, int64(1234), h.maxRequestBodyBytes()) + }) +} + +// TestMaxRequestBodySizeEnforcement exercises both layers the limit is applied +// at: the early middleware, and the MCP SDK handler the request is delegated to. +func TestMaxRequestBodySizeEnforcement(t *testing.T) { const limit = 256 apiHost, err := utils.NewAPIHost("https://api.github.com") @@ -1307,9 +1320,9 @@ func TestRegisterMiddleware_MaxRequestBodySize(t *testing.T) { return strings.Replace(payload, "PADDING", "PADDING"+pad, 1) } - newHandler := func(t *testing.T, mcpServerFactoryCalled *bool) http.Handler { + newHandler := func(t *testing.T, mcpServerFactoryCalled *bool) *Handler { t.Helper() - handler := NewHTTPMcpHandler( + return NewHTTPMcpHandler( context.Background(), &ServerConfig{Version: "test", MaxRequestBodyBytes: limit}, nil, @@ -1327,47 +1340,63 @@ func TestRegisterMiddleware_MaxRequestBodySize(t *testing.T) { }), WithScopeFetcher(allScopesFetcher{}), ) + } + newRouter := func(h *Handler) http.Handler { r := chi.NewRouter() - handler.RegisterMiddleware(r) - handler.RegisterRoutes(r) + h.RegisterMiddleware(r) + h.RegisterRoutes(r) return r } - t.Run("oversized request is rejected before reaching the MCP server", func(t *testing.T) { + newRequest := func(body string) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + req.Header.Set(headers.ContentTypeHeader, headers.ContentTypeJSON) + req.Header.Set(headers.AcceptHeader, strings.Join([]string{headers.ContentTypeJSON, headers.ContentTypeEventStream}, ", ")) + req.Header.Set(headers.AuthorizationHeader, strings.Join([]string{"ghs", "test-token"}, "_")) + return req + } + + t.Run("middleware rejects an oversized request before the MCP server is built", func(t *testing.T) { var mcpServerFactoryCalled bool - r := newHandler(t, &mcpServerFactoryCalled) + r := newRouter(newHandler(t, &mcpServerFactoryCalled)) body := buildBody(limit + 1) require.Greater(t, len(body), limit) - req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) - req.Header.Set(headers.AuthorizationHeader, strings.Join([]string{"ghs", "test-token"}, "_")) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) + r.ServeHTTP(rr, newRequest(body)) assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) assert.Contains(t, rr.Body.String(), "request body too large") assert.False(t, mcpServerFactoryCalled, "the MCP server should never be constructed for an oversized request") }) - t.Run("boundary-size request at the configured limit succeeds", func(t *testing.T) { + t.Run("request at the configured limit succeeds", func(t *testing.T) { var mcpServerFactoryCalled bool - r := newHandler(t, &mcpServerFactoryCalled) + r := newRouter(newHandler(t, &mcpServerFactoryCalled)) body := buildBody(limit) require.Len(t, body, limit) - req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) - req.Header.Set(headers.ContentTypeHeader, headers.ContentTypeJSON) - req.Header.Set(headers.AcceptHeader, strings.Join([]string{headers.ContentTypeJSON, headers.ContentTypeEventStream}, ", ")) - req.Header.Set(headers.AuthorizationHeader, strings.Join([]string{"ghs", "test-token"}, "_")) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) + r.ServeHTTP(rr, newRequest(body)) assert.Equal(t, http.StatusOK, rr.Code, "response body: %s", rr.Body.String()) assert.True(t, mcpServerFactoryCalled, "the MCP server should be constructed for an allowed request") }) + + t.Run("SDK handler enforces the configured limit when the middleware is bypassed", func(t *testing.T) { + h := newHandler(t, nil) + + body := buildBody(limit + 1) + require.Greater(t, len(body), limit) + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, newRequest(body)) + + assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + assert.Contains(t, rr.Body.String(), fmt.Sprintf("request body exceeds %d bytes", limit), + "the SDK should report the configured limit, not its own default") + }) } diff --git a/pkg/http/middleware/body_limit.go b/pkg/http/middleware/body_limit.go index 8d18a878a6..897da53658 100644 --- a/pkg/http/middleware/body_limit.go +++ b/pkg/http/middleware/body_limit.go @@ -3,24 +3,22 @@ package middleware import ( "errors" "net/http" + + "github.com/modelcontextprotocol/go-sdk/mcp" ) -// DefaultMaxRequestBodyBytes bounds the size of HTTP request bodies accepted -// by the MCP endpoints when no explicit limit is configured. -const DefaultMaxRequestBodyBytes int64 = 10 << 20 // 10 MiB +// DefaultMaxRequestBodyBytes tracks the MCP SDK's own request-body limit, so +// enforcing it earlier in the chain does not change which requests are accepted. +const DefaultMaxRequestBodyBytes int64 = mcp.DefaultMaxRequestBodyBytes -// WithMaxBodySize returns middleware that bounds the size of the request -// body. It must be registered before any middleware that reads or buffers -// the body (e.g. WithMCPParse, WithScopeChallenge) so that an oversized -// payload is rejected before it is ever fully buffered in memory, rather than -// relying on a size guard applied later by the MCP SDK or a downstream -// handler. +// WithMaxBodySize bounds the size of the request body. It must be registered +// before any middleware that reads or buffers the body (WithMCPParse, +// WithScopeChallenge), so an oversized payload is rejected before it is +// buffered in memory rather than by a later guard in the MCP SDK. // -// When Content-Length is known and already exceeds maxBytes, the request is -// rejected immediately without touching the body. Otherwise the body is -// wrapped with http.MaxBytesReader, so any subsequent read (including -// chunked or unknown-length bodies) fails with a *http.MaxBytesError once -// maxBytes have been consumed. +// A body of unknown length (chunked, HTTP/2) cannot be rejected upfront, so +// the limit is instead enforced on read and surfaces as a *http.MaxBytesError +// to whichever middleware reads the body first. func WithMaxBodySize(maxBytes int64) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -38,16 +36,10 @@ func WithMaxBodySize(maxBytes int64) func(http.Handler) http.Handler { } } -// writeRequestTooLarge writes the standard "request body too large" response. -// Every middleware that reads the request body should use this so oversized -// requests get a consistent, clear response regardless of which layer -// detects the overflow. func writeRequestTooLarge(w http.ResponseWriter) { http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) } -// isMaxBytesError reports whether err resulted from a body exceeding the -// limit applied by WithMaxBodySize, as opposed to some other read failure. func isMaxBytesError(err error) bool { var maxBytesErr *http.MaxBytesError return errors.As(err, &maxBytesErr) diff --git a/pkg/http/middleware/body_limit_test.go b/pkg/http/middleware/body_limit_test.go index 0bcb4746f9..c82e4e2724 100644 --- a/pkg/http/middleware/body_limit_test.go +++ b/pkg/http/middleware/body_limit_test.go @@ -11,9 +11,8 @@ import ( "github.com/stretchr/testify/require" ) -// unknownLengthBody wraps a reader without exposing a Len method, so -// httptest.NewRequest cannot infer Content-Length from it. This mirrors a -// chunked-transfer-encoded request, where the body size is unknown upfront. +// unknownLengthBody hides Len from httptest.NewRequest so ContentLength is -1, +// as it is for a chunked or HTTP/2 request. func unknownLengthBody(s string) io.Reader { return io.NopCloser(strings.NewReader(s)) } diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index 0f261fc2cf..0965c6db61 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -190,11 +190,8 @@ func TestWithMCPParse_BodyRestoration(t *testing.T) { assert.Equal(t, originalBody, capturedBody, "body should be restored for downstream handlers") } -// TestWithMCPParse_WithMaxBodySize composes the body-size limit with -// WithMCPParse, mirroring the production middleware ordering where -// WithMaxBodySize runs first. It verifies that an oversized body is rejected -// with a clear 413 before parsing runs, while requests within the limit -// (including exactly at the boundary) still parse and preserve the body. +// TestWithMCPParse_WithMaxBodySize mirrors the production middleware ordering, +// where WithMaxBodySize runs ahead of WithMCPParse. func TestWithMCPParse_WithMaxBodySize(t *testing.T) { const limit = 128 @@ -203,7 +200,6 @@ func TestWithMCPParse_WithMaxBodySize(t *testing.T) { if len(payload) >= size { return payload } - // Pad the JSON with a longer string value so we can hit an exact byte size. pad := strings.Repeat("x", size-len(payload)) return strings.Replace(payload, "PADDING", "PADDING"+pad, 1) } @@ -219,11 +215,8 @@ func TestWithMCPParse_WithMaxBodySize(t *testing.T) { handler := WithMaxBodySize(limit)(WithMCPParse()(nextHandler)) - // Use an unknown-length body so WithMaxBodySize can't reject the - // request via its known-Content-Length fast path (already covered by - // body_limit_test.go). This forces the request through to - // WithMCPParse's own io.ReadAll call, exercising its *http.MaxBytesError - // handling. + // An unknown length skips WithMaxBodySize's Content-Length fast path, + // so the overflow surfaces from WithMCPParse's own read. req := httptest.NewRequest(http.MethodPost, "/mcp", unknownLengthBody(body)) require.Equal(t, int64(-1), req.ContentLength, "test setup: Content-Length should be unknown") diff --git a/pkg/http/middleware/scope_challenge_test.go b/pkg/http/middleware/scope_challenge_test.go index 221e8c19ba..9ce67bc7cd 100644 --- a/pkg/http/middleware/scope_challenge_test.go +++ b/pkg/http/middleware/scope_challenge_test.go @@ -14,11 +14,8 @@ import ( "github.com/stretchr/testify/require" ) -// TestWithScopeChallenge_MaxBodySize verifies the fallback body-parsing path -// (used when WithMCPParse has not already populated MCPMethodInfo in -// context) respects the request-body size limit and returns a clear 413 -// instead of silently continuing, when composed with WithMaxBodySize as it -// is in production. +// TestWithScopeChallenge_MaxBodySize covers the fallback body-parsing path, +// used when WithMCPParse has not already populated MCPMethodInfo in context. func TestWithScopeChallenge_MaxBodySize(t *testing.T) { const limit = 64 oauthCfg := &oauth.Config{} @@ -48,11 +45,8 @@ func TestWithScopeChallenge_MaxBodySize(t *testing.T) { handler := WithMaxBodySize(limit)(WithScopeChallenge(oauthCfg, fetcher)(next)) - // Use an unknown-length body so WithMaxBodySize can't reject the - // request via its known-Content-Length fast path (already covered by - // body_limit_test.go). This forces the request through to - // WithScopeChallenge's fallback io.ReadAll call, exercising its - // *http.MaxBytesError handling. + // An unknown length skips WithMaxBodySize's Content-Length fast path, + // so the overflow surfaces from the fallback read. req := newRequestWithBody(unknownLengthBody(body)) require.Equal(t, int64(-1), req.ContentLength, "test setup: Content-Length should be unknown") diff --git a/pkg/http/server.go b/pkg/http/server.go index e68f420c70..0cac24af30 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -109,8 +109,7 @@ type ServerConfig struct { MRTRStateKey string // MaxRequestBodyBytes bounds the size of HTTP request bodies accepted by - // the MCP endpoints, enforced before any middleware reads or buffers the - // body. When zero, middleware.DefaultMaxRequestBodyBytes is used. + // the MCP endpoints. When zero, middleware.DefaultMaxRequestBodyBytes is used. MaxRequestBodyBytes int64 disableDeleteRepository bool From 0f7f2de8c0a3eb4b9beccd01d51f27cc56298d6a Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 19 Aug 2026 16:05:28 +0200 Subject: [PATCH 4/4] Set the request-body limit to 5 MiB at both layers 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> --- pkg/http/handler.go | 4 ++-- pkg/http/handler_test.go | 40 ++++++++++++++++++++++++------- pkg/http/middleware/body_limit.go | 10 ++++---- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/pkg/http/handler.go b/pkg/http/handler.go index a1b02152b5..e4a9d198ec 100644 --- a/pkg/http/handler.go +++ b/pkg/http/handler.go @@ -250,8 +250,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return ghServer }, &mcp.StreamableHTTPOptions{ Stateless: true, - // Keep the SDK's own guard in step with the middleware, otherwise its - // default would silently cap a larger configured limit. + // Required, not just belt-and-braces: the effective limit exceeds the + // SDK's own default, which would otherwise cap it. MaxRequestBodyBytes: h.maxRequestBodyBytes(), }) diff --git a/pkg/http/handler_test.go b/pkg/http/handler_test.go index d959f04007..f051084785 100644 --- a/pkg/http/handler_test.go +++ b/pkg/http/handler_test.go @@ -15,6 +15,7 @@ import ( ghcontext "github.com/github/github-mcp-server/pkg/context" "github.com/github/github-mcp-server/pkg/github" "github.com/github/github-mcp-server/pkg/http/headers" + "github.com/github/github-mcp-server/pkg/http/middleware" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" @@ -1289,12 +1290,16 @@ func TestUIMetaStrippedWhenClientLacksCapability(t *testing.T) { require.NotNil(t, unknown[0].Tool.Meta["ui"], "_meta.ui should be preserved when capability is unknown and FF is on") } -// TestMaxRequestBodyBytes checks the effective limit tracks the MCP SDK -// default and honours an operator override. +// TestMaxRequestBodyBytes checks the effective limit and, critically, that it +// sits above the MCP SDK default — which is why it must also be passed to +// StreamableHTTPOptions rather than left to the SDK. func TestMaxRequestBodyBytes(t *testing.T) { - t.Run("defaults to the MCP SDK limit", func(t *testing.T) { + t.Run("default leaves headroom above the MCP SDK limit", func(t *testing.T) { h := &Handler{config: &ServerConfig{}} - assert.Equal(t, int64(mcp.DefaultMaxRequestBodyBytes), h.maxRequestBodyBytes()) + + assert.Equal(t, int64(5<<20), h.maxRequestBodyBytes()) + assert.Greater(t, h.maxRequestBodyBytes(), int64(mcp.DefaultMaxRequestBodyBytes), + "the default intentionally exceeds the SDK limit, so the SDK must be told about it") }) t.Run("configured value overrides the default", func(t *testing.T) { @@ -1320,11 +1325,11 @@ func TestMaxRequestBodySizeEnforcement(t *testing.T) { return strings.Replace(payload, "PADDING", "PADDING"+pad, 1) } - newHandler := func(t *testing.T, mcpServerFactoryCalled *bool) *Handler { + newHandler := func(t *testing.T, maxBytes int64, mcpServerFactoryCalled *bool) *Handler { t.Helper() return NewHTTPMcpHandler( context.Background(), - &ServerConfig{Version: "test", MaxRequestBodyBytes: limit}, + &ServerConfig{Version: "test", MaxRequestBodyBytes: maxBytes}, nil, translations.NullTranslationHelper, slog.Default(), @@ -1359,7 +1364,7 @@ func TestMaxRequestBodySizeEnforcement(t *testing.T) { t.Run("middleware rejects an oversized request before the MCP server is built", func(t *testing.T) { var mcpServerFactoryCalled bool - r := newRouter(newHandler(t, &mcpServerFactoryCalled)) + r := newRouter(newHandler(t, limit, &mcpServerFactoryCalled)) body := buildBody(limit + 1) require.Greater(t, len(body), limit) @@ -1374,7 +1379,7 @@ func TestMaxRequestBodySizeEnforcement(t *testing.T) { t.Run("request at the configured limit succeeds", func(t *testing.T) { var mcpServerFactoryCalled bool - r := newRouter(newHandler(t, &mcpServerFactoryCalled)) + r := newRouter(newHandler(t, limit, &mcpServerFactoryCalled)) body := buildBody(limit) require.Len(t, body, limit) @@ -1387,7 +1392,7 @@ func TestMaxRequestBodySizeEnforcement(t *testing.T) { }) t.Run("SDK handler enforces the configured limit when the middleware is bypassed", func(t *testing.T) { - h := newHandler(t, nil) + h := newHandler(t, limit, nil) body := buildBody(limit + 1) require.Greater(t, len(body), limit) @@ -1399,4 +1404,21 @@ func TestMaxRequestBodySizeEnforcement(t *testing.T) { assert.Contains(t, rr.Body.String(), fmt.Sprintf("request body exceeds %d bytes", limit), "the SDK should report the configured limit, not its own default") }) + + // The default headroom only exists if it reaches the SDK as well; leaving + // the SDK on its own default would silently reject this request. + t.Run("unconfigured handler accepts a request above the MCP SDK limit", func(t *testing.T) { + var mcpServerFactoryCalled bool + r := newRouter(newHandler(t, 0, &mcpServerFactoryCalled)) + + body := buildBody(mcp.DefaultMaxRequestBodyBytes + 1024) + require.Greater(t, int64(len(body)), int64(mcp.DefaultMaxRequestBodyBytes)) + require.Less(t, int64(len(body)), middleware.DefaultMaxRequestBodyBytes) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, newRequest(body)) + + assert.Equal(t, http.StatusOK, rr.Code, "response body: %s", rr.Body.String()) + assert.True(t, mcpServerFactoryCalled, "the MCP server should be constructed for an allowed request") + }) } diff --git a/pkg/http/middleware/body_limit.go b/pkg/http/middleware/body_limit.go index 897da53658..59fc8c0bc7 100644 --- a/pkg/http/middleware/body_limit.go +++ b/pkg/http/middleware/body_limit.go @@ -3,13 +3,13 @@ package middleware import ( "errors" "net/http" - - "github.com/modelcontextprotocol/go-sdk/mcp" ) -// DefaultMaxRequestBodyBytes tracks the MCP SDK's own request-body limit, so -// enforcing it earlier in the chain does not change which requests are accepted. -const DefaultMaxRequestBodyBytes int64 = mcp.DefaultMaxRequestBodyBytes +// DefaultMaxRequestBodyBytes bounds the total HTTP request, not just the tool +// payload within it. It sits modestly above the MCP SDK's own default to leave +// room for JSON-RPC and tool-call envelope overhead; because it is the larger +// of the two, callers must also pass it to the SDK or the SDK would cap it. +const DefaultMaxRequestBodyBytes int64 = 5 << 20 // 5 MiB // WithMaxBodySize bounds the size of the request body. It must be registered // before any middleware that reads or buffers the body (WithMCPParse,