From 053d9046080b78ddc438e86a73d579e57b32b18d Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 09:07:49 +0100 Subject: [PATCH 1/2] fix: dup file descriptors in stdio_server to avoid closing real stdin/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 #1933 --- src/mcp/server/stdio.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index e526bab56..00cd91368 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -17,6 +17,7 @@ async def run_server(): ``` """ +import os import sys from contextlib import asynccontextmanager from io import TextIOWrapper @@ -34,14 +35,18 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. """Server transport for stdio: this communicates with an MCP client by reading from the current process' stdin and writing to stdout. """ - # Purposely not using context managers for these, as we don't want to close - # standard process handles. Encoding of stdin/stdout as text streams on - # python is platform-dependent (Windows is particularly problematic), so we - # re-wrap the underlying binary stream to ensure UTF-8. + # Duplicate the file descriptors so that closing the TextIOWrapper does not + # close the real sys.stdin / sys.stdout. Without this, any code that runs + # after the server exits (or after the transport is torn down) gets + # ``ValueError: I/O operation on closed file`` when touching stdio. if not stdin: - stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) + stdin = anyio.wrap_file( + TextIOWrapper(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), encoding="utf-8") + ) if not stdout: - stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) + stdout = anyio.wrap_file( + TextIOWrapper(os.fdopen(os.dup(sys.stdout.fileno()), "wb"), encoding="utf-8") + ) read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] From 41b86297dc0737d82575bcf4476ecde6b622fe37 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 14:05:51 +0100 Subject: [PATCH 2/2] style: apply ruff format --- src/mcp/server/stdio.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index 00cd91368..cba77d86d 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -40,13 +40,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. # after the server exits (or after the transport is torn down) gets # ``ValueError: I/O operation on closed file`` when touching stdio. if not stdin: - stdin = anyio.wrap_file( - TextIOWrapper(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), encoding="utf-8") - ) + stdin = anyio.wrap_file(TextIOWrapper(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), encoding="utf-8")) if not stdout: - stdout = anyio.wrap_file( - TextIOWrapper(os.fdopen(os.dup(sys.stdout.fileno()), "wb"), encoding="utf-8") - ) + stdout = anyio.wrap_file(TextIOWrapper(os.fdopen(os.dup(sys.stdout.fileno()), "wb"), encoding="utf-8")) read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]