ClaudeMap

·MCP 服务器

MCP 协议级调试——用 MCP Inspector 驱动服务器、抓 JSON-RPC 流量、stdio 与 HTTP 调试差异、五个真实 bug 复现,外加把可调试性设计进服务器的预防性观测与 2026-07-28 规范升级期排障。

MCP 服务器调试:Inspector、抓包与失败模式(2026)

MCP 调试是协议级调试而非应用级调试:一个 MCP 服务器是一个常驻进程,通过 stdio(标准输入输出)或 Streamable HTTP 承载 JSON-RPC 2.0 消息——故障几乎从不在你以为的地方出现。一个服务器可能干净启动、声明了自己的工具,却返回不了任何有用的东西,因为某个环境变量为空、因为 stdout 泄漏进了协议流、或者因为 OAuth 鉴权在远端调用链里悄悄失败。本文基于 MCP 2025-11-25 规范版本,整理:MCP Inspector 抓 JSON-RPC 流、五类真实 bug 复现、stdio vs HTTP 调试差异,以及从本地到生产环境的迁移清单。

TL;DR

  • MCP 服务器是 JSON-RPC 进程;调试的关键是区分协议层(schema/握手)与应用层(handler 逻辑)
  • stdio 传输上 stdout 是协议通道——console.log 会污染协议流,必须走 stderr
  • 远端调试核心是 Streamable HTTP 抓包:看 Mcp-Session-Idtools/call 响应
  • 五类最常见 bug:schema 不一致 / 协议版本错配 / OAuth 过期 / 工具 handler 异常 / 环境变量未透传
  • 本文覆盖:Inspector 抓流、五个 bug 复现、抓包工具、stdio/HTTP 调试差异、生产迁移清单

从 Inspector 开始

对 MCP 而言,最好的单一调试工具是官方的 MCP Inspector——一个浏览器 UI,直接和服务器说协议,中间没有任何 Host。你可以对任何服务器命令启动它:

npx @modelcontextprotocol/inspector node path/to/server.js

Inspector 会打开一个本地网页。在那里你可以:

  • 查看连接握手过程和服务器声明的能力。
  • 列出服务器的工具、资源和提示模板,以及它们完整的 JSON schema。
  • 用手敲的参数调用一个工具,检查原始响应。
  • 逐条查看 Inspector 和服务器之间的 JSON-RPC 流量。

这是反馈最快的循环。如果一个工具在 Inspector 里能用、在 Claude Desktop 里不行,问题在 Host 配置或环境,不在你的服务器。如果在 Inspector 里也失败,你就可以直接迭代服务器代码,不用每次改动都等应用完整重启。

Inspector 适用于任何 stdio 服务器。对于 HTTP 服务器,把它的 URL 传给 Inspector 即可。它也是在你决定集成某个服务器之前,先摸清它的好办法——你能在写任何配置之前就了解工具名和参数形状。

读懂 Host 已经在写的日志

当服务器在某个 Host 里运行时,Host 会捕获它的输出。值得知道的两份日志:

Claude Desktop 在 macOS 上按服务器各写一个日志文件,路径是 ~/Library/Logs/Claude/mcp-server-<名字>.log。Windows 上对应的是 %USERPROFILE%\AppData\Roaming\Claude\logs\。你的服务器写到 stdout 或 stderr 的任何东西都会落到这里,和 Host 自己的协议消息交织在一起。在复现 bug 时实时跟踪这个文件:

tail -n 100 -f ~/Library/Logs/Claude/mcp-server-filesystem.log

Claude Code 通过 /mcp 命令交互式地展示服务器状态——列出每个已配置的服务器、它的连接状态、它注册了哪些工具。要看更深的细节,它会和其他会话输出一起写日志;通常 /mcp 面板就足够判断一个服务器到底有没有连上。

一个常见做法是:开发期间给自己的服务器加结构化日志,发布时再剥掉。把每行日志写成一个带时间戳和级别的单一 JSON 对象——这让交织的日志比自由格式文本好扫得多。

协议级抓包:怎么监看 JSON-RPC 流量

MCP 调试的真正杠杆是看到原始 JSON-RPC 帧——而不是只看「服务器返回了空」这种症状。三个分层方法:

方法 1:MCP Inspector(最常用)。Inspector 自带 JSON-RPC 面板:在 tools/list 标签页能看到服务器注册的完整 schema;在 tools/call 标签页能手动构造请求并看到原始响应帧。Inspector 默认走 stdio transport,直接 npx @modelcontextprotocol/inspector node ./server.js 启动。

方法 2:stdio 上的 socat 分叉。当你想看 Host 实际发给服务器的字节流时,把 stdio 中间夹一层 socat

socat -v TCP-LISTEN:7000,fork,reuseaddr EXEC:"node ./server.js"

然后让 Host 连接到 localhost:7000 而不是直接跑 node ./server.js-v 让 socat 把所有进出字节打到 stderr,这才是看到协议流的真实方式。比 Inspector 麻烦,但能捕获到 Host 自加的握手行为(Claude Desktop / Claude Code 会在 initialize 之后多发一个 notifications/initialized 帧——漏写这个就是 OAuth 失败的最常见原因之一)。

方法 3:HTTP 端的 mcp-proxy。对 Streamable HTTP 传输,在 server 与 client 之间跑一个转发代理(mcp-proxy),代理同时记录请求/响应体到磁盘。POSTGET 两个 endpoint 都要看:POST 是 client → server 的 RPC,GET 是 server → client 的 SSE 流(用来推送 notifications/resources/updated 之类的事件)。

三个关键握手必须看:

| 握手 | 期望 | 失败症状 | |---|---|---| | initialize | 返回 protocolVersion: "2025-11-25"capabilitiestools | 客户端报 "Unsupported protocol version" | | notifications/initialized | 客户端发,服务器不应回任何东西 | OAuth 上下文丢失、后续 call 报 "Missing scope" | | tools/list | 返回 tools: [] 数组,每项含 name/description/inputSchema | 客户端显示「无工具」但服务器日志显示已注册 |

initialize 响应里的 protocolVersion 错配是 2026 年最常见的握手失败——客户端升了,服务器没升(或反之)。UnsupportedProtocolVersionErrordata.supported 字段列了服务器实际支持的版本,对照升级即可(参考 MCP 规范 transports)。

stdio vs Streamable HTTP:调试方法的根本差异

把 stdio 服务器搬到远端,几乎所有调试手段都要重写。差异不只是「端口 vs 管道」,而是信任边界与会话状态的转移。

| 维度 | stdio(本地) | Streamable HTTP(远端) | |---|---|---| | 鉴权 | 通常无(OS 进程边界即信任边界) | 必须 OAuth 2.1(参考 authorization 规范RFC 8707 Resource Indicators) | | 会话状态 | 进程内变量;进程死即丢 | 用 Mcp-Session-Id header 跨请求维护 | | 调试工具 | Inspector + socat + console.error | mcp-proxy + 浏览器 DevTools Network + OAuth 调试页 | | 错误可见性 | stderr 即时 | HTTP 状态码 + JSON-RPC error 对象;401 = 重新 OAuth | | 性能调优关注点 | 进程启动延迟(影响冷启动 UX) | 连接池、TLS 握手、首字节延迟 |

stdio 转 HTTP 必踩的 3 个坑

  1. OAuth scope 漂移。stdio 服务器一般用环境变量传 token,搬到 HTTP 后必须走 OAuth;「我之前明明能调用」的常见原因是 token scope 不含当前 tool 需要的资源。
  2. Mcp-Session-Id 复用导致状态串台。多窗口开同一个远端服务器时,如果不正确隔离 session,A 窗口的 resources/read 缓存会被 B 窗口的 resources/subscribe 触发更新。
  3. CORS 与 Origin 校验。浏览器端 Host(如 Claude.ai 网页版)连远端 MCP server 时,服务端必须正确处理 Origin header(security best practices 详述)——否则浏览器拒绝连。

五个真实 bug 复现(带最小代码与修法)

下面 5 个 bug 占真实调试会话的 80% 时间。每个给「现象 → 最小复现 → 根因 → 修法」。

Bug A:Zod schema 与 JSON Schema 不一致.optional() 不会自动传到 JSON Schema,导致模型字段被静默丢弃。

// Bug:模型发的 `{ name: "x" }` 进来变成 `{}`
const Schema = z.object({ name: z.string().optional() });

// 修法:显式声明 .nullish() 或在转换时打补丁
const Schema = z.object({ name: z.string().nullish() });
const jsonSchema = zodToJsonSchema(Schema);
// 验证:name 必须在 schema 里,缺了就报 "Missing required argument"

Bug B:stdio 上 console.log 漏一个字节。Claude Desktop 间歇报 "Unexpected token" 或 "Message parsing failed",删掉 log 就好。

// Bug:污染协议流
console.log("debug:", args);
// 修法:全部走 stderr,加 JSON 包装便于解析
console.error(JSON.stringify({ level: "debug", msg: "called", args }));

Bug C:handler 抛未捕获异常。模型收到 "Tool failed" 但无细节——因为 MCP 协议默认不序列化抛出的异常。修法是用 isError: true 的结构化返回:

// Bug:模型只看到 "Tool failed"
async function handler(args) { throw new Error("DB down"); }

// 修法:MCP 标准错误返回
async function handler(args) {
  try {
    return await realHandler(args);
  } catch (e) {
    return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
  }
}

Bug D:环境变量未透传到子进程。stdio 服务器是 Host 的子进程,process.env 默认不包含 Host 自身的 env(Host 出于安全会过滤敏感变量)。最常见:「我本地能跑,Host 一调就 401」。

// Bug:Host 启动时没传 GITHUB_TOKEN
const token = process.env.GITHUB_TOKEN; // undefined
// 修法 1:在 Host 配置里显式白名单——见 MCP 规范的 [servers[].env 字段定义](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server#environment-variables)
// 修法 2:让用户用 OAuth 走授权流(更安全)

Bug E:UnsupportedProtocolVersionError。客户端期望 2025-11-25,服务器还停在 2024-11-05。看 data.supported

{"code": -32000, "message": "Unsupported protocol version", "data": {"supported": ["2024-11-05"], "attempted": "2025-11-25"}}

修法是升级服务器 SDK 到对应版本,或在 initialize handler 里显式回退到旧协议(最后手段)。

占据大多数 bug 的失败模式

当你排除了「服务器根本没跑起来」之后,几乎所有剩余的 MCP bug 都能归到下面这几类里。

服务器启动即崩

Host 拉起进程,进程立刻退出,服务器显示为失败。原因几乎总能在日志里看到:一个未处理的异常、缺失的依赖、或者入口文件里的语法错误。修复办法是在终端里运行你配置里那条完全相同的命令:

node /absolute/path/to/server.js

如果它在这里非零退出,在 Host 里也会非零退出。先在独立环境修好。

连接打开了但没有工具出现

服务器活着,但 Host 显示零个工具。两个常见嫌疑:服务器从没注册过工具(你忘了在 server.connect(...) 之前调用 server.tool(...)),或者服务器说的是 Host 听不懂的协议版本。Inspector 会立刻告诉你答案——如果它列出了工具,注册代码就是好的,问题在版本协商。如果你怀疑不匹配,钉死 SDK 版本并查一下 Host 支持的协议版本。

工具出现了但什么也不返回

模型决定调用某个工具,却拿到空响应或错误响应。这时工具处理器本身是头号嫌疑。在每个处理器开头打印收到的参数——你会惊讶地发现,schema 校验通过了,但参数形状并不是处理器假设的那个,这种情况有多常见。把错误以规范的 MCP content 形式返回,而不是抛异常:

server.tool("get_user", { id: z.string() }, async ({ id }) => {
  const user = await db.findUser(id);
  if (!user) {
    return {
      isError: true,
      content: [{ type: "text", text: `No user with id ${id}` }],
    };
  }
  return { content: [{ type: "text", text: JSON.stringify(user) }] };
});

返回 isError: true 以结构化方式告诉模型这次调用失败了,让模型能优雅恢复。相比之下,抛出未处理异常通常只会以一句泛泛的「工具失败」呈现给模型,没有任何细节。

stderr 泄漏进协议

这是最微妙的一个。在 stdio 传输上,stdout 是协议通道,stderr 是日志通道。如果你的服务器往 console.log(写到 stdout)而不是 console.error 写了任何东西,这些字节就会污染 JSON-RPC 流,Host 看到的是格式错乱的消息。症状是间歇性的协议错误,而你一去掉日志它就消失。经验法则:在 stdio 服务器里,每条诊断都走 console.error,绝不走 console.log。HTTP 服务器则没有这个约束。

权限和提示

某些 Host 会在工具第一次被调用时弹出权限提示。如果你拒绝了,或者它在没法展示的上下文里触发(无头 CI 运行、后台 Agent),工具就什么也不做。在 Claude Code 里,检查你当前会话的权限模式,必要时显式授予工具。在自动化环境里,在配置里预先批准工具,这样就不需要交互式提示。

一套行之有效的调试流程

当服务器行为异常时,从最简单的可复现场景向外排查:

  1. 独立运行服务器命令。 把配置里的 commandargs 完整粘贴到终端。它能启动并保活吗?
  2. 用 Inspector 驱动它。 连上 Inspector,用 Host 当初用的同样参数调用失败的工具。返回对吗?
  3. 查看 Host 日志。 在 Host 里复现故障,读 mcp-server-<名字>.log。服务器打印了什么?
  4. 核实环境。 在服务器启动期间打印 process.env,跟你预期的对比。缺失 token 是「终端里能跑、Host 里失败」最常见的原因。
  5. 收窄协议。 如果你怀疑是版本或能力不匹配,把 Inspector 报告的与 Host 看到的做对比。

这个顺序很重要。如果服务器根本没启动,却跳过这一步直接去读 Host 日志,是浪费时间。

生产环境注意事项

把 MCP 服务器从笔记本搬到共享环境,调试故事会变样。

传输从 stdio 换成 HTTP。 生产里你通常把服务器作为远端 HTTP+SSE 或 streamable-HTTP 端点运行,而不是子进程。这意味着你失去了按进程的日志文件,却多了网络故障、超时和认证要操心。从第一天起就加上健康检查和结构化日志。

并发。 本地 stdio 服务器服务一个用户;远端 HTTP 服务器可能服务很多。确保你的处理器是无状态的,或者任何共享状态都受到保护。数据库连接池、内存缓存、限流器,都得在并发访问下安全。

认证和授权。 远端服务器需要先认证 Host 才能信任它的请求,它暴露的工具也得尊重按用户的权限。别发布一个让任何调用方都能跑任何工具的远端 MCP 服务器——这相当于在生产环境里把数据库写用户留在配置里。

可观测性。 规模化时,记录每一次工具调用:工具名、参数(脱敏后的)、延迟、结果。这是当服务器被许多会话共享时,唯一能回答「今天的 Agent 怎么这么慢?」的办法。

版本管理。 显式钉死服务器和 SDK 版本。MCP 还在成熟中,一个传递依赖的升级可能悄悄改变协议行为。把 @modelcontextprotocol/sdk 的版本当成数据库驱动版本一样对待——有意识地升级,而不是不小心升级。

预防性可观测性:把「可调试」设计进服务器

前面所有章节都在讲「出了事怎么查」;更省钱的思路是让服务器在被查之前就把证据摆好。四个低成本习惯:

结构化日志带请求 ID。 每个 JSON-RPC 请求进来时生成 requestId,之后所有日志行都带上它——工具执行、数据库查询、上游调用。出问题时用户只要给你一个时间点,你按 ID 一过滤就是完整链路。纯文本日志在排查时的价值只有结构化日志的十分之一。

错误以 isError content 返回,同时留下细节日志。 模型看到的是简短的 isError: true 消息(这是协议约定),但你自己的日志里要记全栈与入参。只做后者,模型反复重试瞎猜;只做前者,你无从下手——两个都要。

启动时自检并打印能力清单。 server 启动时把「我注册了哪些工具、参数 schema 版本、协议版本、连接的上游」打成一行结构化日志。一半以上的「工具出现了但不干活」问题,在启动日志里就能看出是 schema 版本不符或上游没连上。

健康检查与 Inspector 冒烟脚本。 给远程 server 加 /healthz(进程活着)之外,再加一个 tools/list 探针(协议活着);把 MCP Inspector 的连接测试写进 CI——部署后自动跑一次列工具 + 调用只读工具,失败即回滚。协议级回归在部署时暴露,成本最低。

规范升级期的高发故障:2026-07-28 迁移排障

规范大版本升级期(如 2026-07-28)会产生一类新故障:代码没改、环境没改,只是某天对端升级了。四个高发症状(传输层细节见 传输层全解):

症状 1:UnsupportedProtocolVersionError 新客户端对旧 server(或反之):新版把协议版本放进每个请求的 _meta,版本不匹配直接报错。修法:升级两端 SDK;server 侧实现 server/discover 探测端点,让客户端能先探测再选版本。

症状 2:握手代码突然多余或有害。 旧版手写的 initialize / notifications/initialized 序列在新协议下已移除——残留代码可能对不上新时序。修法:删手写握手,全部交给 SDK。

症状 3:依赖 Mcp-Session-Id 的多步流程断链。 会话头被移除(SEP-2567),跨调用状态不再由传输层托管。修法:业务状态改为 server 签发显式句柄、作为工具参数传递。

症状 4:SSE 断线后客户端卡死等重投。 新版移除了 Last-Event-ID 续传——断线即在途请求作废。修法:客户端实现「断线即重发新请求」;不要等待永远不会再来的补投。

升级期的通用姿势:先在 staging 跑一遍 Inspector 全链路(列工具 + 调只读工具 + 一次带 OAuth 的调用),再动生产。

常见问题

什么是 MCP Inspector,什么时候该用它?

MCP Inspector 是一个官方浏览器 UI,中间没有 Host,直接和服务器说 MCP 协议。把它作为调试的第一步:对服务器命令启动它,你就能列出工具、用带类型的参数调用它们、查看原始的 JSON-RPC 流量。如果某个工具在 Inspector 里能用、在 Host 里失败,问题在 Host 配置或环境,不在你的服务器。

为什么我的 MCP 工具出现了,却什么有用信息也不返回?

服务器连上了也注册了工具,但工具处理器本身失败了。在每个处理器开头打印收到的参数,确认你代码假设的形状和模型实际发送的一致,并把错误以 isError: true 的结构化 MCP content 返回,而不是抛异常。未处理的异常通常只会以一句没有细节的泛泛失败呈现给模型。

Claude Desktop 把 MCP 服务器日志写在哪里?

macOS 上,Claude Desktop 按服务器各写一个日志文件,路径是 ~/Library/Logs/Claude/mcp-server-<名字>.log,里面包含服务器写到 stdout 和 stderr 的所有内容,交织着 Host 自己的协议消息。Windows 上对应的是 %USERPROFILE%\AppData\Roaming\Claude\logs\。复现 bug 时实时跟踪这个文件。

我能用 console.log 调试 stdio MCP 服务器吗?

不能——在 stdio 传输上,stdout 是 JSON-RPC 协议通道。你写到 console.log 的任何东西都会污染协议流,导致间歇性的「消息格式错乱」错误。把每条诊断都改走 console.error(stderr),那才是指定的日志通道。HTTP 服务器则不受这个约束。

远端 MCP 服务器的超时一般设多少合适?

tools/call 的端到端超时按场景分三档:纯查询类(数据库读 / 搜索)5–10s 足够;写操作(含事务提交)30s;批量同步或外部触发链可达 60–120s,但超过 30s 强烈建议用 SSE 流式返回进度,而不是让 client 等。MCP 规范的 Streamable HTTP transport 默认没有硬超时,由 server 框架决定;用 FastAPI / Express 时显式配 timeout=30 防止 client 永远挂着。

Inspector 看到的能跑,但 Claude Desktop 不行,怎么定位是协议错还是配置错?

分两步:(1) 用 claude --mcp-debugclaude desktop --enable-mcp-logs 重启 Host,让 Host 把握手字节流打到日志;(2) 对比 Inspector 跑的 initialize 响应和 Host 收到的 initialize 响应——如果两者一致但 Host 后续 call 失败,问题在 Host 配置(OAuth scope、allowed paths、permissions);如果两者不一致,问题在协议版本不匹配或 server 没正确返回 capabilities。

官方参考资料

本文基于截至 2026 年 8 月的 MCP 2025-11-25 规范版本,相关 API 可能演进;建议每 6 个月查一次规范版本号。