第 8 章 / 共 12 章
它没反应的时候:调试与故障定位
8.1 调试 MCP server 的难处:你看不见现场
普通 Web 服务出问题,你有请求日志、有响应码、有浏览器开发者工具。MCP server 出问题,你面对的往往是一句「这个 server 没有工具」或者对话里模型轻描淡写的一句「工具调用失败了」。
难处在于中间隔了太多层:宿主应用、它内部的 client、子进程、协议流、你的代码。任何一层断了,症状都长得差不多。
所以调试的第一原则是缩短链路:先不要在 Claude Code 里调,先用 Inspector 把 server 单独连起来。Inspector 直连你的 server,中间没有模型、没有宿主策略、没有权限系统。它工作,说明问题在宿主那边;它不工作,问题就在你的代码里。

8.2 Inspector:一个包,三种用法
MCP Inspector 是官方的调试客户端。要注意的是它在 2026-07-28 规范发布时出了 v2,是一套全新的代码库——网上大量中文教程写的还是 v1(只有一个网页界面、用 MCP_PROXY_AUTH_TOKEN 那套),照着抄会对不上。写这份教程时的版本是 2.5.0,要求 Node 22.19.0 以上。
一个包,三种客户端形态:
npx @modelcontextprotocol/inspector # 网页界面(默认)
npx @modelcontextprotocol/inspector --cli # 命令行,适合脚本与 CI
npx @modelcontextprotocol/inspector --tui # 终端界面
连上你的本地 server:
npx @modelcontextprotocol/inspector uv run python server.py
或者用 SDK 自带的封装(等价,但会自动带上项目依赖):
uv run mcp dev server.py
连远程 server:
npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
有一个坑官方专门强调过:模式旗标必须放在最前面,第一个不属于 Inspector 的参数之后,所有内容都会原样转发给你的 server。要给自己的 server 传参数,用 -- 分隔:
npx @modelcontextprotocol/inspector uv run python server.py -- --verbose
CLI 模式在自动化里很好用,比如把「工具列表没坏」写进 CI:
npx @modelcontextprotocol/inspector --cli uv run python server.py --method tools/list
8.3 各个标签页分别在看什么
网页界面里的标签不少,但排查时你只需要盯住其中三个。
| 标签 | 什么时候出现 | 你在这里看什么 |
|---|---|---|
| Servers | 总是 | server 列表、连接状态、单个 server 的设置 |
| Tools | server 有 tools 能力 | 浏览 schema、填参数、调用、看结果 |
| Prompts / Resources | 对应能力存在 | 列出、读取、预览 |
| Protocol | 总是 | 完整的 JSON-RPC 往返记录 |
| Console | stdio server | server 进程的 stderr |
| Network | HTTP server | 原始 HTTP:状态码、头、body |
加粗那两个是排查时的主力。Protocol 让你看见协议层到底发生了什么(第 3 章那些 JSON 就是从这里能看到的东西),Console 让你看见你的日志。注意 Network 和 Console 不会同时出现——一个是远程场景,一个是本地场景。
这几个监控类标签可以固定成右侧的常驻栏,一边点工具一边看协议流量,这是效率最高的调试姿势。
还有一个默认值陷阱要知道:Inspector 的 server 配置里有一项叫 Protocol Era,取值是 legacy / auto / modern,而默认是 legacy。也就是说,如果你想验证 2026-07-28 的行为(无握手、新的必填头、多轮往返),得手动把它切成 modern 或 auto,否则你测的是旧形态。
关于 token:网页界面背后的 Node 服务会启动子进程,所以接口是有鉴权的,token 每次启动随机生成,并且直接写在它打印出来的那条 URL 里。要打开那条完整 URL,不要凭记忆敲 localhost:6274——那样会因为没有 token 而连不上。想固定 token 用于脚本,设 MCP_INSPECTOR_API_TOKEN。
8.4 故障对照表
按症状查,每一行给出最可能的原因和最短的验证动作。
| 症状 | 最可能的原因 | 先做这一步 |
|---|---|---|
| 宿主里这个 server 一个工具都没有 | 进程根本没起来(import 期就报错),常见于 @mcp.tool 忘了括号、依赖没装、路径不对 | 在终端里手动把宿主配置里那条启动命令原样跑一遍,看报错 |
| 工具列表曾经有、现在没了 | 有东西污染了 stdout(print、包装脚本的 echo) | 看 Console 标签或 server 的 stderr 日志;全局搜一遍 print( |
| Inspector 连不上 | 打开的 URL 少了 token;或 Node 版本低于 22.19 | 复制它启动时打印的那条完整 URL |
| Inspector 能调,宿主里不能调 | 宿主传的环境变量不同、工作目录不同、用的是另一个 Python 环境 | 在配置里改用绝对路径,并显式传 env |
| 调用报错但看不到细节 | 你抛的是普通异常,信息被 SDK 遮蔽了 | 改抛 ToolError(见 6.4),或者去 stderr 看完整堆栈 |
| 调用很慢或超时 | 每次调用都在重建连接/索引;或同步函数里做了长阻塞 | 把准备工作挪进 lifespan(第 7 章);给工具设超时 |
| 返回内容被截断 | 宿主对工具输出有上限 | 在工具里主动分页并给出引导(见 6.6),不要指望宿主放宽 |
| 改了代码没生效 | 宿主还连着旧进程 | 重启宿主,或用它的重连命令 |
8.5 让日志真的帮到你
第 2.6 节讲过日志要走 stderr。这里补三条让日志真正有用的写法。
记录每次工具调用的入参摘要,但不要记录完整内容。
log.info("search_notes keyword=%r limit=%d", keyword[:50], limit)
截断是为了防止一次超长参数把日志刷爆,也是为了不把用户数据完整地写进磁盘。
密钥永远不进日志。 这条在第 11 章会再出现一次,因为它被违反的次数实在太多——有安全研究专门统计过,大量 MCP 环境把第三方长期 API key 明文存在本地文件里,权限还是所有用户可读。
出错时记录足够定位的上下文,但返回给模型的是另一句话。 第 6.4 节那个机制在这里体现得最清楚:
try:
result = await upstream.fetch(query)
except UpstreamTimeout:
log.exception("upstream timeout, query=%r", query) # 给你自己看
raise ToolError("上游服务超时,请稍后重试,或把 limit 调小。") # 给模型看