fix(mcp): keep POST SSE responses uncompressed (#8303)

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Ravi Tharuma
2026-07-24 14:35:35 +02:00
committed by GitHub
parent 1684adcd63
commit cbe49f6929
2 changed files with 84 additions and 2 deletions

View File

@@ -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<Response> {
* Used by the Next.js route at /api/mcp/stream.
*/
export async function handleMcpStreamableHTTP(request: Request): Promise<Response> {
return handleStreamableRequest(request);
return protectMcpSseResponse(request, await handleStreamableRequest(request));
}
/**
@@ -273,7 +293,10 @@ export async function handleMcpSSE(request: Request): Promise<Response> {
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" }), {

View File

@@ -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");
});