Skip to content

Commit 28721e1

Browse files
committed
fix(mcp-server): implement SSE stream on GET /mcp to stop client polling
MCP Streamable HTTP spec requires GET /mcp to return an SSE stream (text/event-stream) or 405. Returning plain JSON caused clients to retry constantly, generating 666k requests/day from just 5 users.
1 parent 9bf56d9 commit 28721e1

File tree

1 file changed

+45
-3
lines changed

1 file changed

+45
-3
lines changed

apps/mcp-server/src/index.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function addCorsHeaders(response: Response, corsHeaders: Record<string, string>)
6363
}
6464

6565
export default {
66-
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
66+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
6767
const url = new URL(request.url);
6868
const corsHeaders = getCorsHeaders(request, env);
6969

@@ -104,8 +104,50 @@ export default {
104104
const response = await handleMcpRequest(request, env);
105105
return addCorsHeaders(response, corsHeaders);
106106
}
107-
// GET returns server info for debugging
108-
return addCorsHeaders(handleMcpInfo(), corsHeaders);
107+
108+
if (request.method === "GET") {
109+
const accept = request.headers.get("Accept") || "";
110+
111+
// MCP Streamable HTTP: return SSE stream for clients expecting event-stream
112+
if (accept.includes("text/event-stream")) {
113+
const { readable, writable } = new TransformStream();
114+
const writer = writable.getWriter();
115+
const encoder = new TextEncoder();
116+
117+
ctx.waitUntil(
118+
(async () => {
119+
try {
120+
// Initial keepalive
121+
await writer.write(encoder.encode(":ok\n\n"));
122+
// Send keepalive every 30s to hold connection open
123+
while (true) {
124+
await new Promise((resolve) => setTimeout(resolve, 30000));
125+
await writer.write(encoder.encode(":keepalive\n\n"));
126+
}
127+
} catch {
128+
// Client disconnected — stream closed
129+
}
130+
})(),
131+
);
132+
133+
request.signal.addEventListener("abort", () => {
134+
writer.close().catch(() => {});
135+
});
136+
137+
return addCorsHeaders(
138+
new Response(readable, {
139+
headers: {
140+
"Content-Type": "text/event-stream",
141+
"Cache-Control": "no-cache",
142+
},
143+
}),
144+
corsHeaders,
145+
);
146+
}
147+
148+
// Non-SSE GET returns server info for debugging
149+
return addCorsHeaders(handleMcpInfo(), corsHeaders);
150+
}
109151
}
110152

111153
// REST API endpoints

0 commit comments

Comments
 (0)