Skip to content

Commit 07e65d9

Browse files
committed
fix: strip trailing \r from lines in client stdio_reader to handle CRLF
When a Windows MCP server emits CRLF (\r\n) line endings, the client's stdout_reader splits on \n alone, leaving a trailing \r on each line. This causes JSON parsing to fail since "{...}\r" is not valid JSON. Fix by stripping trailing \r from each line before JSON parsing in the client's stdio_reader. Also adds a test that verifies CRLF-terminated messages from a server are correctly parsed. Fixes #2433
1 parent b882921 commit 07e65d9

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/mcp/client/stdio.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ async def stdout_reader():
149149
buffer = lines.pop()
150150

151151
for line in lines:
152+
# Strip any trailing \r left by CRLF line endings.
153+
# A Windows MCP server may emit \r\n, and splitting on
154+
# \n alone leaves a \r suffix that corrupts JSON parsing.
155+
line = line.rstrip("\r")
152156
try:
153157
message = types.jsonrpc_message_adapter.validate_json(line, by_name=False)
154158
except Exception as exc: # pragma: no cover

tests/client/test_stdio.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,37 @@ def sigterm_handler(signum, frame):
558558
f"stdio_client cleanup took {elapsed:.1f} seconds for stdin-ignoring process. "
559559
f"Expected between 2-4 seconds (2s stdin timeout + termination time)."
560560
)
561+
562+
563+
@pytest.mark.anyio
564+
async def test_stdio_client_handles_crlf_line_endings():
565+
"""Test that the client correctly parses JSON-RPC messages from a server
566+
that outputs CRLF (\\r\\n) line endings, as is common on Windows.
567+
568+
A Windows MCP server may write JSON messages terminated by \\r\\n. The
569+
client's stdout_reader splits on \\n, so each line retains a trailing \\r
570+
which would corrupt JSON parsing. This test verifies the CRLF is handled.
571+
"""
572+
# Python script that writes a JSON-RPC message with CRLF line endings
573+
server_script = textwrap.dedent(
574+
"""\
575+
import sys
576+
msg = '{"jsonrpc":"2.0","id":1,"result":{}}'
577+
sys.stdout.buffer.write((msg + '\\r\\n').encode('utf-8'))
578+
sys.stdout.buffer.flush()
579+
# Exit after sending one message
580+
"""
581+
)
582+
583+
server_params = StdioServerParameters(
584+
command=sys.executable,
585+
args=["-c", server_script],
586+
)
587+
588+
with anyio.fail_after(5.0):
589+
async with stdio_client(server_params) as (read_stream, write_stream):
590+
async with read_stream:
591+
message = await read_stream.receive()
592+
assert not isinstance(message, Exception), f"Expected message, got exception: {message}"
593+
assert isinstance(message, SessionMessage)
594+
assert message.message == JSONRPCResponse(jsonrpc="2.0", id=1, result={})

0 commit comments

Comments
 (0)