Skip to content

Commit bcf9384

Browse files
committed
fix(mcp): detect orphaned stdio host and self-terminate
StdioServerTransport only listens for stdin data/error events, so a stdio-mode server never notices when its host (Claude Code) dies without sending SIGTERM/SIGINT first. The child is left running as an orphan (ppid 1) with nothing to talk to, observed spinning at sustained high CPU indefinitely until manually killed. Detect this two ways - stdin closing and a periodic ppid check - and run the existing shutdownFn immediately once either fires.
1 parent 36ffff7 commit bcf9384

1 file changed

Lines changed: 31 additions & 1 deletion

File tree

mcp/src/index.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ function resolveTransport(env: TransportMode): TransportMode {
8888
* For stdio/http, installs `SIGINT`/`SIGTERM` handlers invoking the
8989
* transport's `shutdownFn`, plus `unhandledRejection`/`uncaughtException`
9090
* handlers logging via {@link Logger} — the latter sets `process.exitCode = 1`
91-
* without exiting immediately, letting in-flight work finish.
91+
* without exiting immediately, letting in-flight work finish. Stdio mode also
92+
* self-terminates on stdin close or a detected ppid-1 orphan, since a dead
93+
* host process never sends a signal in that case.
9294
*/
9395
async function main() {
9496
const config = loadConfig();
@@ -118,6 +120,34 @@ async function main() {
118120
await stdioTransport.close?.();
119121
await server.close();
120122
};
123+
124+
// The SDK's StdioServerTransport only listens for stdin "data"/"error"
125+
// events, so it never notices when the host (Claude Code) dies without
126+
// sending SIGTERM/SIGINT first: stdin just closes silently and this
127+
// process lingers as an orphan (ppid 1) — observed spinning at high CPU
128+
// indefinitely with nothing left to talk to. Detect that two ways —
129+
// stdin closing and a periodic ppid check, since either can fire first
130+
// depending on how the host exited — and self-terminate immediately.
131+
let orphanHandled = false;
132+
const exitIfOrphaned = (reason: string) => {
133+
if (orphanHandled) return;
134+
orphanHandled = true;
135+
clearInterval(orphanCheckInterval);
136+
logger.info("Host process gone; shutting down orphaned stdio server", { reason });
137+
shutdownFn?.()
138+
.catch((error) => {
139+
logger.error("Error during orphan shutdown", {
140+
error: error instanceof Error ? error.message : String(error),
141+
});
142+
})
143+
.finally(() => process.exit(0));
144+
};
145+
process.stdin.on("end", () => exitIfOrphaned("stdin_end"));
146+
process.stdin.on("close", () => exitIfOrphaned("stdin_close"));
147+
const orphanCheckInterval = setInterval(() => {
148+
if (process.ppid === 1) exitIfOrphaned("ppid_orphaned");
149+
}, 5000);
150+
orphanCheckInterval.unref();
121151
}
122152

123153
// ── HTTP mode (SSE + Streamable HTTP) ───────────────────────

0 commit comments

Comments
 (0)