You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sse timeout set dont work & always close after 5mins #876
Hey there! The problem is that os kills the network connection after 5min of inactivity. The simplest way to prevent it is send some data regularly, "notifications/progress" MCP message works perfectly for it.
Please find below working example with 20min task (here I send a notification progress message every 30sec):
minimal-sse-fix.ts:
importexpressfrom'express';import{Client}from'@modelcontextprotocol/sdk/client/index.js';import{SSEClientTransport}from'@modelcontextprotocol/sdk/client/sse.js';import{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import{SSEServerTransport}from'@modelcontextprotocol/sdk/server/sse.js';import{z}from'zod';// Test: 20-minute task with 30-minute timeout and progress notificationsconstTASK_SECONDS=1200;// 20 minutesconstCLIENT_TIMEOUT=1800000;// 30 minutes// ServerfunctioncreateServer(){constapp=express();app.use(express.json());consttransports: Record<string,SSEServerTransport>={};constserver=newMcpServer({name: 'test',version: '1.0.0'},{capabilities: {}});server.tool('task','Long task with progress',{seconds: z.number()},async({ seconds },{ sendNotification })=>{conststart=Date.now();for(letelapsed=0;elapsed<seconds;elapsed+=30){awaitnewPromise(r=>setTimeout(r,Math.min(30,seconds-elapsed)*1000));constactualElapsed=Math.round((Date.now()-start)/1000);constremaining=Math.max(0,seconds-actualElapsed);awaitsendNotification({method: "notifications/progress",params: {progressToken: "task-progress",progress: (actualElapsed/seconds)*100,message: `${actualElapsed}s/${seconds}s (${remaining}s remaining)`}});if(actualElapsed>=seconds)break;}return{content: [{type: 'text',text: `Completed ${seconds}s task`}]};});app.get('/sse',async(req,res)=>{consttransport=newSSEServerTransport('/messages',res);transports[transport.sessionId]=transport;transport.onclose=()=>deletetransports[transport.sessionId];awaitserver.connect(transport);});app.post('/messages',async(req,res)=>{constsessionId=req.query.sessionIdasstring;consttransport=transports[sessionId];if(!transport)returnres.status(404).send('Not found');awaittransport.handlePostMessage(req,res,req.body);});returnapp.listen(3004);}// Client
async functionrunTest(): Promise<boolean>{const transport =newSSEClientTransport(newURL('http://localhost:3004/sse'));constclient=newClient({name: 'test',version: '1.0.0'},{capabilities: {}});conststart=Date.now();try{awaitclient.connect(transport);console.log(`[${ts()}] Starting ${TASK_SECONDS}s task...`);awaitclient.callTool({name: 'task',arguments: {seconds: TASK_SECONDS}},undefined,{timeout: CLIENT_TIMEOUT});constduration=(Date.now()-start)/1000;console.log(`[${ts()}] ✅ SUCCESS: Completed after ${duration.toFixed(1)}s`);returntrue;}catch(error: any){constduration=(Date.now()-start)/1000;console.log(`[${ts()}] ❌ FAILED: ${error.message} (after ${duration.toFixed(1)}s)`);returnfalse;}finally{awaitclient.close();awaittransport.close();}}// Mainfunctionts(){returnnewDate().toISOString().slice(11,19);}asyncfunctionmain(){console.log(`[${ts()}] Testing: 20-minute task with progress notifications (no timeout config)`);constserver=createServer();awaitnewPromise(r=>setTimeout(r,1000));constresult=awaitrunTest();server.close();console.log(`\n[${ts()}] `+(result ?
'🎯 SOLUTION: Progress notifications keep SSE alive past 20 minutes!' :
'❌ Still fails - more investigation needed'));}if(import.meta.url===`file://${process.argv[1]}`){main().catch(console.error);}
The reproduction uses the old SSE server transport (SSEServerTransport), which writes nothing while a tool runs, so a client or proxy that gives up on a silent stream after five minutes (Node's fetch does) drops it; the request timeout option does not change that. That transport is deprecated: Streamable HTTP sends a keep-alive comment every 15 seconds since 1.30.0 (#2538) and 2.0.0 (#2541), and on SSE the progress notifications shown above keep the stream open. Closing.
Activity
antonpk1 commentedon Aug 18, 2025
Hey there! The problem is that os kills the network connection after 5min of inactivity. The simplest way to prevent it is send some data regularly, "notifications/progress" MCP message works perfectly for it.
Please find below working example with 20min task (here I send a notification progress message every 30sec):
minimal-sse-fix.ts:
package.json:
{ "name": "issue_876", "version": "1.0.0", "type": "module", "main": "index.js", "scripts": { "minimal-fix": "npx tsx minimal-sse-fix.ts" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.3", "express": "^5.1.0", "zod": "^3.25.76" }, "devDependencies": { "@types/express": "^5.0.3", "@types/node": "^24.3.0", "ts-node": "^10.9.2", "tsx": "^4.20.4", "typescript": "^5.9.2" } }output:
Hope that helps! MCP team, should we add this tip to README.md?
ochafik commentedon Sep 17, 2025
@antonpk1 good idea, may want to point to references of os-specific default timeouts if applicable
pja-ant commentedon Oct 13, 2025
The spec actually says that it should be sending pings anyway, which would keep the connection alive.
https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping
I don't think implementations do that now, so it's a bug. I don't think clients should have to do this manually.
sstklen commentedon Feb 24, 2026
Hey! I ran into a similar pattern in our bug knowledge base and thought this might help.
The fix mentioned above looks right — we've seen the same pattern work in other projects. ✅
Added this to our shared KB so other folks hitting the same issue can find it faster.
🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
feat(server): add keepAliveInterval to standalone GET SSE stream
felixweinberger commentedon Oct 5, 2026
The reproduction uses the old SSE server transport (
SSEServerTransport), which writes nothing while a tool runs, so a client or proxy that gives up on a silent stream after five minutes (Node'sfetchdoes) drops it; the requesttimeoutoption does not change that. That transport is deprecated: Streamable HTTP sends a keep-alive comment every 15 seconds since 1.30.0 (#2538) and 2.0.0 (#2541), and on SSE the progress notifications shown above keep the stream open. Closing.