Skip to content

Commit 1bb9cf3

Browse files
committed
feat: enhance SSE support in MCP server and proxy by preventing timeouts and ensuring connection persistence
1 parent 5fc4f56 commit 1bb9cf3

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

packages/mcp/src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,32 @@ export async function ensureLocalHttpServer(): Promise<void> {
481481
}
482482
});
483483

484+
// --- NEW: Prevent SSE connections from being terminated by default Node timeouts ---
485+
try {
486+
// Disable per-request inactivity timeout (prevents ~2-5 min server-side termination across Node versions)
487+
// @ts-expect-error - requestTimeout exists on http.Server in Node, but TS may not include in some lib targets
488+
localHttpServer.requestTimeout = 0;
489+
490+
// Disable header timeout to avoid premature close before stream fully established on slow environments
491+
// @ts-expect-error - headersTimeout exists on http.Server in Node
492+
localHttpServer.headersTimeout = 0;
493+
494+
// Also set legacy/general socket timeout to 0 (no timeout)
495+
// @ts-expect-error - setTimeout on http.Server sets the socket timeout
496+
typeof localHttpServer.setTimeout === 'function' && localHttpServer.setTimeout(0);
497+
498+
// Keep TCP connection alive to maintain long-lived SSE streams; send probes every 60s
499+
localHttpServer.on('connection', (socket: any) => {
500+
try {
501+
socket.setKeepAlive?.(true, 60_000);
502+
socket.setNoDelay?.(true);
503+
} catch {}
504+
});
505+
} catch {
506+
// Best-effort hardening; ignore if any of the props/methods are unavailable in the host Node
507+
}
508+
// -------------------------------------------------------------------------------
509+
484510
await new Promise<void>((resolve, reject) => {
485511
localHttpServer!.listen(0, '127.0.0.1', () => resolve());
486512
localHttpServer!.once('error', reject);

packages/next/src/mcp-route.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,23 @@ async function proxy(method: string, req: NextRequest): Promise<Response> {
4444
const qs = req.nextUrl.search;
4545
if (qs) upstreamUrl.search = qs;
4646

47+
// Prepare headers to forward; nuke hop-by-hop headers
48+
const forwardHeaders = copyHeaders(req.headers);
49+
50+
// Ensure SSE friendliness on GET (defensive in case a client omits Accept)
51+
if (method === 'GET') {
52+
const accept = forwardHeaders.get('accept') || '';
53+
if (!/\btext\/event-stream\b/i.test(accept)) {
54+
forwardHeaders.set('accept', accept ? `${accept}, text/event-stream` : 'text/event-stream');
55+
}
56+
forwardHeaders.set('connection', 'keep-alive');
57+
// Clients are recommended to include MCP-Protocol-Version; we simply forward what we received.
58+
}
59+
4760
// Build init for fetch
4861
const init: RequestInit = {
4962
method,
50-
headers: copyHeaders(req.headers)
63+
headers: forwardHeaders
5164
};
5265

5366
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
@@ -57,10 +70,23 @@ async function proxy(method: string, req: NextRequest): Promise<Response> {
5770

5871
// Stream response back to the client
5972
const upstream = await fetch(upstreamUrl, init);
60-
const headers = copyHeaders(upstream.headers);
73+
74+
// Copy upstream response headers, then harden for SSE responses
75+
const respHeaders = copyHeaders(upstream.headers);
76+
const ct = upstream.headers.get('content-type') || '';
77+
78+
if (/^text\/event-stream\b/i.test(ct)) {
79+
// Prevent buffering and keep the connection alive for SSE
80+
respHeaders.set('content-type', 'text/event-stream');
81+
respHeaders.set('cache-control', 'no-cache, no-transform');
82+
respHeaders.set('connection', 'keep-alive');
83+
// Disable proxy buffering where applicable (e.g., nginx)
84+
respHeaders.set('x-accel-buffering', 'no');
85+
}
86+
6187
return new Response(upstream.body, {
6288
status: upstream.status,
63-
headers
89+
headers: respHeaders
6490
});
6591
}
6692

0 commit comments

Comments
 (0)