Skip to content

Commit 9a57007

Browse files
steps-reclaude
andcommitted
fix(stdio): handle bufferless in-memory text streams in fallback
The no-fd fallback reached for std.buffer, which raises AttributeError on bufferless text streams such as io.StringIO. Guard with hasattr and wrap the text stream directly in that case (nothing to tear down). Adds a regression test. Addresses the cubic review on #3090. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent 0e48b3e commit 9a57007

2 files changed

Lines changed: 25 additions & 0 deletions

File tree

src/mcp/server/stdio.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ def wrap_std(std: TextIO, mode: str, errors: str | None) -> anyio.AsyncFile[str]
6262
wrapper = TextIOWrapper(binary, encoding="utf-8", errors=errors)
6363
to_close.append(wrapper)
6464
except (AttributeError, OSError, ValueError, io.UnsupportedOperation):
65+
# No real fd. A bufferless in-memory text stream (e.g. io.StringIO)
66+
# has no .buffer to re-wrap, so use it directly -- it is already text,
67+
# and we did not create it, so there is nothing to tear down. Otherwise
68+
# re-wrap .buffer and detach() on exit so the finalizer cannot close the
69+
# real handle.
70+
if not hasattr(std, "buffer"):
71+
return anyio.wrap_file(std)
6572
wrapper = TextIOWrapper(std.buffer, encoding="utf-8", errors=errors)
6673
to_detach.append(wrapper)
6774
return anyio.wrap_file(wrapper)

tests/server/test_stdio.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ async def test_stdio_server_does_not_close_real_std_handles(monkeypatch: pytest.
118118
stdout_file.flush()
119119

120120

121+
@pytest.mark.anyio
122+
async def test_stdio_server_bufferless_text_streams(monkeypatch: pytest.MonkeyPatch) -> None:
123+
"""Regression: default startup must not crash on bufferless in-memory text streams.
124+
125+
In-memory text streams like `io.StringIO` have neither a real `fileno()` nor a
126+
`.buffer`, so both the fd-dup path and the `.buffer` re-wrap path fail. In that
127+
case `stdio_server()` must use the stream directly. Before the fix, the fallback
128+
reached for `std.buffer` and raised `AttributeError` during startup.
129+
"""
130+
monkeypatch.setattr(sys, "stdin", io.StringIO()) # empty -> immediate EOF
131+
monkeypatch.setattr(sys, "stdout", io.StringIO())
132+
133+
with anyio.fail_after(5):
134+
async with stdio_server() as (read_stream, write_stream):
135+
async with read_stream, write_stream:
136+
pass
137+
138+
121139
@pytest.mark.anyio
122140
async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch) -> None:
123141
"""Non-UTF-8 stdin bytes surface as an in-stream exception without killing the stream.

0 commit comments

Comments
 (0)