Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/read-body-without-request-body-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

`readRequestBody`, and with it `createMcpHandler` and the Streamable HTTP transport, now reads request bodies in runtimes that have no `Request.body` stream, such as Firefox. Previously every POST there was answered `400` with a JSON-RPC parse error (`-32700`). In those runtimes the body is read whole and then checked against `maxRequestBodySize`, so oversized bodies are still answered `413`.
9 changes: 8 additions & 1 deletion packages/server/src/server/requestBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function resolveMaxRequestBodySize(value: number | undefined): number {
* Reads a request body as text, up to `maxBytes` (default
* {@linkcode DEFAULT_MAX_REQUEST_BODY_SIZE}). A declared `Content-Length` over the
* limit is refused without reading anything; otherwise the read stops as soon as
* more than the limit has arrived. Stream failures propagate.
* more than the limit has arrived. Where the runtime has no `Request.body` stream
* (Firefox), the body is read whole and then checked against the limit. Stream
* failures propagate.
*/
export async function readRequestBody(
request: Request,
Expand All @@ -40,6 +42,11 @@ export async function readRequestBody(
if (request.body === null) {
return { tooLarge: false, text: '' };
}
// Firefox does not implement `Request.body` yet, though `arrayBuffer()` works.
if (request.body === undefined) {
const bytes = await request.arrayBuffer();
return bytes.byteLength > maxBytes ? { tooLarge: true } : { tooLarge: false, text: new TextDecoder().decode(bytes) };
}
const reader = request.body.getReader();
const decoder = new TextDecoder();
let received = 0;
Expand Down
14 changes: 14 additions & 0 deletions packages/server/test/server/createMcpHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,20 @@ describe('createMcpHandler — stateless legacy fallback (the default)', () => {
}
});

it('reads the body where the runtime has no Request.body stream (Firefox), keeping the size limit', async () => {
const { factory, state } = testFactory();
const handler = createMcpHandler(factory);
const withoutBodyStream = (request: Request): Request => Object.defineProperty(request, 'body', { value: undefined });

const served = await handler.fetch(withoutBodyStream(postRequest(modernToolsCall('echo', { text: 'no body stream' }))));
expect(served.status).toBe(200);
expect(await served.text()).toContain('no body stream');

const refused = await handler.fetch(withoutBodyStream(postRequest('x'.repeat(4 * 1024 * 1024 + 1))));
expect(refused.status).toBe(413);
expect(state.contexts).toHaveLength(1);
});

it('still serves the modern path on the same endpoint (one factory, both legs)', async () => {
const { factory, state } = testFactory();
const handler = createMcpHandler(factory);
Expand Down
Loading