12.5 MCP Inspector 工具链
🎯 学习目标:把 Inspector 用成"能交互调试、也能进 CI 校验"的日常工具 ⏱️ 预计时间:40 分钟 📊 难度等级:⭐⭐⭐(动手为主)
📌 一句话结论:Inspector 是官方调试器,有三种形态——CLI(脚本/CI)、TUI(终端交互)、Web(图形界面)。它最独特的设计是 protocol era:同一个 HTTP 地址,可以按"旧版"或"新版"协议去连,方便你对比行为差异。配套的 npm 包还附带一个实验性的
mcpdo连接客户端(一次连上、保持长连接跨多条命令)以及一份 Security 威胁模型文档。
🧰 三种形态,怎么选
| 形态 | 适合 | 特点 |
|---|---|---|
| CLI | CI 流水线、shell 一行命令、编码 agent 即时验证服务器改动 | 每次运行只发一个 --method 指定的请求,打印结果后退出 |
| TUI | 在终端里交互式调试 | 全键盘操作,不依赖浏览器 |
| Web | 图形化查看请求/响应/流 | 标签页式界面,最直观 |
💡 三者的服务器连接解析逻辑完全一致:配置文件里的每服务器设置(headers、超时、OAuth、protocol era、roots)在三种形态下都按同样规则生效。
🕰️ 核心概念:Protocol Era
2026-07-28 对协议做了大改,所以 Inspector 把 protocol era(协议时代:legacy 旧 / modern 新,以 2026-07-28 为界)当作一等公民的"每服务器设置",与传输方式正交——同一个 HTTP URL 既能当 legacy 服务器查,也能当 modern 服务器查。
每个服务器带一个 protocolEra,取值 legacy / auto / modern:
| Era | 连接时 Inspector 做什么 |
|---|---|
legacy | 默认值。 直接 initialize,不做任何探测 |
auto | 先探测 server/discover,遇到任何"非 modern"结果就回退到 initialize |
modern | 精确钉住 2026-07-28,不回退,遇到非 modern 服务器就直接失败 |
配置位置:Web 客户端在 Server Settings;catalog/配置文件里是 protocolEra 字段;CLI 与 TUI 从同一份文件读取,也可以用 --protocol-era <legacy|auto|modern> 在单次运行时覆盖该设置。
💡 为什么默认是
legacy而不是auto? 调试工具不应该自动探测。对一个沉默的 legacy stdio 服务器发server/discover探测会卡住,还会污染你本就要看的那份 transcript。选auto或modern是一个主动动作,这样你在 Protocol 标签里看到的,就是"你的服务器面对一个按你配置行事的客户端时"会看到的东西。
连接成功后,协商出的 era 会作为徽章显示在 Protocol 标签的 Messages 头部,并出现在 Connection Info 里。modern 连接下,server/discover 还会提供 capabilities(含 extensions)、instructions 与 supportedVersions;服务器名与版本在结果 _meta 的 io.modelcontextprotocol/serverInfo 里。
era 差异速览(同一功能在两种 era 下的不同表现):
| 功能 | Legacy | Modern |
|---|---|---|
| 日志 | 会话级:logging/setLevel 设一次,服务器持续推 notifications/message | logging/setLevel 已移除;改为每请求在 _meta["io.modelcontextprotocol/logLevel"] 上 opt-in |
| 资源订阅 | resources/subscribe,会话级标记 | subscriptions/listen 长连接流,带 stream-status 徽章(Connecting → Listening;断流时 Reconnecting...,彻底关闭 Stream ended,服务器不确认时 Not acknowledged) |
| Tasks | 靠 capabilities.tasks 显示标签页;tasks/list + 阻塞式 tasks/result | 靠协商的扩展 io.modelcontextprotocol/tasks;只轮询 tasks/get,完成即内联结果 |
| MRTR | 服务器主动发请求(server.elicitInput 等) | 手动逐轮驱动:input_required 在 pending-request 弹窗里等你回答 |
| 会话 | 可能有 Mcp-Session-Id,断开时发 DELETE;legacy 还会在 initialize 后开一条独立的 GET 通知流(Server Settings 里的 Suppress Notification Stream 可跳过它) | 无会话、按请求;不向服务器发 DELETE,断开纯属本地 |
| 取消 | 发 notifications/cancelled | modern Streamable HTTP 改为关闭该请求自己的 SSE 响应流(2026-07-28 的取消信号);stdio 仍走 notifications/cancelled |
⚠️ legacy 的
collect_elicitation模式在 2026-07-28 连接上会报错,因为该协议版本不允许服务器向客户端发请求。MRTR 是它的现代替代。
密钥存储与明文风险
Inspector 把凭据移出 mcp.json / client.json / oauth.json,改存进一个密钥存储:获取的 OAuth 令牌(access / refresh / IdP session)、每个服务器的 OAuth client secret、以及每个 stdio 服务器的 env: 值(env 键留在文件里、值清空,真实值进存储)。headers 不会被移走,仍原样写在 mcp.json 里。
⚠️ 没有密钥环时,密钥会落进明文文件,而且你没被询问。 在密钥环探测失败的机器上(没装 libsecret 或没有运行中的 Secret Service 的 Linux、headless / SSH 会话、Termux、挂载卷的容器),Inspector 自动把密钥存到
~/.mcp-inspector/secrets.json,未加密(除非你提供密钥)。文件权限0600能挡住其他非 root 用户,但挡不住 root、挡不住你家目录的备份或副本,也挡不住以你身份运行的程序——包括 Inspector 启动的 stdio 服务器。
三选一:
- 找回密钥环:装 libsecret 并运行一个 Secret Service(如
gnome-keyring),或在桌面会话里跑 Inspector;下次启动会把文件里的密钥迁进密钥环并删除文件。 - 加密文件:用
MCP_INSPECTOR_SECRET_KEY_FILE(推荐)或MCP_INSPECTOR_SECRET_KEY提供生成的密钥。 - 完全不落盘:
MCP_INSPECTOR_SECRET_STORE=memory,每次会话重新输入。
💻 CLI 用法详解
每次 CLI 运行:连上服务器 → 用 --method 发一个请求 → 打印结果 → 退出。这正是 CI 与一行命令需要的形态。
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list安装后可直接用 mcp-inspector 二进制;未全局安装则前面加 npx @modelcontextprotocol/inspector。
选择服务器
# stdio:位置参数就是要 spawn 的命令
mcp-inspector --cli node build/index.js --method tools/list
# HTTP
mcp-inspector --cli https://api.example.com/mcp --transport http --method tools/list
# 从配置文件里按名取
mcp-inspector --cli --config ./mcp.json --server myserver --method tools/list⚠️ 配置文件是给一次运行提供 roots 的唯一持久方式:没有 roots 相关的 flag,
--method roots/set只对那一条短命连接生效。为某服务器配置的 roots 会在连接时公布,所以调用roots/list的服务器(如@modelcontextprotocol/server-filesystem用来获知允许目录)能拿到。
支持的 --method
--method | 必需的配套参数 | 说明 |
|---|---|---|
initialize | 无 | 仅连接的探测:返回 {serverInfo, protocolVersion, capabilities, instructions} |
tools/list | 无 | --strict 把 schema 可移植性检查变成一道闸门(见 CI 闸门) |
tools/call | --tool-name,加(可选)--tool-arg / --tool-args-json | |
resources/list | 无 | |
resources/read | --uri | |
resources/directory/read | --uri,可选 --cursor | 分页读取,传回上一页的 nextCursor |
resources/templates/list | 无 | |
prompts/list | 无 | |
prompts/get | --prompt-name,加(可选)--prompt-args | |
logging/setLevel | --log-level | 仅 legacy;modern 服务器改为按请求 opt-in |
skills/list | 无 | --verify 校验返回的 skills(见 CI 闸门) |
skills/get | --uri | 同样支持 --verify |
servers/list | 无 | 不连任何服务器,只读 catalog |
servers/show | --server | 同上,针对单条条目 |
servers/add | --server,加一个位置参数 target 或 --server-url | 写 catalog(不连接),见 编辑 catalog |
servers/edit | --server,加要改的项 | 同上 |
servers/remove | --server | 同上 |
流式/会话类方法(如 logging/tail)会被拒绝——一个发完就退出的进程无法保持流打开。
传参数:两种方式,语义不同
--tool-arg key=value:会尝试把每个值按 JSON 解析,能解析就转成对应类型,所以 count=1 和 zip=10001 都会发成数字;解析不了的(zip=012、裸词)按字符串发送,再由工具 input schema 声明的类型转换,因此数值型 zip 下 zip=012 仍会发成 12:
mcp-inspector --cli <server> --method tools/call --tool-name mytool \
--tool-arg key=value --tool-arg count=1 --tool-arg 'options={"format":"json"}'--tool-args-json:一次传入整个参数对象,跳过 key=value 解析,所以 {"zip":"10001"} 发的是字符串 "10001" 而非数字;schema 转换仍会作用于字符串值,只有该属性被声明为字符串(或未声明)时才保留原字符串。两者互斥:
mcp-inspector --cli <server> --method tools/call --tool-name mytool \
--tool-args-json '{"zip":"10001"}'输出格式
--format text(默认):给人看的美化输出--format json:stdout 上输出单个 JSON 对象、无横幅,便于整段管道处理:
mcp-inspector --cli <server> --method tools/list --format json | jq '.result.tools[].name'退出码与错误信封(CI 关键)
每个非零退出都映射到一个稳定的失败类别,调用方可以据此分支,而不必去猜文案:
| 码 | 含义 |
|---|---|
0 | 成功 |
1 | 用法或意外错误(兜底),也包括读不到的密钥存储或 OAuth 状态文件 |
2 | 该工具没有 MCP App(--app-info 探测) |
3 | 服务器要求认证(HTTP 401/403,或 SDK 授权错误) |
4 | 服务器不可达(DNS、连接被拒、超时、fetch failed) |
5 | 工具错误:tools/call 返回 isError: true,或工具不存在 |
6 | --strict 发现 error 级工具 schema 可移植性问题 |
7 | --verify 发现违反 SEP-2640 的 skill(一致性、摘要或大小不符) |
8 | --verify 未能在读取边界内检查完所有 skill |
9 | --verify --require-digests 发现未声明摘要的 skill |
任何非零退出,CLI 还会向 stderr 写一行 JSON:
{
"error": {
"code": "auth_required",
"message": "Unauthorized",
"status": 401,
"url": "https://api.example/mcp"
}
}信封始终带 code 与 message,已知时还会带 cause(底层错误,如 DNS 失败)、status(HTTP 状态码)与 url;URL 中的 query 密钥会被脱敏。code 是一组稳定的失败名:auth_required、unreachable、tool_not_found、tool_is_error、no_app、schema_unportable、store_unavailable、oauth_state_unrecognized,或兜底 error 等。因为只有一行,可以用 2>&1 | tail -1 | jq .error 解析。
💡
tools/call即使返回isError: true仍会打印其载荷,但退出码为5——所以&&链不会在失败调用后继续。
脚本里的授权
默认情况下 CLI 会跑和 TUI 一样的 loopback OAuth 流程:开浏览器、等一个 localhost 回调——CI 跑不通。两个 flag 让非交互运行可预测:
| flag | 行为 |
|---|---|
--stored-auth-only | 从不启动交互式 OAuth 或 step-up,从不自动开浏览器;有令牌就用共享存储里的,没有就立即以 auth_required 失败。CI 要的就是它 |
--use-stored-auth | 复用本机 Web Inspector 已取得的令牌,有刷新令牌时先刷新。按 URL 查找令牌,因此需配合 --server-url |
两者都不给、且 stdin/stderr 没有 TTY 时,CLI 会快速失败为 auth_required,而不是傻等十五分钟的回调。
CI 闸门
三个 flag 能把一次检查变成非零退出,各自有独立退出码,便于在 job 里区分失败类型:
--strict(配合tools/list):把工具 schema 可移植性问题完整打印到 stderr(路径、问题、建议修复),出现 error 级问题时退出6;不加则tools/list只打印一行计数。--verify(配合skills/list或skills/get):跑 SEP-2640 一致性与摘要校验,每个 skill 在 stdout 打印一份 JSON 报告;出现违规退出7,读取边界提前终止退出8。--skill-catalog-max-skills/--skill-catalog-max-bytes设定边界。--require-digests(配合--verify):遇到未声明摘要的 skill 退出9,而不是当作不可校验并退出0。
mcp-inspector --cli <server> --method tools/list --strict > /dev/null
mcp-inspector --cli <server> --method skills/list --verify --require-digests编辑 catalog
servers/add、servers/edit、servers/remove 不连接任何服务器就能写 catalog,走的是与 Web 客户端服务器列表相同的代码,所以 env 值与 client secret 会落进密钥存储(就像从 UI 录入一样),正在运行的 Web 客户端也会立刻看到改动。只有可写的 catalog(--catalog、MCP_CATALOG_PATH 或默认位置)能被写入;--config 会被拒绝。
# 新增:位置参数 target 或 --server-url 描述的是要登记的项,不是要连的服务器
mcp-inspector --cli node build/index.js -e API_KEY=secret --method servers/add --server my-server
# 修改:改 target、-e、--cwd、--header、--protocol-era,或重命名
mcp-inspector --cli --method servers/edit --server my-server --rename my-renamed-server
# 删除该条目及其存储的密钥
mcp-inspector --cli --method servers/remove --server my-renamed-server⚠️ 使用内存态密钥存储时,带
-e值(或--rename)的写入会被拒绝,因为这些密钥会在进程退出时丢失。
🍳 常用 recipe
在 CI 里验证服务器
set -euo pipefail
# 连不上、或没暴露目标工具,就让构建失败
mcp-inspector --cli --config ./ci-servers.json --server my-server \
--stored-auth-only --method tools/list --format json \
| jq -e '.result.tools | map(.name) | index("get_weather")' > /dev/null按失败类别分支
if out=$(mcp-inspector --cli "$URL" --transport http --method tools/list 2>err.json); then
echo "$out"
else
case $? in
3) echo "需要认证:先跑一次 web inspector 登录" ;;
4) echo "服务器不可达" ;;
*) jq .error < err.json ;;
esac
fi找出所有带 UI 的工具
mcp-inspector --cli "$URL" --transport http --method tools/list --app-info \
| jq -r 'select(.hasApp) | .toolName'不连接、只查 catalog
mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --method servers/list
mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --method servers/show --server my-server⚠️
servers/show会脱敏带密钥的字段(env值、敏感 header、OAuth client secret),但不会清理 URL 里内嵌的凭据(userinfo 或 query token),也不会清理 stdio 的args。贴 issue 前把原始 URL 和detail字段当作敏感信息。
🧭 排错范式
| 你看到的 | 去哪看 | 大概率原因 |
|---|---|---|
| 请求发出但无响应 | Web 的 Network / Protocol | 传输端点、Mcp-* 头缺失 |
| 登录/授权失败 | Web 的 Authorization | OAuth 配置或 iss 校验(见 12.1) |
| 工具列表里缺了某工具 | Tools 标签的 Excluded 分隔区 | 标了无效的 x-mcp-header(SEP-2243),合规客户端会丢弃它 |
返回 -32022 | Protocol 标签 | 协议版本不受支持;data.supported 里有服务器支持的版本 |
返回 -32020 | Network 标签 | 缺少或错误的镜像 header |
| 日志一条都没有 | Logs 标签 | modern era 下 logLevel 为 off——这是正确行为,不是 bug |
💡 代理:连远程 HTTP/SSE 服务器遵循常规代理变量(
HTTPS_PROXY/HTTP_PROXY/NO_PROXY),无需 Inspector 专属 flag。
💡 不想让它自动开浏览器? 设置
MCP_AUTO_OPEN_ENABLED=false即可关闭 Web 客户端的自动打开;在 CLI 下则不再强制自动打开、也不在无 TTY 时跑交互式 OAuth。
📖 官方资料
- 📋 Inspector 总览
- 💻 CLI client
- 🖥️ TUI client
- ⚙️ Configuration and flags
- 🕰️ Protocol eras
- 🍳 Recipes
- 🔌 mcpdo 连接客户端
- 🛡️ Security(威胁模型)
- 💻 Inspector 代码仓库
🎉 恭喜你走完全书。 从第1章的概念到这里,你已经覆盖了从写一个服务器,到授权、治理、扩展、分发与调试的完整链路。