MCP -32700 Parse error 怎么修?stdout、stdio 与 Tool list failed 排查:MCP 协议文章封面 - XBSTACK

MCP -32700 Parse Error 怎么修?stdout 污染、Tool list failed 与版本排查

Release Date
2026-06-04
Reading Time
21分钟
Content Size
11,309 chars
MCP 协议
mcp-server
json-rpc
claude
cursor
troubleshooting

先给结论

  • MCP -32700 Parse Error、Tool list failed 或 Unexpected non-JSON line 怎么排查?先分离 stdout/stderr,再区分 JSON 解析、启动路径、SDK 实现和 2025/2026 MCP 协议版本差异。

适合谁读

  • 正在把 mcp / mcp-server / json-rpc / claude 落到真实项目里的开发者。
  • 不想只看概念,希望知道取舍、边界、风险和下一步怎么做的独立开发者。
  • 正在做技术选型、工具链治理、自动化工作流或个人数字资产建设的读者。

本文解决的问题

  • 为什么在 Cursor 或 Claude Desktop 中刷新工具列表时会提示 Tool list failed?
  • 怎么在 Node.js 和 Python 代码中实现 stdout 和 stderr 的物理隔离?
  • 如何在 macOS 等系统下定位 Cursor 和 Claude 的本地运行日志?
  • 怎样利用命令行裸跑和手动构造 JSON-RPC 报文来快速定位连接问题?
  • MCP Unexpected non-JSON line、-32700 与 -32600 分别代表什么?
  • MCP -32700 Parse Error 怎么修是什么?

直接答案:-32700 Parse Error 表示收到的内容无法按 JSON 解析。stdio 场景下,第一步仍然是把 stdout 与 stderr 分开:普通日志、banner、截断 JSON 或非法编码都可能污染协议流;但不要把所有 -32700 都归因于 stdout 日志。TypeScript SDK v2 的 ReadBuffer 已会跳过非 JSON stdout 行,真正的故障还可能来自损坏 JSON、JSON-RPC schema 不合法、SDK/客户端版本差异或启动阶段异常。

2026-08-10 版本边界: MCP 2026-07-28 已正式发布,并移除了旧版核心里的 initialize / initialized 握手与 Session 依赖。本文保留 2025-06-18 的手动 initialize 报文,是为了排查仍使用旧规范的 Claude/Cursor/SDK 组合;如果你的客户端已经声明 2026-07-28,应按新 stateless 生命周期和该客户端对应 SDK 文档排查,不能照搬旧握手步骤。

如果连接已经正常,只是不清楚 Tool、Resource 与扩展能力应该怎么组织,请查看:MCP Resources、Tools、Prompts、Roots 有什么区别?

先按这 5 步排查

遇到 -32700 Parse errorTool list failed 或 MCP 连接指示器变红时,先不要改业务逻辑,按下面顺序排查:

  1. 检查 stdout:stdio 模式下,stdout 只能输出协议消息;普通日志全部改到 stderr。
  2. 使用绝对路径启动:把 nodepython3、脚本路径和工作目录写成绝对路径,先排除 spawn ENOENT
  3. 单独运行 Server:在终端启动一次,确认初始化阶段没有 banner、warning 或第三方库输出。
  4. 检查 JSON-RPC 消息:确认 jsonrpcidmethodparamsresulterror 结构合法,字符串经过正确转义。
  5. 检查协议版本和能力协商:如果连接能启动但工具列表失败,继续核对 initialize 响应、capabilities 和客户端支持的协议版本。

30 秒诊断:先把 stdout 和 stderr 分开

可直接保存使用:MCP -32700 Parse Error 30 秒排查卡stdout JSON-RPC 验证脚本。如果想直接拿一份可运行的校验器和 clean / polluted / invalid JSON-RPC fixtures,可用 xbstack/mcp-stdio-diagnostics

macOS / Linux:

/usr/local/bin/node /absolute/path/server.js \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

python3 - <<'PY'
import json
from pathlib import Path
for index, raw in enumerate(Path('/tmp/mcp-stdout.log').read_text(encoding='utf-8').splitlines(), 1):
    if not raw.strip():
        continue
    try:
        message = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise SystemExit(f'line {index} is not JSON: {exc}: {raw[:160]!r}')
    if message.get('jsonrpc') != '2.0':
        raise SystemExit(f'line {index} is not JSON-RPC 2.0: {raw[:160]!r}')
print('stdout contains only JSON-RPC 2.0 messages')
PY

Windows PowerShell:

& "C:\Program Files\nodejs\node.exe" "C:\absolute\path\server.js" `
  1> "$env:TEMP\mcp-stdout.log" `
  2> "$env:TEMP\mcp-stderr.log"

Get-Content "$env:TEMP\mcp-stdout.log" | ForEach-Object {
  if ($_ -and -not ($_ | Test-Json -ErrorAction SilentlyContinue)) {
    throw "stdout contains a non-JSON line: $_"
  }
}

判断标准只有三个:

  • stdout 出现 banner、普通日志、Warning 或堆栈:先修输出污染;
  • stdout 每行都是 JSON,但缺少 jsonrpc: "2.0":修 JSON-RPC 结构;
  • stdout 为空且进程未退出:Server 很可能只是在等待 initialize,继续检查客户端日志和启动路径。

三类报错不要混在一起

现象优先检查
-32700 Parse errorstdout 污染、消息截断、非法 JSON、编码和换行
Tool list failedServer 是否完成初始化、是否声明 tools capability、tools/list 是否返回合法结构
spawn ENOENT可执行文件路径、脚本路径、PATH、工作目录和文件权限

症状、验证命令和通过标准

症状先做什么通过标准
Server 一启动就报 -32700单独启动并分别捕获 stdout、stderrstdout 中只有完整 JSON-RPC 消息,普通日志只出现在 stderr
Tool list failed先完成 initializenotifications/initialized,再发 tools/list初始化响应包含协商后的协议版本和 tools capability
spawn ENOENT在客户端配置中使用 which node / which python3 得到的绝对路径客户端能启动进程,日志中不再出现找不到可执行文件
连接几秒后断开查看最早出现的 stderr 和客户端日志,而不是只看最后一个 EPIPE找到第一个解析错误、未捕获异常或进程退出原因
只有长文本工具失败检查序列化后的单行 JSON、编码和消息大小每条 stdio 消息以换行分隔,消息内部没有未转义换行

这篇文章只解决 stdio 传输下的 JSON-RPC 解析与本地启动问题。远程部署请看 MCP Streamable HTTP 实战;公网认证请看 MCP OAuth 认证实战;权限与审计请看 MCP 安全最佳实践

用 MCP Inspector 和导入隔离定位隐式输出

如果终端裸跑没有明显日志,但客户端仍提示解析失败,再做两步:

  1. 使用 MCP Inspector 启动同一个 Server,观察 initialize、tools/list 和工具调用过程中是否混入非协议文本。
  2. 把第三方模块逐个延后导入;有些 Python 或 Node.js 依赖会在 import、require 或初始化阶段输出 banner、弃用警告或版本提示。
  3. 不要只替换 printconsole.log,还要检查 sys.stdout.writeprocess.stdout.write 以及第三方日志框架的默认 transport。

旧的 stdio 污染专项页已经合并到本页,后续本地连接、stdout 污染和 -32700 错误统一在这里维护。

实战复核清单

排查 parse error 时,不要先猜模型问题,按下面顺序查:

  • 裸跑 MCP Server,确认 stdout 是否只输出合法 JSON-RPC 消息。
  • 把所有调试日志改到 stderr 或独立日志文件。
  • 检查 Cursor / Claude 启动子进程时的 PATH 是否缺失。
  • 用最小 JSON-RPC 初始化报文手动测试 stdin/stdout。
  • 检查长文本、换行符、大文件 Resource 是否造成消息截断或缓冲区异常。

先用官方规范定义问题边界

MCP 的 stdio 传输边界仍然很明确:Server 从 stdin 读取协议消息,通过 stdout 返回协议消息;诊断日志应写到 stderr,不能把普通应用日志和协议输出混在同一条 stdout 流里。生命周期则必须按协议版本区分2025-06-18 客户端使用 initialize、capabilities 与 notifications/initialized2026-07-28 已改为 stateless core,不再要求这套握手。

排查旧客户端时可对照 2025-06-18 Transports 规范Lifecycle 规范;针对新客户端,应先确认其声明的 MCP-Protocol-Version 并对照 2026-07-28 规范。JSON 解析错误码本身仍可参考 JSON-RPC 2.0 规范

错误现象、所属层级和第一检查点

错误现象所属层级常见原因第一检查点
Unexpected non-JSON linestdio 分帧console.logprint、banner 或依赖警告进入 stdout分别捕获 stdout 与 stderr,找出第一行非 JSON 文本
-32700 Parse errorJSON 解析收到的文本不是合法 JSON,或单条消息被截断、混入控制字符对 stdout 每一行执行 JSON 解析验证
-32600 Invalid RequestJSON-RPC 结构JSON 可以解析,但缺少 jsonrpcmethod 等必要字段对照 JSON-RPC Request / Response 结构检查字段
Tool list failedMCP 生命周期或工具能力初始化未完成、未声明 tools capability、tools/list 响应不合法先验证 initializenotifications/initialized
spawn ENOENT进程启动客户端 PATH 不完整、命令或脚本路径错误使用 node、python 和脚本的绝对路径
HTTP 401、403、415、502Streamable HTTP认证、Content-Type、代理、路由或上游服务异常查 HTTP 请求头、网关日志和服务端日志,不查 stdout

这几类错误不能混在一起。-32700 表示接收方无法把收到的文本解析成 JSON;-32600 表示已经得到 JSON,但它不是合法的 JSON-RPC Request;Tool list failed 则只是客户端界面上的上层症状,根因可能发生在进程启动、初始化、capability 声明或工具列表响应中的任意一层。

为什么 console.log 和 print 会污染 stdio

在普通 HTTP 服务中,控制台日志和 HTTP Response 是两个通道;在 MCP stdio 模式中,stdout 本身就是协议通道。Node.js 的 console.log() 和 Python 的 print() 默认都会写入 stdout,因此一行类似 Database connected 的普通文本会被客户端当成下一条 MCP 消息处理,最终触发非 JSON 行或 Parse error。

污染也可能来自第三方依赖:import / require 阶段的 banner、弃用警告、调试模式和默认 Console Handler 都需要检查。修复原则不是全局劫持 stdout,而是关闭依赖的普通输出,让应用日志显式写入 stderr 或独立日志文件,并让 MCP SDK 保持对 stdout 协议流的控制。

最小复现代码:如何让应用日志显式走 stderr

稳定做法不是全局覆盖 process.stdout.writeconsole.log 或 Python 的 sys.stdout,而是让自己的应用日志从一开始就明确写入 stderr,并关闭第三方依赖的 banner 或 Console Handler。全局劫持可能同时截断 SDK 的合法协议输出,属于最后也不应采用的补丁。

下面的 Node.js 示例只使用 console.error 记录诊断信息,不修改任何全局输出流:

// safe-mcp-server.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// 初始化 Server
const server = new Server(
  {
    name: "safe-demo-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 注册工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
  console.error("收到客户端的 list tools 请求"); // 使用 console.error 打印日志
  return {
    tools: [
      {
        name: "calculate_future_value",
        description: "计算复利未来价值",
        inputSchema: {
          type: "object",
          properties: {
            principal: { type: "number", description: "本金" },
            rate: { type: "number", description: "年化收益率" },
            years: { type: "number", description: "投资年限" },
          },
          required: ["principal", "rate", "years"],
        },
      },
    ],
  };
});

// 注册工具执行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  console.error(`开始执行工具 ${name},参数为:`, args);

  if (name === "calculate_future_value") {
    const { principal, rate, years } = args;
    const result = principal * Math.pow(1 + rate, years);
    return {
      content: [
        {
          type: "text",
          text: `经过 ${years} 年的复利增值,本金 ${principal} 将增长至 ${result.toFixed(2)}`,
        },
      ],
    };
  }

  throw new Error(`未知的工具方法: ${name}`);
});

// 启动服务,绑定到 stdio transport
async function run() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server 已成功通过安全 Stdio 通道启动并监听");
}

run().catch((error) => {
  console.error("Server 发生严重崩溃:", error);
  process.exit(1);
});

Python 端不要在 SDK 初始化后执行 sys.stdout = sys.stderr。FastMCP 的 stdio transport 需要保留合法响应写入 stdout;更稳的做法是让应用日志显式走 logging.StreamHandler(sys.stderr),并关闭第三方库的 Console Handler。

# safe_mcp_server.py
import logging
import sys
from mcp.server.fastmcp import FastMCP

logger = logging.getLogger("safe-mcp-server")
logger.setLevel(logging.INFO)
logger.handlers.clear()
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
logger.propagate = False

mcp = FastMCP("Safe Python MCP Server")

@mcp.tool()
def calculate_dca_returns(monthly_investment: float, annual_rate: float, years: int) -> str:
    logger.info(
        "calculate_dca_returns monthly=%s rate=%s years=%s",
        monthly_investment,
        annual_rate,
        years,
    )
    monthly_rate = annual_rate / 12
    months = years * 12
    total_value = 0.0
    for _ in range(months):
        total_value = (total_value + monthly_investment) * (1 + monthly_rate)
    return f"期末总资产: {total_value:.2f}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

如果某个依赖在 import 阶段强制向 stdout 打印,优先关闭它的 banner 或把该依赖隔离到单独子进程。不要覆盖 process.stdout.write 或 Python 的全局 stdout 来“兜底”,因为这也可能截断 SDK 的合法协议输出。

可复制的 stdout / stderr 验证命令

先把两个输出流分别保存。Server 在没有收到 initialize 前保持等待且 stdout 为空,属于正常现象。

# Node.js
/usr/local/bin/node /absolute/path/server.js \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

# Python
/usr/bin/python3 /absolute/path/server.py \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

然后验证 stdout 中每一个非空行是否都是合法 JSON:

# validate_mcp_stdout.py
import json
from pathlib import Path

path = Path("/tmp/mcp-stdout.log")
for line_no, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
    if not raw.strip():
        continue
    try:
        message = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"line {line_no} is not JSON: {exc}: {raw[:160]!r}")
    if message.get("jsonrpc") != "2.0":
        raise SystemExit(f"line {line_no} is not JSON-RPC 2.0: {raw[:160]!r}")

print("stdout contains only JSON-RPC 2.0 messages")

执行:

python3 validate_mcp_stdout.py

如果第一条失败内容是 banner、数据库连接提示或 DeprecationWarning,就先处理 stdout 污染;如果所有行都能解析,再继续检查生命周期、capabilities 和工具响应结构。

排查流程:Claude 和 Cursor 内部是如何定位与抓取这些底层报错的

在 IDE 客户端如 Claude Desktop 或 Cursor 连接失效时,通过终端裸跑、捕获 stderr 流以及查看本地客户端日志,是物理排障的唯一路径。在大模型应用生态中,客户端如 Claude Desktop 和 Cursor 在集成 MCP 时,由于运行在图形界面后台,其底层的网络与管道交互对用户来说是一个彻底的黑盒。当工具列表显示失败时,很多开发者只能在界面上看到红色的警告按钮,而无法直接查看到错误的源头。为了撕开这个黑盒,我总结了一套在本地环境对客户端进行全方位物理审计与日志抓取的标准排障流程。

第一步,终端孤立调试法。 不要急于在配置文件中添加你的 Server,而是先在你的终端中进行本地隔离运行。打开你的 iTerm 或者是终端,执行你的启动命令。正常情况下,服务正在等待客户端的 initialize 握手请求,控制台应该没有普通文本输出并保持运行。如果你的程序一启动就在控制台上打印了任何初始化文字,请立即定位到输出该文字的代码行,将其删除或重定向到 stderr。

第二步,按 MCP 生命周期完成最小握手测试。 不要一上来就发送 tools/list。初始化必须是客户端与 Server 的第一次交互;Server 返回协议版本和 capabilities 后,客户端再发送 notifications/initialized,随后才能进入正常操作。

先发送初始化请求:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-debug-client","version":"1.0.0"}}}

确认 Server 返回合法的 result.protocolVersionserverInfocapabilities 后,再依次发送:

{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

每一条都必须是独立的单行 JSON-RPC 消息。健康的 Server 会在初始化完成后返回合法工具列表;如果初始化阶段就混入 banner、warning 或普通日志,应先解决 stdout 污染,而不是继续调业务工具。实际调试优先使用 MCP Inspector,因为它会正确处理生命周期和消息顺序。

第三步,全面检索客户端本地日志文件。 如果终端测试一切顺利,但挂载到客户端后仍然报错,那么就必须去检索客户端写在本地磁盘上的运行日志。

对于 Claude Desktop: 在 macOS 上,打开终端,执行以下命令来查看日志: cat ~/Library/Logs/Claude/mcp.log 在 Windows 上,该日志通常保存在: %APPDATA%\Claude\logs\mcp.log 在 Linux 上,该日志文件通常保存在: ~/.config/Claude/logs/mcp.log 在这个日志文件中,Claude 会详细记录每一次尝试启动 MCP 进程的命令行参数,以及子进程所输出的每一行内容。如果子进程输出的内容破坏了 JSON-RPC 规范,日志中会抛出诸如 Unexpected non-JSON line 的明确物理警报。

对于 Cursor: Cursor 是基于 Electron 架构开发的,这意味着它的内部运行着一个 Chromium 浏览器实例。我们完全可以像调试网页一样来调试 Cursor 的后台通信。 打开 Cursor,点击顶部菜单的 Help,找到 Toggle Developer Tools。这会弹出一个 Chrome DevTools 面板。切换到 Console 选项卡。当你在 Cursor 选项中刷新 MCP Server 时,所有的管道读写报错、进程异常退出日志,都会以红色的 Error 形式打印在控制台里。你可以在控制台中过滤关键字 mcp,查看是否有 child process exited with code 或 stdio connection closed 的报错堆栈。

此外,Cursor 的扩展主机进程也会记录日志。在 macOS 上,日志路径通常为: ~/Library/Application Support/Cursor/logs 在 Windows 上,该日志路径为: %APPDATA%\Cursor\logs 你可以使用终端的查找工具在这些日志目录下搜索最新的 log 文件,里面通常会包含 Cursor 后台子进程启动时的 stdio 捕获数据。

第四步,使用协议感知工具抓取消息。 不要直接把 tee 插在 MCP Server 的 stdout 后面。普通 Shell 管道不了解 MCP 生命周期、取消请求和双向消息边界,错误的代理脚本还可能因为额外输出再次污染 stdout。优先使用 MCP Inspector;如果确实要做字节级抓取,代理进程必须满足三个条件:

  1. stdin 和 stdout 原样双向转发,不修改、不缓冲合并消息。
  2. 所有诊断信息只写 stderr 或独立文件。
  3. 先在自动测试中验证代理前后的每条消息仍是合法单行 JSON-RPC。

更简单的做法是让 Server 的业务日志写入独立文件,同时保存客户端的 MCP 日志。把两边时间戳对齐,通常已经足以定位第一个污染字节或最早的进程异常。

修复方案:按根因处理,不做全局 stdout 劫持

  1. 先保留第一现场:分别保存 stdout、stderr 和客户端日志,按时间戳找到最早的异常;EPIPE 往往只是前序断连后的连锁错误。
  2. 清理日志通道:业务日志使用 console.errorlogging.StreamHandler(sys.stderr) 或文件 transport;关闭第三方依赖的 banner 和默认 Console Handler。
  3. 让 SDK 负责协议输出:不要自行拼接、批量合并或全局重定向 MCP 消息;保持 UTF-8、单行 JSON-RPC 和换行分隔。
  4. 自动验证生命周期:测试必须依次覆盖 initializenotifications/initializedtools/list,并断言响应包含协商后的 protocolVersion、serverInfo 和 tools capability。
  5. 固定启动环境:客户端配置使用绝对命令、绝对脚本路径和明确的工作目录;依赖 nvm、pyenv 或虚拟环境时,显式配置 PATH 和必要环境变量。

这套处理的目标不是承诺“永不出现 Parse error”,而是把错误稳定地归类到进程启动、stdio 分帧、JSON 解析、JSON-RPC 结构、MCP 生命周期或工具执行六个层级,使每次失败都有可复现证据。


常见坑 / 常见报错 (Error Logs)

理解 JSON-RPC 标准错误代码及其在 stderr 中的真实堆栈信息,能帮助我们快速锁定物理故障节点。这里列出了最常遇到的五种底层报错文本及其触发的物理根因。

错误码 (Code)错误消息 (Message)协议定义 (Specification)物理表现与常见根因排障物理操作
-32700Parse error解析错误:服务端接收到无效的 JSON 报文stdout 被 console.log、print 或第三方依赖库的 banner/警告信息污染,或者消息编码、分帧与换行不合法删除普通 stdout 输出,让应用日志显式写 stderr;使用 SDK 负责协议序列化,不覆盖全局 stdout
-32600Invalid Request无效请求:发送的 JSON 结构不符合 JSON-RPC 2.0 规范遗漏了 jsonrpc: “2.0” 版本标识、缺失了 method 字段,或在 response 中同时包含 result 又包含 error使用 Schema validator 校验消息格式,在 payload 序列化前做白名单过滤
-32601Method not found找不到方法:该方法在服务端未声明或不支持客户端调用的 tool 名称拼写错误,或者 server 初始化能力集时没有声明 tools capabilities验证客户端与服务端工具列表命名映射,检查 ListToolsRequestSchema 返回的 schema
-32602Invalid params无效参数:方法调用的参数结构与声明不匹配客户端传入的 arguments 类型(如 string vs number)或必填项与 tool 定义 of inputSchema 不符严格比对 inputSchema 中的 properties 定义,在 schema 验证失败时显式向 stderr 记录 payload 堆栈
-32603Internal error内部错误:MCP 服务端执行工具时内部崩溃业务代码抛出未捕获异常(如 DB 连接失败、文件读写权限不足),导致 Node.js/Python 进程抛错在 handler 顶层使用 try…catch 物理拦截,将堆栈用 console.error 输出,返回合规的 error payload
  1. 客户端输出 Unexpected non-JSON line 错误:
[json-rpc] Unexpected non-JSON line: "DB Connection established..."
[json-rpc] Unexpected non-JSON line: "DeprecationWarning: Big-endian support is deprecated"

这个报错的物理根因是:你的 Server 在向 stdout 写入合法消息之前,打印了诸如数据库连接成功、或者底层的第三方库输出了 Deprecation 警告。客户端的解析器期待的是一个 JSON,结果读到了这段纯文本,解析器瞬间当场崩溃。

  1. JSON-RPC -32700 Parse error 错误:
{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

这个错误是由客户端(如 Claude)返回给你的 Server,或者由 Server 返回给客户端的。这说明在流传输过程中,某一方接收到了数据,但是使用 JSON.parse 尝试解析该数据时失败了。这通常是因为 JSON 结构被截断(缓冲区未完全 Flush),或者传输的内容中夹杂了无法识别的控制字符、乱码、非 UTF-8 字符。

  1. IDE 客户端提示 spawn ENOENT 错误:
Failed to run command: spawn node ENOENT
Failed to run command: spawn python3 ENOENT

这个报错说明 Cursor 或 Claude 尝试在后台启动你的 MCP 进程,但是由于它的环境变量 PATH 中找不到 node 或 python3 的可执行文件,导致进程根本没有跑起来。IDE 抛出这个物理异常,通常伴随着连接状态直接变为红色。

  1. 传输管道破裂 write EPIPE 错误:
Error: write EPIPE at AfterWriteReq.oncomplete (node:internal/stream_base_commons:90:16)

这是当你的 Node.js 进程尝试往 process.stdout 写入消息时,发现另一端的读取进程(Claude / Cursor)已经退出了,或者因为之前发生了 parse error 主动关闭了标准输入管道。这是一个典型的连锁反应错误,说明根源在更早的通信异常里。

  1. 消息格式不合规导致 -32600 Invalid Request:
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request: missing jsonrpc version"}, "id": 1}

这说明收到的消息能够成功被解析为 JSON,但是 JSON 对象内部缺失了关键的协议标识。比如你拼写错了 jsonrpc(写成了 json-rpc),或者在 Request 消息里漏掉了 method 字段,或者 Response 里既有 result 又有 error 字段。


Stdio 与 Streamable HTTP:排错边界不同

当前 MCP 主要使用本地 stdio 和远程 Streamable HTTP 两类传输。本文的 stdout 污染问题只直接影响 stdio;远程 HTTP 服务更常见的是认证、Content-Type、会话、代理和网络层错误。

排查维度StdioStreamable HTTP
连接方式Host 启动本地子进程,通过 stdin/stdout 通信Client 通过 HTTP POST 发送消息,可选 SSE 流式返回
最敏感问题stdout 混入普通日志、PATH、工作目录、进程退出URL、认证、请求头、反向代理、会话和超时
日志位置stderr 或独立日志文件应用日志、网关日志和请求追踪
常见报错-32700spawn ENOENTEPIPE401、403、404、415、502、超时
适用场景本地桌面客户端和开发工具远程共享、团队服务和云端接入

如果本地 stdio Server 已经稳定,但目标是跨机器或多用户共享,不要继续围绕 stdout 打补丁,应转到 MCP Streamable HTTP 部署MCP OAuth 认证 的问题域。


常见问题解答

针对物理层与协议层典型场景,这里整理了日常开发中最容易踩坑的几个边缘崩溃场景。

为什么我在本地的终端中单独运行 MCP Server 没有任何报错,但是在 Cursor 里面连接就会一直提示 Tool list failed?

这主要是因为执行环境的环境变量差异。你在终端里跑的时候,使用的是你当前 Shell 中完整的环境变量,比如你的 Node.js 是通过 nvm 安装的,你的 PATH 变量里包含了完整的 node 可执行文件路径。而 Cursor 作为桌面客户端,其在后台 fork 子进程时的 PATH 变量可能是系统默认的极简 PATH,导致它找不到你的 node 可执行文件,从而抛出 spawn ENOENT。另外,有些 Server 在没有接收到标准输入时不会打印任何错误,但一被 Cursor 握手,就会由于收到错误的报文而在初始化阶段崩溃。你应该首先在 Cursor 配置文件中将可执行命令的路径全部写成绝对路径。

如果第三方 Node.js 或 Python 模块在初始化时一定会向 stdout 输出版权声明或更新警告,应该怎么办?

先查该依赖是否支持关闭 banner、静默模式或自定义 Logger,并把它的日志 Handler 明确指向 stderr。不要覆盖 process.stdout.writeconsole.log 或执行 sys.stdout = sys.stderr,因为 MCP SDK 也需要使用 stdout 发送合法协议消息,全局重定向可能让 Server 完全失去响应通道。若依赖无法关闭输出,应把它隔离到另一个子进程,由 MCP Server 通过受控 IPC 调用并只接收结构化结果。

为什么我已经将所有的日志都用 console.error 输出了,但客户端仍然报 -32700 Parse error 错误?

这通常是因为你的消息内容被截断了,或者是你的 JSON-RPC 消息中包含了不合法的字符。比如,如果你输出的 JSON 字符串里有未经过转义的换行符(如直接把一段包含换行的长文本作为字符串放进了 params 中),stdio transport 框架在逐行读取时,会把这个换行符误认为是消息的分隔符,从而把一条完整的消息拆分成了两行,解析第一行时就会因为 JSON 结构不完整而报 Parse error。你应该确保所有放入 JSON 消息的文本都经过了正确的转义(例如使用 JSON.stringify 会自动处理换行符的转义)。

我该如何安全地记录 MCP Server 在生产环境中的运行日志以便随时排查业务 Bug?

最优雅且安全的做法是,使用诸如 Winston (Node.js) 或 Loguru (Python) 这样的专业日志系统,配置一个 File Transport,将所有的日志追加写入到本地物理磁盘的指定日志文件中。这样可以实现日志与协议数据的完全物理隔离。千万不要图一时省事而将日志直接吐给控制台。另外,你也可以直接将日志输出到标准错误流 stderr,因为像 Claude Desktop 和 Cursor 都会捕获子进程的 stderr 并记录到它们内部的日志文件中,但由于客户端日志容易被循环覆盖,还是写到本地独立日志文件最稳妥。

继续阅读

探索更多关于 Model Context Protocol 的高阶实战技巧与架构模式,将助你构建更具韧性的本地 AI 智能代理网络。

专题入口 / MCP Hub

继续按 MCP 生产部署路径读,而不是堆 guide / tutorial

MCP 内容统一按协议理解、本地 Server、远程部署、OAuth、安全治理、stdio/JSON-RPC 排障和工具对比来承接,避免站内关键词互相抢。

下一步阅读

返回专题入口 →
小白

小白

Full-Stack AI Engineer

小白,全栈 AI 工程师,持续构建生产级 Agent 系统、产品工具与独立软件资产。

了解小白与 XBSTACK →

喜欢这篇文章?
加入小白实验室的周刊

每期只整理 AI 工程变化、真实故障、可复现实验、值得尝试的工具和 XBSTACK 新资产,不做泛新闻汇总,也不为周更凑数。

Comments

参与讨论

问题、验证与勘误

登录后可发表评论。所有新评论先进入审核;审核期间仅评论者本人和管理员可见,通过后才公开。

登录评论 审核后公开
正在加载评论区…