Skip to content

fix: dup file descriptors in stdio_server to avoid closing real stdin/stdout#2172

Closed
giulio-leone wants to merge 2 commits intomodelcontextprotocol:mainfrom
giulio-leone:fix/stdio-close-real-stdin-stdout
Closed

fix: dup file descriptors in stdio_server to avoid closing real stdin/stdout#2172
giulio-leone wants to merge 2 commits intomodelcontextprotocol:mainfrom
giulio-leone:fix/stdio-close-real-stdin-stdout

Conversation

@giulio-leone
Copy link
Contributor

Problem

Running a server with transport="stdio" closes the real sys.stdin / sys.stdout when the server exits. Any subsequent stdio operation raises:

ValueError: I/O operation on closed file.

Root Cause

TextIOWrapper(sys.stdin.buffer) shares the underlying file descriptor with sys.stdin. When the TextIOWrapper is closed (or garbage-collected) after the server exits, it also closes sys.stdin.buffer, making any subsequent stdio operation fail.

Solution

Use os.dup() to duplicate the file descriptor before wrapping it in TextIOWrapper. Closing the wrapper then only closes the duplicate fd while leaving the original process handles intact.

# Before (broken)
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))

# After (fixed)
stdin = anyio.wrap_file(
    TextIOWrapper(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), encoding="utf-8")
)

Testing

  • All 15 stdio-related tests pass across 2 consecutive runs
  • Verified manually that sys.stdin.fileno() remains valid after closing the duplicated wrapper

Fixes #1933

g97iulio1609 added 2 commits February 28, 2026 15:41
…/stdout

TextIOWrapper(sys.stdin.buffer) shares the underlying fd with sys.stdin.
When the wrapper is closed (or garbage-collected) after the server exits,
it also closes sys.stdin.buffer, making any subsequent stdio operation
raise `ValueError: I/O operation on closed file`.

Use `os.dup()` to duplicate the fd before wrapping, so closing the
wrapper only closes the duplicate while leaving the original process
handles intact.

Fixes modelcontextprotocol#1933
@giulio-leone
Copy link
Contributor Author

Friendly ping — CI is green and this is ready for review. Happy to address any feedback. Thanks!

@giulio-leone
Copy link
Contributor Author

All CI checks pass. Ready for review.

@maxisbey
Copy link
Contributor

maxisbey commented Mar 5, 2026

Thanks for the PR! This fix is correct, but it's a duplicate of #2040 which was opened on Feb 12 with the same os.dup() approach plus a regression test. Consolidating there — please feel free to review/follow that PR.

AI Disclaimer

@maxisbey maxisbey closed this Mar 5, 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.

Using transport="stdio" closes real stdio, causing ValueError after server exits

2 participants