Skip to content

Commit 432746b

Browse files
committed
fix: prevent CRLF in stdio_server
1 parent 616476f commit 432746b

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

src/mcp/server/stdio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3939
# python is platform-dependent (Windows is particularly problematic), so we
4040
# re-wrap the underlying binary stream to ensure UTF-8.
4141
if not stdin:
42-
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
42+
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace", newline=""))
4343
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
44+
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline=""))
4545

4646
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4747
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

tests/server/test_stdio.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import anyio
66
import pytest
77

8+
import mcp.server.stdio as stdio_module
89
from mcp.server.stdio import stdio_server
910
from mcp.shared.message import SessionMessage
1011
from mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse, jsonrpc_message_adapter
@@ -92,3 +93,29 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
9293
second = await read_stream.receive()
9394
assert isinstance(second, SessionMessage)
9495
assert second.message == valid
96+
97+
98+
@pytest.mark.anyio
99+
async def test_stdio_server_disables_newline_translation(monkeypatch: pytest.MonkeyPatch):
100+
raw_stdin = io.BytesIO()
101+
raw_stdout = io.BytesIO()
102+
103+
monkeypatch.setattr(sys, "stdin", TextIOWrapper(raw_stdin, encoding="utf-8"))
104+
monkeypatch.setattr(sys, "stdout", TextIOWrapper(raw_stdout, encoding="utf-8"))
105+
106+
calls: list[dict[str, object | None]] = []
107+
real_text_io_wrapper = TextIOWrapper
108+
109+
def spy(buffer: io.BufferedIOBase, *args: object, **kwargs: object) -> TextIOWrapper:
110+
calls.append({"errors": kwargs.get("errors"), "newline": kwargs.get("newline")})
111+
return real_text_io_wrapper(buffer, *args, **kwargs)
112+
113+
monkeypatch.setattr(stdio_module, "TextIOWrapper", spy)
114+
115+
with anyio.fail_after(5):
116+
async with stdio_server() as (read_stream, write_stream):
117+
await write_stream.aclose()
118+
await read_stream.aclose()
119+
120+
assert {"errors": "replace", "newline": ""} in calls
121+
assert {"errors": None, "newline": ""} in calls

0 commit comments

Comments
 (0)