Description
Problem Description
When a client connection closes unexpectedly (network failure, timeout, crash), the server continues executing in-flight request handlers instead of cancelling them. This leads to resource waste and prevents proper cleanup in distributed/production environments.
Current Behavior
- Client times out and closes connection
- Server's
Protocol._onclose()
is called - Response handlers are cleaned up
⚠️ Request handlers continue running until completion- Server resources (CPU, memory, external API calls) are wasted
Expected Behavior
- Client times out and closes connection
- Server's
Protocol._onclose()
is called - Response handlers are cleaned up
- ✅ Request handlers are cancelled via their AbortSignals
- Tools/handlers can clean up immediately
Root Cause
In src/shared/protocol.ts
, the _onclose()
method cleans up response handlers but does not abort request handlers:
_onclose() {
var _a;
const responseHandlers = this._responseHandlers;
this._responseHandlers = new Map();
this._progressHandlers.clear();
// ❌ Missing: Abort in-flight request handlers
// for (const controller of this._requestHandlerAbortControllers.values()) {
// controller.abort(new McpError(ErrorCode.ConnectionClosed, "Connection closed"));
// }
// this._requestHandlerAbortControllers.clear();
this._transport = undefined;
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed");
for (const handler of responseHandlers.values()) {
handler(error);
}
}
Impact
- Single-instance servers: Request handlers waste resources after client disconnects
- Distributed servers: No way to stop requests when server instances restart/crash
- Long-running operations: Continue unnecessarily (file uploads, database operations, etc.)
- Resource leaks: External API calls, file handles, network connections remain open
Proposed Solution
Add request handler cancellation to Protocol._onclose()
:
_onclose() {
var _a;
const responseHandlers = this._responseHandlers;
this._responseHandlers = new Map();
this._progressHandlers.clear();
// ✅ Add: Abort all in-flight request handlers
for (const controller of this._requestHandlerAbortControllers.values()) {
controller.abort(new McpError(ErrorCode.ConnectionClosed, "Connection closed"));
}
this._requestHandlerAbortControllers.clear();
this._transport = undefined;
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed");
for (const handler of responseHandlers.values()) {
handler(error);
}
}
Why This Matters
While clients do attempt to send explicit notifications/cancelled
messages on timeout, these can fail due to:
- Network already broken
- Server already crashed
- Connection already closed
- Transport errors
Connection closure should serve as an implicit cancellation signal, just like how response handlers are cleaned up.
Verification
The infrastructure is already in place:
- ✅
_requestHandlerAbortControllers
Map exists - ✅ AbortControllers are created per request in
_onrequest()
- ✅ Request handlers receive
extra.signal
for cancellation - ✅ Transport layers call
_onclose()
on connection drops
This is simply connecting existing cancellation infrastructure to connection close events.
Files Affected
src/shared/protocol.ts
(lines ~83-97 in_onclose()
method)
Environment:
- MCP SDK Version: 1.12.1
- Transport: StreamableHTTP, SSE, Stdio (affects all)
This issue affects production deployments where proper resource cleanup on unexpected disconnections is critical for system stability.