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
Unhandled 'EPIPE' in 'StdioServerTransport' causes fatal process crash on client disconnect #1564
There is a stability issue in the StdioServerTransport implementation of the TypeScript SDK. If an MCP client sends a request but abruptly closes the stdio pipes (stdin/stdout) before the server can write the response, the Node.js process throws an unhandled EPIPE exception and fatally crashes.
Because stdout.write() is not wrapped in a try/catch or lacking an 'error' event listener on the stream, standard client-side timeouts or abrupt disconnects lead to a complete server crash rather than a graceful shutdown. This acts as a Denial of Service (DoS) for local AI agents relying on the server.
Steps to Reproduce
Here is a minimal Python Proof of Concept (PoC) that reproduces the crash by initiating a valid handshake and instantly closing the pipe:
importsubprocessimporttimeimportjson# 1. Start any MCP server using the TS SDK (e.g., server-everything)process=subprocess.Popen(
["npx", "-y", "@modelcontextprotocol/server-everything"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
# 2. Send a valid JSON-RPC initialization requestreq= {
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "poc", "version": "1.0"}
}
}
process.stdin.write(json.dumps(req) +"\n")
process.stdin.flush()
# 3. Instantly close the pipes before Node can replyprocess.stdin.close()
process.stdout.close()
# 4. Read the stderr crash logtime.sleep(1)
print(process.stderr.read())
Actual Behavior (Stack Trace)
node:events:486
throw er; // Unhandled 'error' event
^
Error: EPIPE: broken pipe, write
at Socket._write (node:internal/net:75:18)
at writeOrBuffer (node:internal/streams/writable:570:12)
at _write (node:internal/streams/writable:499:10)
at Writable.write (node:internal/streams/writable:508:10)
at file:///.../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js:66:30
at new Promise (<anonymous>)
at StdioServerTransport.send (file:///.../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js:64:16)
Expected Behavior
The transport layer should catch the EPIPE error and handle it gracefully (e.g., emitting a close event and shutting down the session cleanly) rather than bubbling up as an unhandled exception that kills the host process.
Hey! I ran into a similar pattern in our bug knowledge base and thought this might help.
What's happening: StdioServerTransport.send() calls stdout.write() without any error handling. When a client disconnects abruptly (closes stdin/stdout pipes), the subsequent write to the now-broken stdout pipe emits an unhandled 'EPIPE' error. Node.js treats unhandled stream errors as fatal exceptions, crashing the entire server process. The core issue is that stream.write() can fail asynchronously via the 'error' event or synchronously throw, and neither path is caught in the current implementation.
What worked for us:
Add error handling around stdout.write() in StdioServerTransport: (1) attach an 'error' event listener on the stdout stream to catch async EPIPE/ERR_STREAM_DESTROYED errors gracefully, and (2) wrap the write() call in try/catch for synchronous throws. On EPIPE, log a debug message and trigger graceful shutdown instead of crashing.
Steps:
In StdioServerTransport constructor or start(), add stdout.on('error', handler) that catches EPIPE/ERR_STREAM_DESTROYED and calls close() gracefully instead of letting it propagate as uncaught
Wrap the stdout.write() call in send() with try/catch to handle synchronous write errors on an already-destroyed stream
In the error handler, check for err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED' and treat these as client-disconnect signals rather than fatal errors
Optionally add process.on('SIGPIPE', () => {}) as a defense-in-depth measure to ignore SIGPIPE at the process level (Node.js default already ignores it, but Bun may not)
// In StdioServerTransport class:start(){// ... existing setup ...// Add error listener to prevent unhandled EPIPE crashthis._stdout.on('error',(err)=>{if(err.code==='EPIPE'||err.code==='ERR_STREAM_DESTROYED'){// Client disconnected — graceful shutdown, not a crashthis.close();}else{this.onerror?.(err);}});}asyncsend(message: JSONRPCMessage): Promise<void>{try{constjson=JSON.stringify(message);if(this._stdout.writable){this._stdout.write(json+'\n');}}catch(err: any){if(err?.code==='EPIPE'||err?.code==='ERR_STREAM_DESTROYED'){awaitthis.close();}else{throwerr;}}}
📊 We found 3 similar cases in our knowledge base with the same pattern — this gives us high confidence in this analysis.
Hope this helps! Let me know if it doesn't match your case — happy to dig deeper. 🦞
Disclosure: This analysis is from Confucius Debug, an AI-powered community KB for agent bugs. Please verify before applying.
🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.
@sstklen Thanks for the analysis! This matches what I found through fuzzing with mcpsec. PR #1566 is already addressing this - hopefully it gets merged soon.
The core issue is that any MCP server using StdioServerTransport will crash on client disconnect, which is a DoS vector.
This is a critical issue for production deployments. EPIPE handling should definitely be defensive in any transport layer.
Immediate workaround:
You can patch this at the process level until the SDK is fixed:
// Add at the top of your MCP server entry pointprocess.stdout.on('error',(err)=>{if(err.code==='EPIPE'){// Client disconnected - graceful exitconsole.error('[MCP] Client disconnected (EPIPE)');process.exit(0);}throwerr;// Re-throw other errors});// Also catch uncaughtException as a fallbackprocess.on('uncaughtException',(err)=>{if(err.code==='EPIPE'){process.exit(0);}console.error('Uncaught exception:',err);process.exit(1);});
More robust wrapper for the transport:
// Wrap the send method with EPIPE handlingconstoriginalSend=transport.send.bind(transport);transport.send=async(message: JsonRpcMessage)=>{try{awaitoriginalSend(message);}catch(err: any){if(err.code==='EPIPE'||err.code==='ERR_STREAM_DESTROYED'){// Emit close event and clean uptransport.emit('close');return;}throwerr;}};
Suggested SDK fix:
In StdioServerTransport.send(), the stdout.write() call at line 66 should be wrapped in try/catch, and the stdout stream should have an 'error' listener registered in the constructor:
Activity
fix: handle EPIPE errors in StdioServerTransport
sstklen commentedon Feb 25, 2026
Hey! I ran into a similar pattern in our bug knowledge base and thought this might help.
What's happening: StdioServerTransport.send() calls stdout.write() without any error handling. When a client disconnects abruptly (closes stdin/stdout pipes), the subsequent write to the now-broken stdout pipe emits an unhandled 'EPIPE' error. Node.js treats unhandled stream errors as fatal exceptions, crashing the entire server process. The core issue is that stream.write() can fail asynchronously via the 'error' event or synchronously throw, and neither path is caught in the current implementation.
What worked for us:
Add error handling around stdout.write() in StdioServerTransport: (1) attach an 'error' event listener on the stdout stream to catch async EPIPE/ERR_STREAM_DESTROYED errors gracefully, and (2) wrap the write() call in try/catch for synchronous throws. On EPIPE, log a debug message and trigger graceful shutdown instead of crashing.
Steps:
📊 We found 3 similar cases in our knowledge base with the same pattern — this gives us high confidence in this analysis.
Hope this helps! Let me know if it doesn't match your case — happy to dig deeper. 🦞
🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.
manthanghasadiya commentedon Feb 25, 2026
@sstklen Thanks for the analysis! This matches what I found through fuzzing with mcpsec. PR #1566 is already addressing this - hopefully it gets merged soon.
The core issue is that any MCP server using StdioServerTransport will crash on client disconnect, which is a DoS vector.
xXMrNidaXx commentedon Feb 25, 2026
This is a critical issue for production deployments. EPIPE handling should definitely be defensive in any transport layer.
Immediate workaround:
You can patch this at the process level until the SDK is fixed:
More robust wrapper for the transport:
Suggested SDK fix:
In
StdioServerTransport.send(), thestdout.write()call at line 66 should be wrapped in try/catch, and thestdoutstream should have an'error'listener registered in the constructor:Happy to submit a PR if helpful.
fix: handle EPIPE errors in StdioServerTransport
fix: handle EPIPE errors in StdioServerTransport gracefully
fix: handle EPIPE errors in StdioServerTransport