Summary
mcp/server/stdio.py creates TextIOWrapper(sys.stdout.buffer, encoding="utf-8") without specifying newline="". On Windows, the default newline=None causes \n → \r\n translation, so every JSON-RPC message written to stdout ends with \r\n instead of \n.
The MCP spec uses newline-delimited JSON with \n as the delimiter. Emitting \r\n is a protocol-level impurity.
Affected file
mcp/server/stdio.py lines 46–49
# Current (buggy on Windows)
if not stdin:
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))
if not stdout:
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
Reproduction
On Windows, spawn a Python subprocess that uses this code and read the raw bytes:
import subprocess, sys
script = r'''
import sys
from io import TextIOWrapper
stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
stdout.write('{"jsonrpc":"2.0","result":"ok"}\n')
stdout.flush()
'''
proc = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE)
out, _ = proc.communicate(timeout=5)
print(repr(out))
# Output on Windows: b'{"jsonrpc":"2.0","result":"ok"}\r\n'
# Output on Linux: b'{"jsonrpc":"2.0","result":"ok"}\n'
Verified on:
- OS: Windows 11 Pro (10.0.26200)
- Python: 3.11
- mcp: 1.26.0
Why this matters
While the current JS MCP SDK client (StdioClientTransport) strips trailing \r via .replace(/\r$/, "") before parsing, this is a server-side bug that:
- Violates the NDJSON wire format (which specifies LF-only line endings)
- Creates an asymmetry: the Python
stdio_client sends bare \n, but the Python stdio_server responds with \r\n
- Could break any MCP client that does a strict
split("\n") and then fails to JSON.parse the line with a trailing \r
The comment on line 43 even acknowledges: "Encoding of stdin/stdout as text streams on python is platform-dependent (Windows is particularly problematic)" — but the fix applied (re-wrap to ensure UTF-8) doesn't also fix the newline translation mode.
Fix
Add newline="" to both TextIOWrapper calls. newline="" disables translation while still operating in text mode:
if not stdin:
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", newline=""))
if not stdout:
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline=""))
With this fix:
# newline="" result:
buf = io.BytesIO()
wrapper = TextIOWrapper(buf, encoding="utf-8", newline="")
wrapper.write('{"jsonrpc":"2.0","result":"ok"}\n')
wrapper.flush()
repr(buf.getvalue())
# b'{"jsonrpc":"2.0","result":"ok"}\n' ← correct on all platforms
The same fix should be applied to the stdin wrapper so that incoming messages with bare \n are not translated either (avoiding any future issues if a client sends strict LF).
Context
This was discovered while debugging Windows MCP tool timeouts with mem0-mcp-selfhosted. The eager-init approach fixed the actual timeout, but this CRLF emission was identified as a secondary protocol-level issue during investigation.
Summary
mcp/server/stdio.pycreatesTextIOWrapper(sys.stdout.buffer, encoding="utf-8")without specifyingnewline="". On Windows, the defaultnewline=Nonecauses\n→\r\ntranslation, so every JSON-RPC message written to stdout ends with\r\ninstead of\n.The MCP spec uses newline-delimited JSON with
\nas the delimiter. Emitting\r\nis a protocol-level impurity.Affected file
mcp/server/stdio.pylines 46–49Reproduction
On Windows, spawn a Python subprocess that uses this code and read the raw bytes:
Verified on:
Why this matters
While the current JS MCP SDK client (
StdioClientTransport) strips trailing\rvia.replace(/\r$/, "")before parsing, this is a server-side bug that:stdio_clientsends bare\n, but the Pythonstdio_serverresponds with\r\nsplit("\n")and then fails toJSON.parsethe line with a trailing\rThe comment on line 43 even acknowledges: "Encoding of stdin/stdout as text streams on python is platform-dependent (Windows is particularly problematic)" — but the fix applied (
re-wrap to ensure UTF-8) doesn't also fix the newline translation mode.Fix
Add
newline=""to bothTextIOWrappercalls.newline=""disables translation while still operating in text mode:With this fix:
The same fix should be applied to the
stdinwrapper so that incoming messages with bare\nare not translated either (avoiding any future issues if a client sends strict LF).Context
This was discovered while debugging Windows MCP tool timeouts with
mem0-mcp-selfhosted. The eager-init approach fixed the actual timeout, but this CRLF emission was identified as a secondary protocol-level issue during investigation.