You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stdio transport: MCP server process survives parent death (stdin EOF not causing shutdown) #2231
Stdio transport: MCP server process survives parent death (stdin EOF not causing shutdown)
Description
When an MCP server using transport="stdio" has its parent process die (e.g. the MCP client exits, is killed, or crashes), the server process is orphaned and continues running indefinitely. It gets reparented to init/systemd and never exits.
Expected behavior
When the parent process dies and stdin reaches EOF, the server should detect this and shut down cleanly.
Analysis
Looking at the code flow in mcp/server/stdio.py:
stdin_reader() (line 60) iterates async for line in stdin where stdin is anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))
When stdin reaches EOF, the loop should end, closing read_stream_writer
This should close the read_stream, causing server.run()'s async for message in session.incoming_messages loop to end
run() returns → stdio_server() context exits → task group cancels stdout_writer → process exits
In practice, the process survives. The likely cause is that anyio.wrap_file runs the underlying readline() in a worker thread, and this thread may not wake up or propagate EOF correctly when the pipe is closed due to parent death.
The client is Claude Code 2.1.70 - 2.1.78. The servers can be found in karellen/karellen-rr-mcp and karellen/karellen-lsp-mcp. Both servers have a workaround for the issue now.
@arcivanov as I said, I wasn't able to reproduce this. Without a way to reproduce it, there's no clear path to fixing it. If you supplied code for both the client and the server which I can copy/paste to immediately see the issue then it would be much easier to confirm at fix.
Here's Claude's investigation by the way, seems to point to issues outside the SDK:
TL;DR: The SDK's stdin-EOF detection works correctly. The hang was caused by a combination of (1) Claude Code's FD leak preventing EOF from ever being delivered, and (2) a bug in the reporter's own SIGTERM handler that did cleanup but forgot to exit.
SDK's EOF detection: verified working
Ran the reporter's actual server (karellen-rr-mcp @ 8290992 — the HEAD when this issue was filed) through the stated reproduction (spawn as subprocess with stdin=PIPE → kill -9 parent). Server exits in 0.10s. Also verified across Python 3.13/3.14/3.14t, asyncio/trio, 50-iteration stress test — all exit cleanly.
Actual root cause: Claude Code FD leak + broken SIGTERM handler
Why stdin never reached EOF:anthropics/claude-code#1935 (filed June 2025, still open) documents orphaned MCP servers after Claude Code exits. Reproducing that scenario locally: when the pipe's write-end is held by another process (sibling MCP server, wrapper like npx/docker run -i, etc.), the server's stdin never reaches EOF — there's still a writer. The SDK's readline() correctly blocks until all writers close. I can only trigger the hang reported here by artificially leaking the pipe's write-end to the child.
def_handle_signal(signum, frame):
_cleanup()
# ← no exit!signal.signal(signal.SIGTERM, _handle_signal)
This overrides Python's default SIGTERM-kills-process behavior. Verified: the handler IS called (anyio does not override it) — it just returns without exiting.
The companion repo karellen-lsp-mcp shows the full failure chain at 3cf71b7:server.py#L403-L416: a getppid() watchdog detects parent death → sends SIGTERM to self → handler does _cleanup() without exiting → hang.
The reporter's own fix (f410e1e, ~9 hours after filing this issue) was to add os._exit(0) to the handler — fixing their own bug.
Test matrix
FD leak (client-side)?
SIGTERM handler
Result
no
any
exits 0.1s — SDK's EOF detection works
yes
Python default
HANGS (no mechanism to detect parent death)
yes
broken (_cleanup(), no exit)
HANGS — handler returns, server keeps running
yes
correct (os._exit(0))
exits (via ppid watchdog, if present)
The hang requires the FD leak. The broken handler only matters as a failed fallback.
What is an SDK gap (separate from this issue)
While investigating, found a genuine adjacent bug: when stdin closes while a tool handler is in-flight, Server.run()'s task group waits instead of cancelling. Fixed in #2306. This is not what the reporter hit — their tools have finite timeouts (60-300s), not indefinite hangs, and with the FD leak the task group's async for loop never even ends.
Activity
fix: Ensure stdio server exits when stdin reaches EOF
weiguangli-io commentedon Mar 11, 2026
maxisbey commentedon Mar 18, 2026
Hey @arcivanov I am unable to reproduce the issue, could you please provide example code for both the client and server I can run to see the issue?
arcivanov commentedon Mar 18, 2026
The client is Claude Code 2.1.70 - 2.1.78. The servers can be found in karellen/karellen-rr-mcp and karellen/karellen-lsp-mcp. Both servers have a workaround for the issue now.
fix: properly detect EOF on stdin to shut down server process
maxisbey commentedon Mar 20, 2026
@arcivanov as I said, I wasn't able to reproduce this. Without a way to reproduce it, there's no clear path to fixing it. If you supplied code for both the client and the server which I can copy/paste to immediately see the issue then it would be much easier to confirm at fix.
arcivanov commentedon Mar 20, 2026
Very well, if it's not easily repro then let's just close it and I'll reopen if I find a way to repro it independent of Claude code
maxisbey commentedon Mar 20, 2026
Here's Claude's investigation by the way, seems to point to issues outside the SDK:
TL;DR: The SDK's stdin-EOF detection works correctly. The hang was caused by a combination of (1) Claude Code's FD leak preventing EOF from ever being delivered, and (2) a bug in the reporter's own
SIGTERMhandler that did cleanup but forgot to exit.SDK's EOF detection: verified working
Ran the reporter's actual server (
karellen-rr-mcp@8290992— the HEAD when this issue was filed) through the stated reproduction (spawn as subprocess withstdin=PIPE→kill -9parent). Server exits in 0.10s. Also verified across Python 3.13/3.14/3.14t, asyncio/trio, 50-iteration stress test — all exit cleanly.The
async for line in stdinloop correctly detects EOF when the pipe's write-end closes.Actual root cause: Claude Code FD leak + broken
SIGTERMhandlerWhy stdin never reached EOF: anthropics/claude-code#1935 (filed June 2025, still open) documents orphaned MCP servers after Claude Code exits. Reproducing that scenario locally: when the pipe's write-end is held by another process (sibling MCP server, wrapper like
npx/docker run -i, etc.), the server's stdin never reaches EOF — there's still a writer. The SDK'sreadline()correctly blocks until all writers close. I can only trigger the hang reported here by artificially leaking the pipe's write-end to the child.Why SIGTERM didn't help: At
karellen-rr-mcp@8290992:server.py#L74-L78:This overrides Python's default SIGTERM-kills-process behavior. Verified: the handler IS called (anyio does not override it) — it just returns without exiting.
The companion repo
karellen-lsp-mcpshows the full failure chain at3cf71b7:server.py#L403-L416: agetppid()watchdog detects parent death → sendsSIGTERMto self → handler does_cleanup()without exiting → hang.The reporter's own fix (f410e1e, ~9 hours after filing this issue) was to add
os._exit(0)to the handler — fixing their own bug.Test matrix
_cleanup(), no exit)os._exit(0))The hang requires the FD leak. The broken handler only matters as a failed fallback.
What is an SDK gap (separate from this issue)
While investigating, found a genuine adjacent bug: when stdin closes while a tool handler is in-flight,
Server.run()'s task group waits instead of cancelling. Fixed in #2306. This is not what the reporter hit — their tools have finite timeouts (60-300s), not indefinite hangs, and with the FD leak the task group'sasync forloop never even ends.AI Disclaimer
fix(server): exit when MCP parent process disappears (#217)