Skip to content

fix: exit the process when the stdio transport closes (stop orphaned server processes) - #23

Merged
RapierCraft merged 1 commit into
RapierCraft:mainfrom
storyarcade:fix/exit-on-stdio-close
Jun 22, 2026
Merged

fix: exit the process when the stdio transport closes (stop orphaned server processes)#23
RapierCraft merged 1 commit into
RapierCraft:mainfrom
storyarcade:fix/exit-on-stdio-close

Conversation

@storyarcade

Copy link
Copy Markdown
Contributor

Problem

The server connects the stdio transport with no close/error handling:

const transport = new StdioServerTransport();
server.connect(transport);

When the MCP client disconnects — the session ends, or a long comet_ask browser-modal stall trips the client's timeout and it tears down the pipe — the stdio channel closes, but the Node process does not exit. It keeps running, orphaned and idle, holding a CDP connection to Comet.

In practice these accumulate fast: on one machine, 7 → 34 orphaned perplexity-comet-mcp processes were observed over three days, monotonically increasing, one per session/stall. They are never reaped, and the pileup correlates with mid-session connection drops (handle pressure).

Root cause

A stdio MCP server's lifetime is bound to its stdin/stdout pipe to the client. When the client closes that pipe, the server should exit. StdioServerTransport exposes onclose/onerror; this server wires neither, so a closed pipe is silently ignored and the event loop stays alive.

Fix

Exit on transport close/error and on stdin end/close, with an idempotent guard:

let exiting = false;
const shutdown = (code = 0): void => {
  if (exiting) return;
  exiting = true;
  try { transport.close?.(); } catch { /* ignore */ }
  process.exit(code);
};
transport.onclose = () => shutdown(0);
transport.onerror = (err: unknown) => { console.error("[comet] transport error:", err instanceof Error ? err.message : err); shutdown(1); };
process.stdin.on("end", () => shutdown(0));
process.stdin.on("close", () => shutdown(0));

Notes

  • Pure lifecycle: no change to any tool behavior, prompt handling, or CDP logic.
  • If a tool call is in flight when the client disconnects, that work is abandoned on exit — correct, since there is no client to return the result to.
  • tsc is clean on this file. The two pre-existing cdp-client.ts type errors are unrelated and already present on upstream main.

Out of scope (happy to file separately)

The stalls themselves are triggered by comet_ask auto-rewriting prompts containing words like visit/open/browse/page/.ai into a "use your browser" form, which surfaces Perplexity's browser-control modal. This PR only stops the orphaned-process consequence so a stall can't leak a zombie.

The server connects StdioServerTransport with no close/error handling, so when
the MCP client disconnects (session ends, or a client-side timeout tears down
the pipe) the Node process does not exit — it lingers, orphaned, holding a CDP
connection to Comet. These accumulate one per session/stall and are never
reaped.

Exit on transport close/error and on stdin end/close, with an idempotent
shutdown guard. Pure lifecycle change; no tool behavior is affected. In-flight
work is abandoned on client disconnect (correct: there is no client to return
it to). tsc is clean on this file (the two pre-existing cdp-client.ts errors
are unrelated and present on upstream main).
@RapierCraft

Copy link
Copy Markdown
Owner

Review: PR #23fix: exit the process when the stdio transport closes

Reviewed commit: 11806cd | Review type: Correctness + reliability
Verdict: APPROVE — fixes the most critical user-facing bug (orphaned processes)


Analysis

Problem

Without this fix, when the MCP client disconnects, the server process keeps running indefinitely holding a CDP connection. 7→34 orphaned processes reported over 3 days.

Solution — CORRECT

  • transport.onclose: Exits when MCP SDK detects transport closure
  • transport.onerror: Logs error, exits with code 1
  • process.stdin.on("end"/"close"): Catches pipe closure even if SDK doesn't fire onclose
  • Idempotent guard (let exiting): Prevents double-exit when multiple signals fire simultaneously
  • transport.close?.(): Optional chaining correct — close may not exist on all implementations

Edge Cases Handled

  • Double-fire (onclose + stdin close) → idempotent guard ✓
  • Transport error → logged before exit ✓
  • transport.close() throws → caught in try/catch ✓

Advisory Notes (non-blocking)

1. MEDIUM — CDP connection not cleaned up before exit

shutdown() closes transport but does not call cometClient.disconnect(). The CDP WebSocket will be left half-open until Comet times it out. PR #20 adds complementary SIGINT/SIGTERM handlers that do CDP cleanup. Ideally both should be combined.

2. INFO — server.connect(transport) still fire-and-forget

PR #20 wraps this in .catch(). Not blocking for this PR.

3. INFO — CI red until PR #22 is merged

Pre-existing type errors unrelated to this PR.


Summary

Clean, minimal fix for a real problem. The idempotent guard and multi-signal approach is thorough. Recommend merging after PR #22.


Review complete. 0 blocking findings.

@RapierCraft
RapierCraft merged commit 4440e8a into RapierCraft:main Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants