diff --git a/.changeset/read-body-without-request-body-stream.md b/.changeset/read-body-without-request-body-stream.md new file mode 100644 index 0000000000..2376b64051 --- /dev/null +++ b/.changeset/read-body-without-request-body-stream.md @@ -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`. diff --git a/packages/server/src/server/requestBody.ts b/packages/server/src/server/requestBody.ts index a11e8d99ed..f583f94f90 100644 --- a/packages/server/src/server/requestBody.ts +++ b/packages/server/src/server/requestBody.ts @@ -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, @@ -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; diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index a1b9ce7986..5536ef7606 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -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);