Bug Description
The lifecycle guard (src/lifecycle.ts) added in #103 to prevent orphaned processes registers end/close/error listeners on process.stdin and calls process.stdin.resume(). These conflict with the MCP SDK's StdioServerTransport, which also reads from process.stdin via readline. When stdin emits a transient event, the lifecycle guard interprets it as parent death and calls process.exit(0) immediately — killing the server mid-request.
The client (Claude Code) then receives:
Error: MCP error -32000: Connection closed
This is intermittent and difficult to reproduce deterministically, but occurs frequently during normal sessions — often multiple times per hour.
Root Cause
In src/lifecycle.ts:57-63:
const onStdinClose = () => shutdown();
process.stdin.resume();
process.stdin.on("end", onStdinClose);
process.stdin.on("close", onStdinClose);
process.stdin.on("error", onStdinClose);
Two problems:
-
process.stdin.resume() puts stdin into flowing mode before StdioServerTransport sets up its own readline interface (the guard starts at server.ts:2103, transport at server.ts:2105). This can cause data loss or buffering issues during the transport handshake.
-
end/close/error on stdin — any transient pipe event triggers an immediate gracefulShutdown() → process.exit(0) with no debounce or grace period. In a stdio MCP setup, the transport legitimately manages stdin flow; transient events do not necessarily indicate parent death.
The gracefulShutdown() function (server.ts:2094-2097) also does not await transport.close() before exiting, so any in-flight JSON-RPC request is abruptly terminated.
Suggested Fix
Remove the stdin-based shutdown detection from the lifecycle guard entirely. The remaining mechanisms are sufficient:
- ppid polling (every 30s) — reliably detects parent death via reparenting to init/systemd/launchd
- SIGTERM/SIGINT/SIGHUP signal handlers — catch intentional shutdown
The stdin listeners are redundant in an MCP stdio context because the transport already handles stdin lifecycle, and they're harmful because they create false-positive shutdowns.
Minimal diff
--- a/src/lifecycle.ts
+++ b/src/lifecycle.ts
@@ -54,13 +54,6 @@ export function startLifecycleGuard(opts: LifecycleGuardOptions): () => void {
}, interval);
timer.unref();
- // P0: Stdin close — parent pipe broken
- // Must resume stdin to receive close/end events (Node starts paused)
- const onStdinClose = () => shutdown();
- process.stdin.resume();
- process.stdin.on("end", onStdinClose);
- process.stdin.on("close", onStdinClose);
- process.stdin.on("error", onStdinClose);
-
// P0: OS signals — terminal close, kill, ctrl+c
const signals: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
@@ -70,9 +63,6 @@ export function startLifecycleGuard(opts: LifecycleGuardOptions): () => void {
return () => {
stopped = true;
clearInterval(timer);
- process.stdin.removeListener("end", onStdinClose);
- process.stdin.removeListener("close", onStdinClose);
- process.stdin.removeListener("error", onStdinClose);
for (const sig of signals) process.removeListener(sig, shutdown);
};
}
Environment
- context-mode: v1.0.75
- @modelcontextprotocol/sdk: ^1.26.0 (resolved to 1.29.0)
- Node.js: v22.22.1
- Platform: Linux 6.17.0
- Client: Claude Code (CLI)
Workaround
Manually patch server.bundle.mjs in the plugin cache to remove the stdin listeners from the minified lifecycle guard function (Nx). The patch survives session restarts but is overwritten on plugin updates.
Bug Description
The lifecycle guard (
src/lifecycle.ts) added in #103 to prevent orphaned processes registersend/close/errorlisteners onprocess.stdinand callsprocess.stdin.resume(). These conflict with the MCP SDK'sStdioServerTransport, which also reads fromprocess.stdinviareadline. When stdin emits a transient event, the lifecycle guard interprets it as parent death and callsprocess.exit(0)immediately — killing the server mid-request.The client (Claude Code) then receives:
This is intermittent and difficult to reproduce deterministically, but occurs frequently during normal sessions — often multiple times per hour.
Root Cause
In
src/lifecycle.ts:57-63:Two problems:
process.stdin.resume()puts stdin into flowing mode beforeStdioServerTransportsets up its ownreadlineinterface (the guard starts atserver.ts:2103, transport atserver.ts:2105). This can cause data loss or buffering issues during the transport handshake.end/close/erroron stdin — any transient pipe event triggers an immediategracefulShutdown()→process.exit(0)with no debounce or grace period. In a stdio MCP setup, the transport legitimately manages stdin flow; transient events do not necessarily indicate parent death.The
gracefulShutdown()function (server.ts:2094-2097) also does notawait transport.close()before exiting, so any in-flight JSON-RPC request is abruptly terminated.Suggested Fix
Remove the stdin-based shutdown detection from the lifecycle guard entirely. The remaining mechanisms are sufficient:
The stdin listeners are redundant in an MCP stdio context because the transport already handles stdin lifecycle, and they're harmful because they create false-positive shutdowns.
Minimal diff
Environment
Workaround
Manually patch
server.bundle.mjsin the plugin cache to remove the stdin listeners from the minified lifecycle guard function (Nx). The patch survives session restarts but is overwritten on plugin updates.