Skip to content

Commit adda199

Browse files
kaXianc2-gomclaude
andcommitted
fix: stdio_server emits LF only on Windows stdout
Add newline='' to TextIOWrapper for stdout to prevent \n → \r\n translation on Windows, which corrupts newline-delimited JSON messages. The MCP protocol uses \n as the line delimiter. Emitting \r\n is a protocol-level impurity that breaks clients parsing JSON lines. stdin is intentionally left with the default newline=None so that universal-newline behaviour normalises \r\n → \n on read. Fixes #2433 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 734746a commit adda199

2 files changed

Lines changed: 24 additions & 1 deletion

File tree

src/mcp/server/stdio.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
4141
if not stdin:
4242
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
4343
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
44+
# newline="" prevents \n → \r\n translation on Windows, which would
45+
# corrupt newline-delimited JSON messages (the MCP protocol uses \n).
46+
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline=""))
4547

4648
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4749
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

tests/server/test_stdio.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,24 @@ async def lifespan(server: MCPServer) -> AsyncIterator[None]:
169169
assert events == ["setup", "cleanup"]
170170
response = jsonrpc_message_adapter.validate_json(captured.getvalue().decode().strip())
171171
assert response == JSONRPCResponse(jsonrpc="2.0", id=1, result={})
172+
173+
174+
def test_mcpserver_run_stdio_emits_lf_not_crlf(monkeypatch: pytest.MonkeyPatch) -> None:
175+
"""`MCPServer.run("stdio")` emits LF only, not CRLF, on Windows.
176+
177+
The default stdout path in stdio_server() wraps sys.stdout.buffer with
178+
newline="" so JSON lines end with \\n regardless of platform.
179+
"""
180+
ping = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
181+
stdin_bytes = io.BytesIO(ping.model_dump_json(by_alias=True, exclude_none=True).encode() + b"\n")
182+
captured = _KeepOpenBytesIO()
183+
# Simulate a "default" stdout WITHOUT newline="" — the fix in stdio_server
184+
# must still produce LF-only output by wrapping sys.stdout.buffer itself.
185+
monkeypatch.setattr(sys, "stdin", TextIOWrapper(stdin_bytes, encoding="utf-8"))
186+
monkeypatch.setattr(sys, "stdout", TextIOWrapper(captured, encoding="utf-8"))
187+
188+
_run_stdio_bounded(MCPServer(name="LfStdioServer"))
189+
190+
raw = captured.getvalue()
191+
assert b"\r\n" not in raw, f"stdout should not contain CRLF: {raw!r}"
192+
assert raw.endswith(b"\n"), f"stdout should end with LF: {raw!r}"

0 commit comments

Comments
 (0)