Skip to content

Commit 5ac79b4

Browse files
committed
fix: prevent CRLF in stdio_server
1 parent ac96f88 commit 5ac79b4

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
@@ -8,6 +8,7 @@
88
import anyio
99
import pytest
1010

11+
import mcp.server.stdio as stdio_module
1112
from mcp.server.mcpserver import MCPServer
1213
from mcp.server.stdio import stdio_server
1314
from mcp.shared.message import SessionMessage
@@ -96,6 +97,32 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch) -> Non
9697
assert second.message == valid
9798

9899

100+
@pytest.mark.anyio
101+
async def test_stdio_server_disables_newline_translation(monkeypatch: pytest.MonkeyPatch):
102+
raw_stdin = io.BytesIO()
103+
raw_stdout = io.BytesIO()
104+
105+
monkeypatch.setattr(sys, "stdin", TextIOWrapper(raw_stdin, encoding="utf-8"))
106+
monkeypatch.setattr(sys, "stdout", TextIOWrapper(raw_stdout, encoding="utf-8"))
107+
108+
calls: list[dict[str, object | None]] = []
109+
real_text_io_wrapper = TextIOWrapper
110+
111+
def spy(buffer: io.BufferedIOBase, *args: object, **kwargs: object) -> TextIOWrapper:
112+
calls.append({"errors": kwargs.get("errors"), "newline": kwargs.get("newline")})
113+
return real_text_io_wrapper(buffer, *args, **kwargs)
114+
115+
monkeypatch.setattr(stdio_module, "TextIOWrapper", spy)
116+
117+
with anyio.fail_after(5):
118+
async with stdio_server() as (read_stream, write_stream):
119+
await write_stream.aclose()
120+
await read_stream.aclose()
121+
122+
assert {"errors": "replace", "newline": ""} in calls
123+
assert {"errors": None, "newline": ""} in calls
124+
125+
99126
class _KeepOpenBytesIO(io.BytesIO):
100127
"""A BytesIO that survives its TextIOWrapper being closed.
101128

0 commit comments

Comments
 (0)