diff --git a/open-sse/mcp-server/httpTransport.ts b/open-sse/mcp-server/httpTransport.ts index 917acc8530..ab742858c6 100644 --- a/open-sse/mcp-server/httpTransport.ts +++ b/open-sse/mcp-server/httpTransport.ts @@ -166,6 +166,26 @@ function errorResponse(message: string, code: number, status = 400): Response { ); } +export function protectMcpSseResponse(request: Request, response: Response): Response { + if ( + request.method !== "POST" || + !response.headers.get("content-type")?.toLowerCase().includes("text/event-stream") + ) { + return response; + } + + const headers = new Headers(response.headers); + const cacheControl = headers.get("cache-control"); + if (!/(?:^|,)\s*no-transform(?:\s*(?:,|$))/i.test(cacheControl ?? "")) { + headers.set("cache-control", [cacheControl, "no-transform"].filter(Boolean).join(", ")); + } + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + function withSessionHeader(response: Response, sessionId: string): Response { if (response.headers.get("mcp-session-id")) { return response; @@ -261,7 +281,7 @@ async function handleStreamableRequest(request: Request): Promise { * Used by the Next.js route at /api/mcp/stream. */ export async function handleMcpStreamableHTTP(request: Request): Promise { - return handleStreamableRequest(request); + return protectMcpSseResponse(request, await handleStreamableRequest(request)); } /** @@ -273,7 +293,10 @@ export async function handleMcpSSE(request: Request): Promise { const { transport } = ensureSseServer(); try { - return await withMcpHttpAuthContext(request, () => handleRequestWithAuthInfo(transport, request)); + const response = await withMcpHttpAuthContext(request, () => + handleRequestWithAuthInfo(transport, request) + ); + return protectMcpSseResponse(request, response); } catch (err) { console.error("[MCP] SSE error:", err); return new Response(JSON.stringify({ error: "MCP SSE transport error" }), { diff --git a/tests/unit/mcp-sse-response-headers-8277.test.ts b/tests/unit/mcp-sse-response-headers-8277.test.ts new file mode 100644 index 0000000000..40c60ba022 --- /dev/null +++ b/tests/unit/mcp-sse-response-headers-8277.test.ts @@ -0,0 +1,59 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { protectMcpSseResponse } = await import("../../open-sse/mcp-server/httpTransport.ts"); + +test("POST JSON-RPC SSE response adds no-transform", () => { + const response = protectMcpSseResponse( + new Request("http://localhost/api/mcp/stream", { + method: "POST", + headers: { "Accept-Encoding": "gzip" }, + }), + new Response("event: message\ndata: {}\n\n", { + headers: { + "Content-Type": "Text/Event-Stream; Charset=UTF-8", + "Cache-Control": 'private="Authorization"', + Vary: "Origin", + }, + }) + ); + + assert.equal(response.headers.get("content-encoding"), null); + assert.equal(response.headers.get("cache-control"), 'private="Authorization", no-transform'); + assert.equal(response.headers.get("vary"), "Origin"); +}); + +test("SDK-protected GET SSE response remains unchanged", () => { + const response = new Response("event: ping\ndata: {}\n\n", { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + }, + }); + + assert.strictEqual( + protectMcpSseResponse(new Request("http://localhost/api/mcp/stream"), response), + response + ); +}); + +test("non-SSE response remains unchanged", () => { + const response = new Response("{}", { + headers: { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + Vary: "Accept-Encoding", + }, + }); + + assert.strictEqual( + protectMcpSseResponse( + new Request("http://localhost/api/mcp/stream", { method: "POST" }), + response + ), + response + ); + assert.equal(response.headers.get("content-encoding"), "gzip"); + assert.equal(response.headers.get("vary"), "Accept-Encoding"); +});