Skip to content

fix: remove stdin listeners from lifecycle guard to prevent MCP -32000 errors#237

Closed
ponythewhite wants to merge 1 commit intomksglu:mainfrom
ponythewhite:fix/lifecycle-stdin-conflict
Closed

fix: remove stdin listeners from lifecycle guard to prevent MCP -32000 errors#237
ponythewhite wants to merge 1 commit intomksglu:mainfrom
ponythewhite:fix/lifecycle-stdin-conflict

Conversation

@ponythewhite
Copy link
Copy Markdown
Contributor

@ponythewhite ponythewhite commented Apr 7, 2026

Summary

  • Removes process.stdin.resume() and end/close/error listeners from the lifecycle guard (src/lifecycle.ts)
  • These listeners conflict with MCP SDK's StdioServerTransport, which also reads from process.stdin via readline
  • Transient stdin events cause false-positive parent-death detection → immediate process.exit(0) → client receives MCP error -32000: Connection closed

Root Cause

The lifecycle guard (added in #103) registers stdin listeners to detect parent death. However, in an MCP stdio transport, StdioServerTransport is the rightful owner of process.stdin. Two consumers competing for the same stream causes:

  1. process.stdin.resume() puts stdin into flowing mode before the transport is ready, risking data loss during handshake
  2. end/close/error listeners fire on transient pipe events that don't indicate actual parent death, triggering immediate shutdown with no grace period

What's Preserved

Parent death detection remains robust via:

  • ppid polling (every 30s) — detects reparenting to init/systemd/launchd
  • SIGTERM/SIGINT/SIGHUP signal handlers — catch intentional shutdown

Test Plan

  • Verify MCP server starts and responds to tool calls normally
  • Verify server exits when parent Claude Code session ends (ppid check + signals)
  • Confirm no more spurious -32000: Connection closed errors during extended sessions
  • Run existing test suite if available

Fixes #236

🤖 Generated with Claude Code

…0 errors

The lifecycle guard's process.stdin.resume() and end/close/error listeners
conflict with MCP SDK's StdioServerTransport, which also reads from stdin
via readline. Transient stdin events trigger false-positive parent-death
detection, causing immediate process.exit(0) and "MCP error -32000:
Connection closed" on the client side.

Parent death is still reliably detected via ppid polling (every 30s) and
OS signal handlers (SIGTERM/SIGINT/SIGHUP).

Fixes mksglu#236

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mksglu
Copy link
Copy Markdown
Owner

mksglu commented Apr 7, 2026

CI is throw error. @ponythewhite

flightlesstux added a commit to flightlesstux/context-mode that referenced this pull request Apr 12, 2026
…rors

The lifecycle guard registered end/close/error listeners on process.stdin
and called process.stdin.resume(). This conflicted with StdioServerTransport,
which owns the same stream via readline. Transient pipe events triggered
gracefulShutdown() → process.exit(0), so the MCP client logged
"MCP error -32000: Connection closed" multiple times per hour during
normal sessions.

Remove the stdin-based shutdown path entirely. ppid polling every 30s
plus SIGTERM/SIGINT/SIGHUP handlers still guarantee the orphan protection
added in mksglu#103.

Update tests/lifecycle.test.ts so the integration test asserts the new
contract: stdin close must NOT shut the guard down. The previous positive
assertion is what made PR mksglu#237 fail CI on Ubuntu/macOS.

Supersedes mksglu#237.
Closes mksglu#236.

Co-authored-by: ponythewhite <ponythewhite@users.noreply.github.com>
Co-authored-by: Ercan Ermis <eposta@ercanermis.com>
@flightlesstux
Copy link
Copy Markdown
Contributor

Opened a replacement PR that keeps this diff verbatim and also updates tests/lifecycle.test.ts so CI goes green: #255. Credit preserved via Co-authored-by. If this one stays idle, the other carries the same fix forward.

mksglu pushed a commit that referenced this pull request Apr 13, 2026
…rors (#236)

The lifecycle guard registered end/close/error listeners on process.stdin
and called process.stdin.resume(). This conflicted with StdioServerTransport,
which owns the same stream via readline. Transient pipe events triggered
gracefulShutdown() → process.exit(0), so the MCP client logged
"MCP error -32000: Connection closed" multiple times per hour during
normal sessions.

Remove the stdin-based shutdown path entirely. ppid polling every 30s
plus SIGTERM/SIGINT/SIGHUP handlers still guarantee the orphan protection
added in #103.

Update tests/lifecycle.test.ts so the integration test asserts the new
contract: stdin close must NOT shut the guard down. The previous positive
assertion is what made PR #237 fail CI on Ubuntu/macOS.

Supersedes #237.
Closes #236.

Co-authored-by: ponythewhite <ponythewhite@users.noreply.github.com>
mksglu added a commit that referenced this pull request Apr 13, 2026
…rors (#236)

Cherry-pick from main (bfbf654). Original PR #255 by contributor,
superseding #237 by @ponythewhite.

Removes process.stdin.resume() and end/close/error listeners from
lifecycle guard. These conflicted with StdioServerTransport causing
false-positive parent-death detection and -32000 errors.

ppid polling + OS signals remain as orphan detection mechanisms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mksglu
Copy link
Copy Markdown
Owner

mksglu commented Apr 13, 2026

Hey @ponythewhite — thank you for this! You nailed the root cause: stdin listeners in the lifecycle guard conflict with MCP SDK's StdioServerTransport, causing spurious -32000 errors.

We shipped the same fix on next in two commits:

  1. 156a1cd — removed stdin listeners from lifecycle guard, matching your approach
  2. ae73412 — added regression test guards to ensure stdin listeners never come back

The lifecycle guard now uses only ppid polling + OS signals (SIGTERM/SIGINT/SIGHUP), exactly as you proposed. lifecycle.ts:8 explicitly documents:

"Stdin close is NOT used as a shutdown signal — the MCP stdio transport owns stdin and transient pipe events cause spurious -32000 errors (#236)"

Closing since this is already on next. Your investigation saved us significant debugging time — the -32000 error is notoriously hard to trace back to stdin listener conflicts. 🙏

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.

Lifecycle guard stdin listeners cause spurious MCP -32000 Connection closed

3 participants