Skip to content

Commit dd72cf5

Browse files
committed
close wrappers on exit to fix ResourceWarning
1 parent 2819c02 commit dd72cf5

File tree

2 files changed

+18
-14
lines changed

2 files changed

+18
-14
lines changed

src/mcp/server/stdio.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3535
from the current process' stdin and writing to stdout.
3636
"""
3737
# Re-wrap the `fd` with `closefd=False` to force UTF-8 (Windows encoding is
38-
# platform-dependant) without taking ownership of process stdio.
38+
# platform-dependent) without taking ownership of process stdio.
39+
stdin_opened = stdout_opened = False
3940
if not stdin:
4041
stdin = anyio.wrap_file(TextIOWrapper(open(sys.stdin.fileno(), "rb", closefd=False), encoding="utf-8"))
42+
stdin_opened = True
4143
if not stdout:
4244
stdout = anyio.wrap_file(TextIOWrapper(open(sys.stdout.fileno(), "wb", closefd=False), encoding="utf-8"))
43-
45+
stdout_opened = True
46+
4447
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
4548
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
4649

@@ -75,7 +78,13 @@ async def stdout_writer():
7578
except anyio.ClosedResourceError: # pragma: no cover
7679
await anyio.lowlevel.checkpoint()
7780

78-
async with anyio.create_task_group() as tg:
79-
tg.start_soon(stdin_reader)
80-
tg.start_soon(stdout_writer)
81-
yield read_stream, write_stream
81+
try:
82+
async with anyio.create_task_group() as tg:
83+
tg.start_soon(stdin_reader)
84+
tg.start_soon(stdout_writer)
85+
yield read_stream, write_stream
86+
finally:
87+
if stdin_opened:
88+
await stdin.aclose()
89+
if stdout_opened:
90+
await stdout.aclose()

tests/issues/test_1933_stdio_close.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Test for issue #1933: stdio_server closes real process stdio handles."""
22

3-
import gc
43
import io
54
import os
65
import sys
@@ -12,12 +11,7 @@
1211

1312
@pytest.mark.anyio
1413
async def test_stdio_server_preserves_process_handles():
15-
"""After stdio_server() exits, the underlying stdin/stdout fds should still be open.
16-
17-
Before the fix, TextIOWrapper took ownership of sys.stdin.buffer and
18-
sys.stdout.buffer. When the wrapper was garbage-collected, it closed the
19-
underlying buffer, permanently killing process stdio.
20-
"""
14+
"""After stdio_server() exits, the underlying stdin/stdout fds should still be open."""
2115
# Create real pipes to stand in for process stdin/stdout.
2216
# Real fds are required because the bug involves TextIOWrapper closing
2317
# the underlying fd — StringIO doesn't have file descriptors.
@@ -39,14 +33,15 @@ async def test_stdio_server_preserves_process_handles():
3933
await write_stream.aclose()
4034

4135
await read_stream.aclose()
42-
gc.collect()
4336

4437
# os.fstat raises OSError if the fd was closed
4538
os.fstat(stdin_r_fd)
4639
os.fstat(stdout_w_fd)
4740
finally:
4841
sys.stdin = saved_stdin
4942
sys.stdout = saved_stdout
43+
fake_stdin.close()
44+
fake_stdout.close()
5045
for fd in [stdin_r_fd, stdout_r_fd, stdout_w_fd]:
5146
try:
5247
os.close(fd)

0 commit comments

Comments
 (0)