Skip to content

Commit f9fa9c7

Browse files
committed
fix(stdio): satisfy pyright and cover dup-fd fallback
- Widen `_wrap_stdio_text_stream`'s parameter type from `TextIOWrapper` to `typing.TextIO` so `sys.stdin`/`sys.stdout` (typed `TextIO` in typeshed) pass type checking. - Add a regression test using `io.BytesIO`-backed streams to cover the `AttributeError`/`UnsupportedOperation` fallback path (needed to keep coverage at 100%).
1 parent bd5faee commit f9fa9c7

2 files changed

Lines changed: 24 additions & 1 deletion

File tree

src/mcp/server/stdio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ async def run_server():
2121
import sys
2222
from contextlib import asynccontextmanager
2323
from io import TextIOWrapper, UnsupportedOperation
24+
from typing import TextIO
2425

2526
import anyio
2627
import anyio.lowlevel
@@ -30,7 +31,7 @@ async def run_server():
3031
from mcp.shared.message import SessionMessage
3132

3233

33-
def _wrap_stdio_text_stream(stream: TextIOWrapper, mode: str, errors: str = "strict") -> anyio.AsyncFile[str]:
34+
def _wrap_stdio_text_stream(stream: TextIO, mode: str, errors: str = "strict") -> anyio.AsyncFile[str]:
3435
"""Wrap a stdio text stream without closing the original handle on teardown."""
3536
try:
3637
wrapped_stream = TextIOWrapper(

tests/server/test_stdio.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,25 @@ async def test_stdio_server_does_not_close_process_stdio(monkeypatch: pytest.Mon
131131

132132
sys.stdin.close()
133133
sys.stdout.close()
134+
135+
136+
@pytest.mark.anyio
137+
async def test_stdio_server_falls_back_when_stream_has_no_fileno(monkeypatch: pytest.MonkeyPatch):
138+
"""Streams without a real fd (e.g. pytest capture, in-memory buffers) must
139+
fall back to wrapping the underlying ``.buffer`` instead of crashing."""
140+
valid = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
141+
stdin_buf = io.BytesIO(valid.model_dump_json(by_alias=True, exclude_none=True).encode() + b"\n")
142+
stdout_buf = io.BytesIO()
143+
144+
# io.BytesIO raises UnsupportedOperation from .fileno(), forcing the
145+
# buffer-wrapping fallback in _wrap_stdio_text_stream.
146+
monkeypatch.setattr(sys, "stdin", TextIOWrapper(stdin_buf, encoding="utf-8"))
147+
monkeypatch.setattr(sys, "stdout", TextIOWrapper(stdout_buf, encoding="utf-8"))
148+
149+
with anyio.fail_after(5):
150+
async with stdio_server() as (read_stream, write_stream):
151+
await write_stream.aclose()
152+
async with read_stream: # pragma: no branch
153+
received = await read_stream.receive()
154+
assert isinstance(received, SessionMessage)
155+
assert received.message == valid

0 commit comments

Comments
 (0)