modelcontextprotocol / modelcontextprotocol/python-sdk

Windows: TextIOWrapper in stdio_server() emits CRLF instead of LF, corrupting newline-delimited JSON messages

未关闭 适合新手
#2,433 9 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

bug fix proposed P2 ready for work
主要语言
Python
星标
24.3k
派生
4k
平均合并
1 天 1 小时
30 天内合并 PR
31

描述

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:

  1. Violates the NDJSON wire format (which specifies LF-only line endings)
  2. Creates an asymmetry: the Python stdio_client sends bare \n, but the Python stdio_server responds with \r\n
  3. 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.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 src/mcp/server/stdio.py 中第 46–49 行附近的 TextIOWrapper 调用开始,然后在 Windows 上复现子进程的原始输出。完成的标准是:在不同平台上,stdin 和 stdout 都保留用于以换行分隔的 JSON 的裸 LF 字节。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
backend-api-design
Issue 类型
缺陷
难度
1/5
预计耗时
1 小时以内
活跃度
活跃
描述清晰度
描述清楚
新手友好度
88/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。