Skip to content

fix: preserve real stdin/stdout after stdio server exits#2323

Open
owendevereaux wants to merge 1 commit intomodelcontextprotocol:mainfrom
owendevereaux:fix/stdio-preserve-standard-handles
Open

fix: preserve real stdin/stdout after stdio server exits#2323
owendevereaux wants to merge 1 commit intomodelcontextprotocol:mainfrom
owendevereaux:fix/stdio-preserve-standard-handles

Conversation

@owendevereaux
Copy link

Summary

Fixes #1933

When running the MCP server with transport='stdio', closing the server would also close the process's real stdin/stdout handles. This caused a ValueError: I/O operation on closed file when trying to use stdio after the server exits.

Problem

The issue was that wrapping sys.stdin.buffer/sys.stdout.buffer with TextIOWrapper causes the underlying buffer to be closed when the wrapper is garbage collected, even if we don't use a context manager. The old comment said "Purposely not using context managers for these, as we don't want to close standard process handles" but this didn't prevent the closing.

Solution

Use os.dup() to duplicate the file descriptors before wrapping. When the duplicated descriptors are closed (along with the TextIOWrapper), the original stdin/stdout remain intact and usable.

For streams without a fileno() (e.g., BytesIO in tests), we fall back to wrapping them directly (previous behavior).

Test Plan

Added a regression test test_stdio_server_does_not_close_real_stdio that:

  1. Creates real file descriptors (via temp files) to simulate stdin/stdout
  2. Monkeypatches sys.stdin/stdout to use them
  3. Runs the stdio server
  4. Verifies that the original handles are NOT closed after the server exits

All existing tests continue to pass.

Reproduction (from issue)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Demo")
mcp.run(transport="stdio")
print("?")  # Previously raised: ValueError: I/O operation on closed file.

When running the MCP server with transport='stdio', closing the server
would also close the process's real stdin/stdout handles. This caused
ValueError when trying to use stdio after the server exits.

The issue was that wrapping sys.stdin.buffer/sys.stdout.buffer with
TextIOWrapper causes the underlying buffer to be closed when the
wrapper is garbage collected, even if we don't use a context manager.

Fix: Use os.dup() to duplicate the file descriptors before wrapping.
When the duplicated descriptors are closed, the original stdin/stdout
remain intact and usable.

Fallback: For streams without a fileno() (e.g., BytesIO in tests), we
fall back to wrapping them directly (previous behavior).

Fixes modelcontextprotocol#1933
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