Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/streamable-http-body-size-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/server': patch
---

Add a default `maxBodyBytes` limit for `WebStandardStreamableHTTPServerTransport` to prevent unbounded JSON request body buffering (413 on oversized payloads).

83 changes: 82 additions & 1 deletion packages/server/src/server/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,62 @@ import {
export type StreamId = string;
export type EventId = string;

const DEFAULT_MAX_BODY_BYTES = 1_000_000; // 1MB

class PayloadTooLargeError extends Error {
constructor() {
super('payload_too_large');
this.name = 'PayloadTooLargeError';
}
}

async function readRequestTextWithLimit(req: Request, maxBytes: number): Promise<string> {
const body = req.body;
if (!body) return '';

if (Number.isFinite(maxBytes)) {
const clRaw = req.headers.get('content-length') ?? '';
const cl = Number(clRaw);
if (Number.isFinite(cl) && cl > maxBytes) {
throw new PayloadTooLargeError();
}
}

const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;

try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!value) continue;

total += value.byteLength;
if (Number.isFinite(maxBytes) && total > maxBytes) {
void reader.cancel().catch(() => {});
throw new PayloadTooLargeError();
}
chunks.push(value);
}
} finally {
try {
reader.releaseLock();
} catch {
// Ignore.
}
}

const out = new Uint8Array(total);
let offset = 0;
for (const c of chunks) {
out.set(c, offset);
offset += c.byteLength;
}

return new TextDecoder().decode(out);
}

/**
* Interface for resumability support via event storage
*/
Expand Down Expand Up @@ -152,6 +208,19 @@ export interface WebStandardStreamableHTTPServerTransportOptions {
* @default SUPPORTED_PROTOCOL_VERSIONS
*/
supportedProtocolVersions?: string[];

/**
* Maximum JSON request body size in bytes.
* Used when parsing request bodies to guard against unbounded buffering.
*
* Set to a negative number to disable the limit.
*
* Note: if you pass `parsedBody` to `handleRequest`, this limit is not applied
* (your framework/body parser must enforce its own limit).
*
* @default 1_000_000 (1 MB)
*/
maxBodyBytes?: number;
}

/**
Expand Down Expand Up @@ -231,6 +300,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
private _enableDnsRebindingProtection: boolean;
private _retryInterval?: number;
private _supportedProtocolVersions: string[];
private _maxBodyBytes: number;

sessionId?: string;
onclose?: () => void;
Expand All @@ -248,6 +318,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
this._retryInterval = options.retryInterval;
this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
this._maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
}

/**
Expand Down Expand Up @@ -625,8 +696,18 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {

let rawMessage;
if (options?.parsedBody === undefined) {
const effectiveMaxBodyBytes = this._maxBodyBytes < 0 ? Number.POSITIVE_INFINITY : this._maxBodyBytes;
let text: string;
try {
text = await readRequestTextWithLimit(req, effectiveMaxBodyBytes);
} catch (error) {
if (error instanceof PayloadTooLargeError) {
return this.createJsonErrorResponse(413, -32_000, 'Payload too large');
}
return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON');
}
try {
rawMessage = await req.json();
rawMessage = JSON.parse(text);
} catch {
return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON');
}
Expand Down
17 changes: 17 additions & 0 deletions packages/server/test/server/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,23 @@ describe('Zod v4', () => {
});

describe('POST Requests', () => {
it('should return 413 on oversized JSON request bodies', async () => {
const limitedTransport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
maxBodyBytes: 10
});
await mcpServer.connect(limitedTransport);

const request = createRequest('POST', TEST_MESSAGES.initialize);
const response = await limitedTransport.handleRequest(request);

expect(response.status).toBe(413);
const errorData = await response.json();
expectErrorResponse(errorData, -32_000, /Payload too large/);

await limitedTransport.close();
});

it('should handle post requests via SSE response correctly', async () => {
sessionId = await initializeServer();

Expand Down
Loading