Skip to content

Commit be3f7b3

Browse files
Merge branch 'main' into fix/mcpproxy-unframed-json-sse
2 parents 4bbd643 + 78391c6 commit be3f7b3

6 files changed

Lines changed: 66 additions & 7 deletions

File tree

internal/extproc/processor_impl.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,10 @@ func (r *routerProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessRequest
267267
Header: &corev3.HeaderValue{Key: internalapi.ModelNameHeaderKeyDefault, RawValue: []byte(originalModel)},
268268
})
269269
originalPath := r.requestHeaders[":path"]
270-
if r.requestHeaders[originalPathHeader] == "" {
271-
r.requestHeaders[originalPathHeader] = originalPath
272-
additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{
273-
Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)},
274-
})
275-
}
270+
r.requestHeaders[originalPathHeader] = originalPath
271+
additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{
272+
Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)},
273+
})
276274
if r.requestHeaders[internalapi.EnvoyOriginalPathHeader] == "" {
277275
r.requestHeaders[internalapi.EnvoyOriginalPathHeader] = originalPath
278276
additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{

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
@@ -277,8 +277,18 @@ func (m *mcpRequestContext) servePOST(w http.ResponseWriter, r *http.Request) {
277277
}
278278
}
279279

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