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
Using transport="stdio" closes real stdio, causing ValueError after server exits #1933
I can reproduce this on main; root cause is TextIOWrapper closing sys.stdin.buffer/sys.stdout.buffer due to auto garbage collection. os.dup() adds another pointer to the underlying resource and ensures that the stdio process doesn't prematurely get GC'd. Happy to put up a PR if this is up for grabs.
Hi, PR #2040 is ready for review, all CI green and regression test is included. The fix uses os.dup() to give TextIOWrapper its own file descriptor so closing it doesn't kill process stdio. @Kludex
Hi, I’d like to work on this issue if it’s still available. I’m planning to put together a small fix in src/mcp/server/stdio.py with tests in tests/server/test_stdio.py.
Reliability note for long-running MCP stdio servers:
If transport="stdio" closes the process's real stdin/stdout, any parent that still expects to log, receive protocol frames, or reuse the same pipes will see a hard ValueErrorafter the server exits — often misread as a model failure rather than a transport lifecycle bug.
Fail-closed pattern:
Isolate MCP stdio to dedicated pipes (never the parent process stdio).
On shutdown, emit a terminal event {stage: "mcp_stdio", status: "closed"} before tearing down pipes.
Expected-vs-actual test: start server, exchange one request, exit cleanly, assert parent logging still works and no unhandled ValueError.
Verified and reproduced on Python 3.12 (Linux x86_64) with mcp==1.25.0.
Root Cause: anyio.wrap_file(TextIOWrapper(sys.stdin.buffer)) closes the underlying buffer upon aclose(), causing subsequent sys.stdout or sys.stdin operations to raise ValueError: I/O operation on closed file.
Portability Note for os.dup(): While os.dup(sys.stdin.fileno()) resolves basic TTY/pipe closures, it raises io.UnsupportedOperation on Windows GUI applications (pythonw), frozen executables, or custom in-memory streams (io.BytesIO) that lack a native OS file descriptor.
Recommended Fix: Using a non-closing stream proxy wrapper around process-owned streams ensures process handles remain open across Linux, macOS, and Windows without requiring a native OS fileno().
Note: Acknowledging existing open PRs #2040, #2734, and #3090 tracking this issue.
I'd like to work on this. The proposed approach looks right: duplicating the fds before wrapping them prevents the async wrappers from closing the process's real stdin/stdout when the server exits. I'll open a PR with the server-side fix and a regression test shortly.
Actually, I see there are already open PRs addressing this (#2040, #2734, #3090). I'll step back from this one to avoid a duplicate and pick a different MCP issue. Thanks!
Current main appears to have addressed this now. src/mcp/server/stdio.py uses _UnownedTextWrapper, whose close() detaches instead of closing the process-owned buffer, and the fd-claim path duplicates/diverts/restores stdio explicitly. The inline comment also references issue #1933 and PR #3117. That should cover the original TextIOWrapper lifecycle failure without relying solely on os.dup() being available for every custom stream. If there isn't a remaining reproduction against current main, this issue looks closable.
Closing the process’s real stdin/stdout when transport="stdio" tears down leaves later prints/input raising ValueError. I will track where the stdio transport closes the underlying streams, stop closing the process-owned fds (only the transport wrappers), and add a regression test that runs a stdio server then writes to stdout after exit without error.
Activity
adityuhkapoor commentedon Feb 12, 2026
I can reproduce this on main; root cause is TextIOWrapper closing sys.stdin.buffer/sys.stdout.buffer due to auto garbage collection. os.dup() adds another pointer to the underlying resource and ensures that the stdio process doesn't prematurely get GC'd. Happy to put up a PR if this is up for grabs.
fix: prevent stdio_server from closing process stdio handles
fix: prevent stdio_server from closing process stdio handles
adityuhkapoor commentedon Feb 17, 2026
Hi, PR #2040 is ready for review, all CI green and regression test is included. The fix uses os.dup() to give TextIOWrapper its own file descriptor so closing it doesn't kill process stdio. @Kludex
fix: preserve stdin/stdout file descriptors after server exits
fix: use os.dup to prevent closing real stdio after server exits
Fix modelcontextprotocol#1933: Prevent closing real stdio when server…
Fix modelcontextprotocol#1933: Format code with ruff
fix(server): prevent stdio transport from closing original stdio handles
GitAashishG commentedon Apr 26, 2026
Hi, I’d like to work on this issue if it’s still available. I’m planning to put together a small fix in
src/mcp/server/stdio.pywith tests intests/server/test_stdio.py.Never close a buffer the transport does not own
sherlocklovercn commentedon Jul 24, 2026
I'd like to help with this if it's still needed.
I see several open PRs (#2040, #3090, …). Happy to:
Please let me know which is preferred.
Never close a buffer the transport does not own
IgorGanapolsky commentedon Jul 24, 2026
Reliability note for long-running MCP stdio servers:
If
transport="stdio"closes the process's real stdin/stdout, any parent that still expects to log, receive protocol frames, or reuse the same pipes will see a hardValueErrorafter the server exits — often misread as a model failure rather than a transport lifecycle bug.Fail-closed pattern:
{stage: "mcp_stdio", status: "closed"}before tearing down pipes.Pure free diagnostic notes — no product pitch.
camirian commentedon Jul 30, 2026
Verified and reproduced on Python 3.12 (Linux x86_64) with
mcp==1.25.0.Root Cause:
anyio.wrap_file(TextIOWrapper(sys.stdin.buffer))closes the underlying buffer uponaclose(), causing subsequentsys.stdoutorsys.stdinoperations to raiseValueError: I/O operation on closed file.Portability Note for
os.dup(): Whileos.dup(sys.stdin.fileno())resolves basic TTY/pipe closures, it raisesio.UnsupportedOperationon Windows GUI applications (pythonw), frozen executables, or custom in-memory streams (io.BytesIO) that lack a native OS file descriptor.Recommended Fix: Using a non-closing stream proxy wrapper around process-owned streams ensures process handles remain open across Linux, macOS, and Windows without requiring a native OS
fileno().Note: Acknowledging existing open PRs #2040, #2734, and #3090 tracking this issue.
RahilOp commentedon Aug 7, 2026
I'd like to work on this. The proposed approach looks right: duplicating the fds before wrapping them prevents the async wrappers from closing the process's real stdin/stdout when the server exits. I'll open a PR with the server-side fix and a regression test shortly.
RahilOp commentedon Aug 7, 2026
Actually, I see there are already open PRs addressing this (#2040, #2734, #3090). I'll step back from this one to avoid a duplicate and pick a different MCP issue. Thanks!
yashkhou commentedon Sep 28, 2026
Current
mainappears to have addressed this now.src/mcp/server/stdio.pyuses_UnownedTextWrapper, whoseclose()detaches instead of closing the process-owned buffer, and the fd-claim path duplicates/diverts/restores stdio explicitly. The inline comment also references issue #1933 and PR #3117. That should cover the originalTextIOWrapperlifecycle failure without relying solely onos.dup()being available for every custom stream. If there isn't a remaining reproduction against currentmain, this issue looks closable.HarshXAI commentedon Sep 29, 2026
I can fix this.
Closing the process’s real stdin/stdout when
transport="stdio"tears down leaves later prints/inputraisingValueError. I will track where the stdio transport closes the underlying streams, stop closing the process-owned fds (only the transport wrappers), and add a regression test that runs a stdio server then writes to stdout after exit without error.Will open a PR once the repro is green locally.