Skip to content

Commit fbdde20

Browse files
nacxhustxiayang
authored andcommitted
mcp: limit request body size by default (#2238)
**Description** Limit by default the size of MCP request payloads. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera <nacx@apache.org> Signed-off-by: yxia216 <yxia216@bloomberg.net>
1 parent d34a29d commit fbdde20

5 files changed

Lines changed: 62 additions & 1 deletion

File tree

internal/mcpproxy/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type (
3131
tracer tracingapi.MCPTracer
3232
client http.Client
3333
logRequestHeaderAttributes map[string]string
34+
maxRequestBodySize int64 // maximum allowed POST body size in bytes
3435
}
3536

3637
mcpProxyConfig struct {

internal/mcpproxy/handlers.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,18 @@ func (m *mcpRequestContext) servePOST(w http.ResponseWriter, r *http.Request) {
276276
}
277277
}
278278

279-
body, err := io.ReadAll(r.Body)
279+
limit := m.maxRequestBodySize
280+
if limit <= 0 {
281+
limit = defaultMaxRequestBodySize
282+
}
283+
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, limit))
280284
if err != nil {
285+
var maxBytesErr *http.MaxBytesError
286+
if errors.As(err, &maxBytesErr) {
287+
errType = metrics.MCPErrorInternal
288+
onErrorResponse(w, http.StatusRequestEntityTooLarge, "request body too large")
289+
return
290+
}
281291
errType = metrics.MCPErrorInternal
282292
onErrorResponse(w, http.StatusBadRequest, err.Error())
283293
return

internal/mcpproxy/handlers_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ func TestServePOST_InvalidJSONRPC(t *testing.T) {
168168
require.Contains(t, rr.Body.String(), "invalid JSON-RPC message")
169169
}
170170

171+
func TestServePOST_OversizedBody(t *testing.T) {
172+
proxy := newTestMCPProxy()
173+
proxy.maxRequestBodySize = 16 // tiny limit to exercise the guard without allocating a large buffer.
174+
175+
body := strings.NewReader(`{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}`) // > 16 bytes
176+
req := httptest.NewRequest(http.MethodPost, "/mcp", body)
177+
rr := httptest.NewRecorder()
178+
179+
proxy.servePOST(rr, req)
180+
181+
require.Equal(t, http.StatusRequestEntityTooLarge, rr.Code)
182+
require.Contains(t, rr.Body.String(), "request body too large")
183+
}
184+
171185
func TestServePOST_InvalidSessionID(t *testing.T) {
172186
proxy := newTestMCPProxy()
173187
req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(`{"jsonrpc":"2.0","method":"tools/call","params":{"name":"test-tool"},"id":"1"}`))

internal/mcpproxy/mcpproxy.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"log/slog"
1515
"maps"
1616
"net/http"
17+
"os"
18+
"strconv"
1719
"strings"
1820
"sync"
1921
"time"
@@ -39,6 +41,20 @@ type mcpRequestContext struct {
3941
perBackendMetricsRecorded bool
4042
}
4143

44+
// defaultMaxRequestBodySize is the default maximum allowed POST body size in bytes (4 MiB).
45+
const defaultMaxRequestBodySize = 4 * 1024 * 1024
46+
47+
// getMaxRequestBodySize returns the configured POST body limit from the environment variable,
48+
// falling back to 4 MiB if the variable is unset or invalid.
49+
func getMaxRequestBodySize() int64 {
50+
if v, ok := os.LookupEnv("MCP_PROXY_MAX_REQUEST_BODY_SIZE"); ok {
51+
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 {
52+
return n
53+
}
54+
}
55+
return defaultMaxRequestBodySize
56+
}
57+
4258
// NewMCPProxy creates a new MCPProxy instance.
4359
func NewMCPProxy(l *slog.Logger, mcpMetrics metrics.MCPMetrics, tracer tracingapi.MCPTracer, sessionCrypto SessionCrypto, logRequestHeaderAttributes map[string]string) (*ProxyConfig, *http.ServeMux, error) {
4460
toolChangeSignaler := newMultiWatcherSignaler() // used to signal changes to all active sessions.
@@ -49,6 +65,7 @@ func NewMCPProxy(l *slog.Logger, mcpMetrics metrics.MCPMetrics, tracer tracingap
4965
l: l,
5066
client: http.Client{}, // No timeout as it's enforced at Envoy level.
5167
logRequestHeaderAttributes: maps.Clone(logRequestHeaderAttributes),
68+
maxRequestBodySize: getMaxRequestBodySize(),
5269
}
5370
mux := http.NewServeMux()
5471
mux.HandleFunc(

internal/mcpproxy/mcpproxy_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ func (f *fakeTracer) StartSpanAndInjectMeta(context.Context, *jsonrpc.Request, m
6060

6161
var noopTracer = tracingapi.NoopMCPTracer{}
6262

63+
func TestGetMaxRequestBodySize(t *testing.T) {
64+
for _, tc := range []struct {
65+
name string
66+
envValue string
67+
want int64
68+
}{
69+
{name: "default when env var not set", envValue: "", want: defaultMaxRequestBodySize},
70+
{name: "custom value from env var", envValue: "1048576", want: 1048576},
71+
{name: "default when env var is not a number", envValue: "not-a-number", want: defaultMaxRequestBodySize},
72+
{name: "default when env var is zero", envValue: "0", want: defaultMaxRequestBodySize},
73+
{name: "default when env var is negative", envValue: "-1", want: defaultMaxRequestBodySize},
74+
} {
75+
t.Run(tc.name, func(t *testing.T) {
76+
t.Setenv("MCP_PROXY_MAX_REQUEST_BODY_SIZE", tc.envValue)
77+
require.Equal(t, tc.want, getMaxRequestBodySize())
78+
})
79+
}
80+
}
81+
6382
func TestNewMCPProxy(t *testing.T) {
6483
l := slog.Default()
6584
proxy, mux, err := NewMCPProxy(l, stubMetrics{}, noopTracer, NewPBKDF2AesGcmSessionCrypto("test", 100), nil)

0 commit comments

Comments
 (0)