fix: stop reading MCP request bodies past the size limit

The HTTP handler read the whole body before checking its size, so a body
without Content-Length could be buffered without bound. It now reads the
stream and stops as soon as the body passes the limit.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:36 -07:00
1 parent 6f56c84b0b
commit 1b3aa8de8b
2 files changed
+51 -3

No files matched your search

+31 -3
View File
@@ -25,9 +25,8 @@ export function createMcpHttpHandler(server) {
const declared = Number(request.headers.get('content-length'));
if (declared > MAX_BODY_BYTES)
return json(413, { error: 'Request too large' });
const text = await request.text();
if (new TextEncoder().encode(text).length > MAX_BODY_BYTES)
return json(413, { error: 'Request too large' });
const text = await readCapped(request, MAX_BODY_BYTES);
if (text === null) return json(413, { error: 'Request too large' });
let message;
try {
message = JSON.parse(text);
@@ -39,6 +38,35 @@ export function createMcpHttpHandler(server) {
};
}
/**
* Read a request body as text, stopping as soon as it passes `limit` bytes,
* so a body without a Content-Length cannot be buffered without bound.
* Returns null when the body is too large.
*/
async function readCapped(request, limit) {
if (!request.body) return '';
const reader = request.body.getReader();
const chunks = [];
let size = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > limit) {
await reader.cancel().catch(() => {});
return null;
}
chunks.push(value);
}
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
function json(status, body) {
return new Response(JSON.stringify(body), {
status,
+20
View File
@@ -176,6 +176,26 @@ test('the HTTP transport accepts one JSON message per POST', async () => {
);
assert.equal((await post({}, { 'Content-Type': 'text/plain' })).status, 415);
assert.equal((await post('x'.repeat(1024 * 1024 + 1))).status, 413);
// A streamed body without Content-Length stops being read past the limit.
let produced = 0;
const chunk = new Uint8Array(64 * 1024).fill(32);
const body = new ReadableStream({
pull(controller) {
if (produced >= 8 * 1024 * 1024) return controller.close();
produced += chunk.byteLength;
controller.enqueue(chunk);
},
});
const streamed = await handle(
new Request('http://localhost/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
duplex: 'half',
}),
);
assert.equal(streamed.status, 413);
assert.ok(produced <= 1024 * 1024 + 4 * chunk.byteLength, `read ${produced}`);
const get = await handle(new Request('http://localhost/mcp'));
assert.equal(get.status, 405);
assert.equal(get.headers.get('allow'), 'POST');