
mcpsnoop v0.15.0
Wireshark for MCP。一个透明代理,能实时显示AI客户端与MCP服务器之间的每一次真实工具调用,就在你的终端中。
面向 MCP 的 Wireshark。 一个透明代理,可在终端中实时显示 AI 客户端与 MCP 服务器之间的每一次真实工具调用。
问题
官方 MCP Inspector 以独立客户端身份连接,因此它永远看不到你的客户端(Cursor、Claude Code、Codex)实际发送给服务器的内容。而且,任何等待请求到达的工具都无法显示模型从未发出、或使用了错误参数发出的调用。当某个工具悄然未被调用、能力不匹配,或调用一直挂起时,你只能翻日志、靠猜。
mcpsnoop 取而代之,位于真实数据路径中。 用它在你的服务器命令外层包装,即可在真实客户端与服务器对话时,实时查看每一个 JSON-RPC 帧。
快速开始
无需任何配置,立即查看效果。```bash mcpsnoop demo
要真正使用它,请将你的服务器封装到客户端的 MCP 配置中。```json
{
"mcpServers": {
"my-server": {
"command": "mcpsnoop",
"args": ["--", "node", "build/index.js"]
}
}
}
-- 之后的内容是通常用于启动服务器的命令。换成
你已经在使用的任何命令即可,例如 python server.py、npx -y @scope/server 或
编译后的二进制文件。
在 Claude Desktop 中,你无需手动进行该修改。```bash mcpsnoop wrap my-server # route my-server through mcpsnoop mcpsnoop unwrap my-server # put it back
`wrap` 会找到 `claude_desktop_config.json`,首次运行时会将其复制为
`claude_desktop_config.json.mcpsnoop.bak`,并且只重写那一个
服务器的条目,因此你的格式设置和其他所有服务器都不会被改动。
重写后的条目内部,键会按字母顺序排列。`unwrap`
会恢复该文件,并且一旦没有服务器处于 wrapped 状态,就会删除备份。
在这两种操作之后都需要重启 Claude Desktop,因为 MCP 服务器只在启动时加载一次。
然后像平常一样使用你的客户端,并打开 UI。```bash
mcpsnoop
无需标志,无需套接字路径,无需记住启动顺序。shim 和 UI 会自动找到彼此,UI 会从磁盘回填过去的会话。对于可流式 HTTP 服务器,将 mcpsnoop 作为反向代理运行。```bash mcpsnoop http --target http://localhost:3000/mcp --listen :7000
每个响应的 HTTP 状态都会显示在流中,因此即使响应本身
不携带 JSON-RPC 消息,也仍然是一个可见的帧,而不是什么都没有:
401 挑战、被拒绝的 Origin 上的 403、确认通知的 202,
以及目标完全无法访问时的 502。401 的
`WWW-Authenticate` 头会被原样保留并在检查器中显示,因为它
指明了认证方案以及下一步要访问的资源元数据。使用
`status:401` 可在 TUI 中按状态过滤,或使用 `status:err` 按任何失败过滤。4xx 或 5xx
会被计为错误,因此默认的 `mcpsnoop check` 运行会因此失败。
没有自己的服务器?[实际试一试](https://github.com/kerlenton/mcpsnoop/blob/HEAD/docs/TRY_IT.md)针对一个已发布的
测试服务器,并由你自己的客户端驱动。要检查已经发生的会话,
请参阅[从日志中回顾过去的会话](https://github.com/kerlenton/mcpsnoop/blob/HEAD/docs/POST_MORTEM.md)。
### 配置文件
如果你在项目中重复使用相同的 shim 标志,请将它们放在
当前工作目录下的 `.mcpsnoop.toml` 文件中。```toml
label = "filesystem"
trace-file = "trace.jsonl"
redact-secrets = true
redact-key = "token,authorization"
redact-value = "sk-[A-Za-z0-9]+"
redact-path = "$.params.arguments.password"
no-trace = false
将 redact-key、redact-value 和 redact-path 各占一行,即可分别添加多个。
这就是它所支持的全部键。
该文件只在当前工作目录中查找,不会在父目录中查找。
显式的命令行标志会覆盖配置文件中的值。
命令
| 命令 | 作用 |
|---|---|
mcpsnoop -- <server> | 将 stdio 服务器包装为透明垫片 |
mcpsnoop | 打开实时 TUI |
mcpsnoop http --target <url> | 代理 streamable-HTTP 服务器 |
mcpsnoop export | 将会话渲染为 json、html、text、har 或 otlp 格式 |
mcpsnoop check | 在出现错误、无效帧、警告、路由不匹配、挂起调用或延迟结果时使 CI 失败 |
mcpsnoop baseline | 检查、接受或重置受信任的工具定义 |
mcpsnoop diff | 比较两次捕获会话中的工具和调用 |
mcpsnoop open | 在 TUI 中打开已保存的会话 |
mcpsnoop prune | 删除早于截止时间的已保存会话日志 |
mcpsnoop wrap <server> | 将 Claude Desktop 的某个服务器路由到 mcpsnoop |
mcpsnoop unwrap <server> | 将该服务器的条目恢复原样 |
mcpsnoop remote <user@host> | 打印 SSH 隧道命令 |
mcpsnoop demo | 播放脚本化会话 |
运行 mcpsnoop help 查看完整列表,或运行 mcpsnoop help <command> 查看某个命令的标志。
对比
| MCP Inspector | mcpsnoop | |
|---|---|---|
| 查看你的真实客户端和服务器流量 | 否 | 是 |
| 标记挂起调用和流错误 | 否 | 是 |
| 标记会破坏流的杂散输出 | 否 | 是 |
| 标记格式错误的 JSON-RPC 帧 | 否 | 是 |
| 批准后检测工具定义漂移 | 否 | 是 |
| 交互式终端界面 | 否 | 是 |
| 零配置,无需标志或排序 | 否 | 是 |
| 能力检查器 | 部分 | 是 |
| 重放捕获的调用 | 否 | 是 |
| 会话导出(json / html / text / otlp) | 否 | 是 |
| 单一二进制,无运行时依赖 | 否 | 是 |
安装
Go```bash
go install github.com/kerlenton/mcpsnoop/cmd/mcpsnoop@latest
### Homebrew```bash
brew install mcpsnoop
适用于所有平台的预构建二进制文件位于 Releases 页面。
Shell 补全
mcpsnoop 为 bash、zsh、fish 和 PowerShell 提供了补全功能。运行 mcpsnoop completion <shell> --help 查看设置步骤,其中涵盖如何启用补全以及针对你的操作系统的安装路径。
工作原理
mcpsnoop 在一个二进制文件中扮演两个角色。mcpsnoop -- <server> 是你的客户端生成的透明垫片,它逐字转发字节,同时将每一帧的副本发送到 hub。不带参数的 mcpsnoop 就是这个 hub 及其实时 TUI。它们通过一个众所周知的 socket 和磁盘上的日志配对,因此两者都不需要先启动。
默认情况下,hub 会加载最近的 100 个已保存会话,在保留较早痕迹的同时将启动工作量保持在有界范围内。使用 mcpsnoop --history-limit N 选择其他限制,或使用 mcpsnoop --history-limit 0 加载完整历史记录。较早的会话仍可通过 mcpsnoop open <session-id> 和 mcpsnoop export <session-id> 使用。
历史记录限制限定了加载的内容;mcpsnoop prune 限定了保留的内容。它会删除早于某个截止时间的已保存会话日志,并且绝不会自行运行。```bash
mcpsnoop prune --older-than 30d --dry-run # list what would go, remove nothing
mcpsnoop prune --older-than 30d # delete after confirming
mcpsnoop prune --older-than 72h --yes # skip the prompt in a script
`--older-than` 是必需的(没有会删除任何内容的默认值),接受类似 `30d` 的天数或类似 `72h` 的 Go 持续时间。工具基线不会被触碰,因为基线是按服务器标签而非会话进行键控的。
因为它位于实际的管道中,而不是像 Inspector 那样在旁路,所以它能准确看到真实客户端和服务器彼此通信的内容,无论服务器用什么语言编写。
## 快捷键
| 键 | 操作 | | 键 | 操作 |
|---|---|---|---|---|
| `enter` | 检查 / 下钻 | | `/` | 筛选 |
| `esc` | 返回 | | `:` | 命令 |
| `j` / `k` | 移动 | | `r` | 重放调用 |
| `g` / `G` | 顶部 / 底部 | | `c` | 能力 |
| `ctrl-f` / `ctrl-b` | 翻页 | | `s` | 工具摘要 |
| `p` | 暂停 | | `y` | 复制 |
| `shift`+`<key>` | 按列排序 | | `e` | 导出 |
| `ctrl-d` | 删除会话 | | `f` | 跟踪 |
| `?` | 帮助 | | | |
在应用中按 `?` 查看完整列表。
## 筛选流
在会话中按 `/`,将空格分隔的令牌组合起来(AND 逻辑)。纯文本匹配方法、工具、ID 和负载。
| Token | 筛选依据 | Example |
|---|---|---|
| `tool:` | 工具名称 | `tool:search` |
| `method:` | JSON-RPC 方法 | `method:tools/call` |
| `id:` | 请求 ID,以及任何继续该请求的重试 | `id:7` |
| `task:` | 任务 ID | `task:01J...` |
| `dir:` | 方向 (`c2s`, `s2c`) | `dir:s2c` |
| `kind:` | 帧类型 (`req`, `resp`, `notify`, `stderr`, `invalid`) | `kind:invalid` |
| `status:` | 调用结果 (`ok`, `error`, `cancel`, `late`, `cancelled`, `pending`, `bad`, `warn`, `mismatch`, 或类似 `401` 的 HTTP 状态) | `status:error` |
堆叠令牌以进行更精确的筛选。```text
tool:search status:pending # in-flight calls to one search tool
status:cancel # calls the client gave up on (status:cancelled is a cancelled task)
status:late # results that arrived after the cancellation
method:tools/call status:error # tool calls that failed
dir:s2c kind:req # server-initiated requests (servers before 2026-07-28)
最后一种只能在采用 2025-11-25 或更早协议的服务器上找到内容。而 2026-07-28 修订版移除了服务器发起的请求,服务器如果需要客户端提供某些东西, 现在会通过响应客户端自身的请求来索取,然后客户端重试。mcpsnoop 会将这些重试 与它们所延续的请求关联起来,因此整个交互看起来是一次调用,而不是多次。
导出会话
将任意捕获的会话转换为一个可移植的文件。```bash mcpsnoop export -T json|html|text|har|otlp [-o file|-] [session-id|log.jsonl|-]
| 格式 | 你会得到什么 |
|---|---|
| `json` | 关联的调用、各工具计数及 p50/p95/p99 延迟、最慢调用、能力以及原始帧 |
| `html` | 一个自包含的浏览器文件,带搜索和可折叠的 JSON |
| `text` | 一份格式化的纯文本转储 |
| `har` | 每次关联调用对应一条条目,可在浏览器开发者工具以及任何支持读取 HAR 的工具中打开 |
| `otlp` | OTLP JSON,每次关联调用对应一个 span;W3C trace context 会接入调用方的 trace,否则每次会话使用一条 trace |
MCP 不是 HTTP,因此 HAR 条目中的 URL、状态码和计时是针对每次调用的有意映射,
而非线缆上的真实传输记录。
对于 OTLP,请求的 `_meta.traceparent` 提供该调用的 trace 和父 span ID,
而 `_meta.tracestate` 则随 span 一并携带。当 traceparent 缺失或无效时,
mcpsnoop 会保留会话派生的 trace 且不携带任何状态。mcpsnoop 只观察而不参与,
因此它不会添加自己的供应商条目,并会原样传递调用方的状态。```bash
mcpsnoop export -T html -o out.html # an HTML file to open in a browser
mcpsnoop export -T text server.py-48213-7f3a1c9e2b04 # a specific session, as text
mcpsnoop export -T json | jq # the newest session, piped to jq
mcpsnoop export -T har -o session.har # a HAR file to open in browser devtools
mcpsnoop export -T otlp -o trace.json # import into an OTLP-compatible tracing backend
省略 -o 可写入标准输出,省略 session 则取最新的会话,或传入 - 从标准输入读取 JSONL。在 TUI 中,按 e 可将选中的会话导出为 HTML,或在命令模式下运行 :export json|html|text|har|otlp [path]。
在检查或分享捕获内容前如需擦除敏感信息,请在 export 或 open 时传入与捕获阶段相同的脱敏标志:```bash
mcpsnoop export session.jsonl --redact-secrets --redact-key project_token -o shared.json
mcpsnoop open session.jsonl --redact-path '$.params.arguments.password'
这些标志会改写导出的文件或内存中的 TUI 视图,但绝不会改写源 JSONL。`export` 拒绝输出与输入同名的文件,并通过临时文件写入后重命名到位,因此一次失败的运行会让之前的文件保持完整。
`--redact-key` 与 `--redact-secrets` 不会改动 `tools/list` 结果中所公布的工具的 `inputSchema` 与 `outputSchema`。模式中的名字是类型声明而非值,因此名字本身无论何种情况都会保留在日志中;而清除名为 `token` 的属性下的子模式,会连同工具自身的检查一并删除。豁免仅限该位置,因此恰好名为 `inputSchema` 的参数会像其他任何参数一样被清除;该清除会在 `default`、`const`、`examples` 和 `enum` 处停止,因为这些位置保存的是数据而非结构。如需指定模式内部的某个对象,请使用 `--redact-path`;或使用 `--redact-value`,它匹配任意位置的文本,但 mcpsnoop 解析的两个关键字 `type` 与 `x-mcp-header` 除外。
每个标志覆盖的范围各不相同,因此请检查结果而非想当然。四个标志都会清除 JSON-RPC 载荷,而 `--redact-key`、`--redact-path` 与 `--redact-secrets` 仅作用于这些载荷。只有 `--redact-value` 还会清除 stderr、其他非 JSON 文本以及字符串内部内容。`Mcp-Param-*` 头会与其镜像的正文值一同被清除;其他信封元数据——服务器标签、`Mcp-Name`、`Mcp-Method` 与 HTTP 状态——均按捕获原样保留。清除是尽力而为的操作,因此请使用单独的输出路径,并在分享前先查看结果。
### 将已完成的调用流式发送到 OTLP 收集器
在代理运行期间,将其指向 OTLP/HTTP JSON 追踪端点即可发送 span。可重复指定 `--otlp-header` 以设置收集器认证或租户头。```bash
mcpsnoop \
--otlp-endpoint http://localhost:4318/v1/traces \
--otlp-header "Authorization=Bearer $OTLP_TOKEN" \
-- node build/index.js
mcpsnoop http \
--target http://localhost:3000/mcp \
--otlp-endpoint http://localhost:4318/v1/traces
投递是尽力而为的,并且绝不会阻塞被代理的 MCP 流量。如果收集器 不可用,mcpsnoop 会在后台重试,并在其有界队列已满时 丢弃新的跟踪帧。常规的 JSONL 会话日志仍然是持久化 记录。
比较会话
按 ID 或 JSONL 路径比较两个已保存的会话。```bash mcpsnoop diff before-session after-session mcpsnoop diff old.jsonl new.jsonl
报告显示新增或移除的工具、描述和 `inputSchema` 变更、状态发生变化且匹配的工具调用,以及显著的时长变化。调用按工具名称和参数进行匹配,因此重新排序的调用仍能正确比较。默认情况下,时长变化至少需相差 100 毫秒且达到 2 倍;使用 `--duration-threshold` 和 `--duration-ratio` 可调整这些阈值。
传递 `--exit-code` 可在 CI 中基于回归进行门控:当后续会话移除工具、更改工具描述、标题、输入 schema、输出 schema 或注释,出现状态变差的调用,或发生性能下降时,它会以非零状态退出。改进(新增工具、修复调用、速度提升)仍以零状态退出,图标更改(仅改变工具外观而不改变其功能)也是如此。
## 在 CI 中检查会话
根据错误、流损坏、协议警告、路由头不匹配、从未收到响应的调用、导致捕获不完整的丢弃帧、工具定义漂移,或使用已弃用的协议功能,对录制的代理运行进行门控。```bash
mcpsnoop check [--format text|junit|sarif] [--fail-on error,invalid,warn,mismatch,pending,late-result,drift,deprecated,incomplete,schema] [session-id|log.jsonl|-]
error、invalid 和 warn 单独即可导致检查失败。其余为可选启用。
传入逗号分隔的子集,仅对任务关心的信号进行门控;省略会话参数以
检查最新的捕获结果,或使用 - 从标准输入读取 JSONL。
| 信号 | 失败条件 |
|---|---|
error | 以 JSON-RPC 错误应答的调用、标记为 isError 的结果,或以失败告终的任务 |
invalid | 协议通道上不是有效 JSON-RPC 的帧,通常是服务器在向 stdout 记录日志 |
warn | 违反 MCP 或 JSON-RPC 规范所设预期的帧 |
mismatch | 路由头与正文不一致、随批次携带,或在修订版要求时缺失 |
pending | 捕获结束时仍处于打开状态的请求,导致调用方一直在等待 |
late-result | 在其请求被取消之后才到达的响应 |
drift | 在基线获批后发生变更的已公布工具定义 |
deprecated | 规范已弃用的特性 |
incomplete | 上游丢弃的帧,这使所有其他计数成为下限而非总数 |
schema | 使用了在客户端之间兼容性不佳的构造或方言的已公布架构 |
无论信号是否参与门控,每个信号都会被计数;因此,在您决定哪些信号应导致失败之前, 一次运行就会说明它发现了什么。``` session build-agent: errors=1 invalid=0 warnings=0 mismatches=0 pending=0 late_results=0 deprecated=0 missing_frames=0 schema_findings=1 schema findings: oneOf: search check failed: error
丢帧计数也会随产物一并保存,因此,一个低估了自身丢帧数的捕获无论在哪里
打开都会如实体现这一点:JSON 导出中的 `missing_frames`、HAR 中的
`log.comment`,以及 OTLP 中的 `mcpsnoop.session.missing_frames`
资源属性。```bash
mcpsnoop check build-agent
mcpsnoop check --fail-on error,invalid artifacts/session.jsonl
mcpsnoop check --fail-on mismatch gateway-run.jsonl
除了信号计数之外,还要断言运行的整体形态。这些标志相互组合,也支持与 --fail-on 组合使用,任何失败都会以非零状态退出。
| 标志 | 失败条件 |
|---|---|
--max-duration <dur> | 一个或多个已完成的工具调用超出了预算;报告其数量以及最严重的一次调用 |
--expect-tool <name> | 指定名称的工具从未被调用(可重复) |
--forbid-tool <name> | 指定名称的工具被调用过(可重复) |
a contract for the run: search must run, delete must not, nothing over 2s
mcpsnoop check --expect-tool search --forbid-tool delete --max-duration 2s run.jsonl
### 在 CI 已经查看的地方报告
`--format junit` 为每个信号和会话写入一个 `<testcase>`,其失败遵循与文本输出相同的 `--fail-on` 选择。```yaml
- name: Check captured MCP session
run: |
mkdir -p test-results
mcpsnoop check --format junit artifacts/session.jsonl > test-results/mcpsnoop.xml
- name: Upload mcpsnoop JUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: mcpsnoop-junit
path: test-results/mcpsnoop.xml
--format sarif 改为写入 SARIF 2.1.0 日志。junit 为每个信号报告一个
聚合结果,而 SARIF 为每个发现报告一个结果,携带会话、
帧的 Seq 以及帧自身的警告或漂移文本,并指向
该帧被解码出的日志行。在 --fail-on 中指定的信号以
error 级别报告,之外的信号以 note 级别报告,因此报告与
门控永远不会不一致。
结果通过相对于工作目录的路径指向日志,而代码扫描随后
会基于仓库根目录解析该路径。仅当该路径是被分析提交中的
文件时,警报才会连同其周围的代码行一起渲染;因此,工作流
生成到 artifacts/ 的捕获会打开一条警报,携带
消息、规则和行号,但没有源代码视图。只有提交你想
完整渲染的捕获,才能获得源代码视图。从状态目录或
标准输入读取的日志则完全没有路径。
代码扫描会拒绝运行中包含超过 25,000 条结果的文件,并且
只显示其接受的运行中最前面的 5,000 条,因此报告上限为 5,000:
首先是门控失败所针对的发现,然后是一条 mcpsnoop/report-truncated 结果,
说明有多少条被省略。text 和 junit 格式则保持完整。
要将发现放入“安全”选项卡,请将 SARIF 日志交给
upload-sarif。该任务需要 security-events: write 权限,否则上传会返回
403。check 在发现问题时以非零状态退出,因此上传步骤需要 if: always()
才能在需要报告的运行中执行;continue-on-error
将裁决交给代码扫描检查,该检查会在 error 级别
警报时失败,并可设为必需检查。如果你更希望由检查
步骤本身来决定任务是否失败,请将其移除。```yaml
permissions:
required for all workflows
security-events: write
only required for workflows in private repositories
actions: read contents: read
steps:
- name: Check captured MCP session continue-on-error: true run: mcpsnoop check --format sarif artifacts/session.jsonl > mcpsnoop.sarif
- name: Upload mcpsnoop SARIF report if: always() uses: github/codeql-action/upload-sarif@v4 with: sarif_file: mcpsnoop.sarif category: mcpsnoop
### 捕获与请求体不一致的路由标头
在 streamable-HTTP 传输中,网关依据 `Mcp-Method` 和 `Mcp-Name` 进行路由,而服务器读取的是请求体,因此与请求体不一致的标头意味着两者看到的是两个不同的请求。`mismatch` 信号涵盖:标头与请求体不一致、标头搭载在它无法寻址的批次上、以及必需标头完全缺失。
在 2026-07-28 修订版中,缺少路由标头属于校验失败,合规的服务器会以 `400` 和 `-32020` 拒绝该请求。mcpsnoop 仅在确知会话使用该修订版或更高版本时才报告此信号,因为更早的修订版根本没有定义这些标头,在那里省略它们是正确行为。服务器自身的 `-32020` 拒绝也计为同一信号。
无法放入 HTTP 字段值中的名称或资源 URI 会以 Base64 形式置于 `=?base64?…?=` 哨兵中,并在比较之前解码,因此编码正确的客户端永远不会被标记。
在 HTTP `tools/call` 请求上,mcpsnoop 还会显示每个 `Mcp-Param-{Name}` 标头,并且在已知匹配的已发布工具定义时,将其与带注解的参数路径进行比较。嵌套属性、Base64 哨兵、布尔值以及数值等价的安全整数都能被妥善处理,不会因字符串比较而产生误报。未知的参数标头以及没有匹配工具定义的会话仅作观察。基于键和值的脱敏处理会在捕获的参数标头值到达接收端(sink)之前对其生效,而 mcpsnoop 自行擦除的值绝不会被报告为不一致。
### 检测工具定义漂移
对某个服务器标签观察到的第一个完整 `tools/list` 即成为其可信基线。后续会话会逐字段比较该基线:描述、标题、输入和输出 schema、注解和图标,以及新增或移除的工具。注解最为重要,因为一个以 `readOnlyHint` 获批使用、后来却声明自身具有破坏性的工具,正是此项检查所要防范的“撤梯子”(rug-pull)行为,而且规范告知客户端应将注解视为不可信。标题和图标之所以被跟踪,是因为它们是用户实际看到的内容;规范将工具的 `title` 排在 `annotations.title` 和工具名称之上。会话表和工具摘要会标记漂移,但不会阻塞或改变 MCP 流量。
注解通过其规范默认值进行比较,因此服务器开始显式写出它本来就依赖的提示时不会被报告。在 mcpsnoop 开始跟踪某个字段之前记录的基线,对其确实记录的字段仍然有效,并会指出哪些字段它无法回答;一旦你信任当前的定义,请使用 `mcpsnoop baseline --accept` 重新记录。
更改脱敏记录的内容,也就改变了漂移比较的对象。未使用 `--redact-value` 记录的基线,在对照使用该选项进行的捕获检查时,会把被擦除的字段报告为已更改——这是正确的,因为记录下的定义确实发生了变化。更改脱敏设置后,请使用 `--accept` 重新记录。
对于命令名称或目标主机本会冲突的每个服务器,请使用稳定且唯一的 `--label`。基线存储在 mcpsnoop 常规状态目录下,因此 `MCPSNOOP_HOME` 和 `XDG_STATE_HOME` 同样适用。```bash
mcpsnoop check --fail-on drift session.jsonl
mcpsnoop baseline session.jsonl
mcpsnoop baseline --accept session.jsonl # trust a legitimate definition change
mcpsnoop baseline --reset session.jsonl # trust the next complete tools/list
在临时 CI 环境中,状态目录初始为空,因此首次运行仅记录基线且不报告漂移。
基线必须在各次运行之间持续存在,以便后续运行可据此进行验证。请将 --baseline 指向已检入或缓存的目录,或将 MCPSNOOP_HOME 设置为持久化路径。```bash
mcpsnoop check --fail-on drift --baseline .mcpsnoop/baselines session.jsonl
`drift` 对于 `check` 是可选启用的;默认的 `error,invalid,warn` 门控保持不变。
### 标记已弃用的协议特性
2026-07-28 修订版弃用了 Roots、Sampling 和 Logging。它们至少还能
再工作一年,因此 mcpsnoop 会将它们标记出来,而不是视为错误。
流、能力检查器和导出都会标记它们,并且每个标记
都会指明替代方案。
这三个中的两个现在只能通过多次往返请求访问,
方法名位于服务器的 `inputRequests` 映射中,而不是在
帧本身上。这些也会被标记,因此迁移到新模式的服务器
不会悄然停止报告。```bash
mcpsnoop check --fail-on deprecated session.jsonl
与 drift 一样,deprecated 是选择加入的。默认运行会报告计数并保持
绿色,因此使用仍然合法的已弃用功能的会话永远不会自行让 CI
变红。
标记客户端处理不佳的 schema 构造
服务器可以完全有效,但仍然难以让代理使用。客户端 对 JSON Schema 的实际支持程度各不相同,而模型不断错误调用的 工具,往往是其 schema 所要求的超出了客户端 所能交付的范围。
使用 s 打开的工具摘要有一个 SCHEMA 列,标明每个已公布
工具 schema 最值得注意的特点;当有不止一种时,
末尾带一个 +。
| 显示 | 含义 |
|---|---|
no root | inputSchema 缺失、不是 JSON 对象,或根类型不是 "object" |
dialect | $schema 指定的方言不是该修订版默认使用的 2020-12 |
ext ref | $ref 指向文档外部,规范也警告实现者不要盲目跟随这种情况 |
oneOf, anyOf, allOf, not | 组合关键字,各客户端处理方式不一致 |
ref | $ref 指向同一文档内部 |
untyped | 属性未声明类型,也没有其他方式说明其接受什么 |
除第一项外,其余都是观察而非定性结论。使用 oneOf 的
schema 并非错误,只是很可能被不同客户端以不同方式解读,而且
schema 可以声明它喜欢的任何方言。no root 是例外:Tool
定义要求 inputSchema 并将其根类型固定为 "object",因此
验证列表的客户端会直接拒绝该工具,它永远无法变为可调用状态,
网络上没有任何信息说明原因。no root 因这个原因排在列首,
而经过 mcpsnoop 自身脱敏处理清除的 schema 永远不会被报告,
因为不可读的 schema 并不等于错误的 schema。
这种区分决定了 check 如何处理它们。no root 是 tools/list
帧上的警告,因此即使不带任何标志,它也会让默认的
error,invalid,warn 门槛无法通过,而这正是重点:发布不可用工具的服务器
会正常应答每一次握手,只是永远不会收到 tools/call。这些观察
被计为 schema_findings,并在 schema findings: 下报告,只有当你把 schema 添加到 --fail-on 时才会
导致运行失败。两者都会进入 --format junit 和 --format sarif,
而 export 会在 summary.definitions.per_tool[].findings 下携带
按工具划分的列表。```bash
mcpsnoop check session.jsonl # a non-object root already fails this
mcpsnoop check --fail-on schema session.jsonl # and now so do the observations
该列带有警告色,而绝不会是 ERR 列的红色,并且
mcpsnoop 仍然不会改变它转发的流量。
不会解析或获取任何内容。外部 `$ref` 仅凭其形式即可被识别,
它指向的 schema 永远不会被读取。
### 查看服务器在上下文中让你付出的代价
工具定义会在每次对话中进入模型的上下文,工具
结果则会在每次调用时进入。工具摘要(`s`)从你实际捕获的
会话中衡量这两者。
`definitions` 行是固定成本:在发出任何一次调用之前,该服务器的 `tools/list`
所占用的体量。`DEF` 列按工具细分,`RESULT` 则是每个工具的答案迄今为止
所花费的代价。表格保持按错误和延迟排序,因此查看 `DEF`
可找出昂贵的定义;导出按最重优先列出它们。表格下方的一行
会指出单个最重的结果,而总计会将其隐藏。
定义数字是去除了无关紧要空白的 JSON,因此一个美化输出其 `tools/list`
的服务器不会被计为比不这样做的服务器更昂贵,并且同一服务器
在不同捕获中的测量结果相同。`RESULT` 是到达时的字节数:
结果是一次性负载,而非值得规范化的契约。```bash
mcpsnoop export -T json | jq '.summary.definitions'
导出包含同样的数值,按工具划分,并拆分为描述字节和 schema 字节,因此臃肿的描述和臃肿的 schema 仍可分离,且任一都能跨抓取追踪。mcpsnoop diff 会告诉你两次会话之间描述或 schema 是否发生了变化;该变化的大小就保存在导出中。
这些是字节,而非 token。token 数量取决于具体模型,要衡量它就得附带一个分词器,并选定采用谁的。字节是精确的,你可以套用自己的换算比例。未完成的 tools/list 会将其已看到的内容作为下限如实上报,并明确说明如此,而不会把部分合计冒充为总数。
检测篡改服务器状态的客户端
在多往返模式下,服务器会向客户端提供一个不透明的 requestState,客户端在重试时必须原封不动地将其回传。服务器被要求将该值视为攻击者可控的输入,因为篡改它的客户端可能试图改变服务器行为或绕过授权检查。
mcpsnoop 处于通信管道之中,能看到该值出去又回来,因此能够指出契约何时被破坏。它可能以三种方式被破坏,每一种都会在重试时作为协议警告上报。
| Reported | Means |
|---|---|
MRTR retry changed requestState | 客户端回传的内容与服务器发放的内容不一致 |
MRTR retry is missing requestState | 服务器发放了该值,但重试时将其遗漏 |
MRTR retry invented requestState | 重试携带了服务器从未发放的值 |
这些是客户端造成的协议违规,而非我们观察到的现象,因此它们经由普通的警告信号上报,且默认的 check 运行会在出现任意一种时判定失败。这是有意为之。篡改服务器状态的客户端,完全值得让构建为之中止。
该值本身永远不会被显示或记录,也没有任何东西会解码或解析它。它可能是一个携带主体(principal)和 token 的加密数据块,而比对不透明字节就是整个检查的全部。
有一种情况无法覆盖。当服务器返回 requestState 但没有 inputRequests 时,被篡改的重试匹配不到任何内容,也不会返回任何键,因此没有任何东西能把它与原请求关联起来,它会被当作一次无关调用,而非违规。
从另一台机器监控
将抓包保持在流量产生的机器本地,网络跳转则使用 SSH,这样 mcpsnoop 就永远不需要自己的远程传输通道。
实时视图
在工作站上运行 TUI,并将远程机器的 mcpsnoop socket 转发回工作站。实时隧道使用 SSH Unix-socket 转发,因此两端都必须运行 Linux 或 macOS。在 Windows 上,请使用下面介绍的事后日志拷贝方式。```bash
on your workstation, start the TUI
mcpsnoop
create the remote socket directory once
ssh remote-user@remote-host 'mkdir -p ~/.local/state/mcpsnoop'
print the tunnel command, then run the printed ssh -R line
mcpsnoop remote remote-user@remote-host
on the remote host, wrap your server as usual
mcpsnoop -- node build/index.js
套接字位于远程端的状态目录下,解析顺序为 `MCPSNOOP_HOME`,
否则为 `XDG_STATE_HOME/mcpsnoop`,再否则为 `~/.local/state/mcpsnoop`。默认情况下,mcpsnoop
会根据你的 `user@host` 假定 Linux 主目录为 `/home/<user>`,并在回退到该假设时
向 stderr 打印一条提醒。如果远程端解析到其他位置,
请指定那个非默认的项。```bash
# a non-Linux or custom home, macOS is /Users/<user> and root is /root
mcpsnoop remote --remote-home /Users/remote-user remote-user@remote-host
# an explicit MCPSNOOP_HOME on the remote
mcpsnoop remote --remote-mcpsnoop-home /srv/mcpsnoop remote-user@remote-host
# an explicit XDG_STATE_HOME on the remote
mcpsnoop remote --remote-xdg-state-home /var/lib/state remote-user@remote-host
事后分析
通过 SSH 将远程会话直接流式传输到 TUI,无需本地副本。```bash ssh remote-user@remote-host 'cat ~/.local/state/mcpsnoop/sessions/session.jsonl' | mcpsnoop open -
若想改为保留本地副本,请使用 scp 将日志复制到你的 sessions 目录中,然后按正常方式
运行 TUI。```bash
# copy the remote logs into your local sessions directory
mkdir -p ~/.local/state/mcpsnoop/sessions
scp remote-user@remote-host:'~/.local/state/mcpsnoop/sessions/*.jsonl' \
~/.local/state/mcpsnoop/sessions/
# open the TUI, it backfills the copied sessions
mcpsnoop
安全性
mcpsnoop 运行你所包装的服务器命令,因此只包装你信任的服务器,并 在容器中运行不受信任的服务器。它绝不会执行任何未放入 客户端配置的内容。
捕获的帧可能包含提示词、工具参数、凭据和工具 结果。如果负载可能携带机密信息,请选择启用脱敏,以清理 所观察到的流量副本,同时代理字节仍原样通过。
基于键的脱敏会替换匹配的 JSON 对象键下的完整值,并且
同一键集以尽力而为的方式应用于包装的服务器命令行
参数,因此 --api-key=sk-x 和 --token sk-x 会在
--redact-secrets 下被清理。带有机密但没有可识别标志
名称的参数无法被检测到。
基于路径的脱敏只替换由 JSONPath 表达式选中的值,
这在某个常见键名在一个位置敏感但在另一个位置安全时非常有用。
重复使用 --redact-path 可清理多个位置。
基于值的脱敏将正则表达式应用于观察到的字符串值、 stderr 文本和非 JSON 文本帧。
这三种方式都是尽力而为。正则表达式可能遗漏机密、过度匹配无害文本,或 无法识别转换或编码后的值。
脱敏永远不会变成指控。每一项将观察到的内容
与另一项进行比较的检查,将路由头部与正文比较,将 Mcp-Param 值
与其镜像的参数比较,将工具的 schema 与修订版
对其的要求进行比较,都知道 mcpsnoop 何时是重写字节的一方,并
保持沉默,而不是因用户自己的隐私设置而报告服务器。
工具定义漂移是例外,而且是有意为之,因为启用脱敏
会改变记录的内容,从而改变基线所持有的内容。请参阅
检测工具定义漂移。```bash
built-in preset of common secret keys
mcpsnoop --redact-secrets -- node build/index.js
or name your own keys
mcpsnoop --redact-key token,api_key,password -- node build/index.js
scrub one location without redacting every field named password
mcpsnoop --redact-path '$.params.arguments.password' -- node build/index.js
wildcards scrub every matching array element
mcpsnoop --redact-path '$.params.arguments.accounts[*].password' -- node build/index.js
scrub obvious token-shaped values outside known keys
mcpsnoop --redact-value 'sk-[A-Za-z0-9]+' -- node build/index.js
combine the layers in http mode
mcpsnoop http --target http://localhost:3000/mcp --redact-secrets --redact-value 'Bearer\s+\S+'
对于远程工作流,请使用 SSH 隧道或 SSH 文件传输,以便传输认证、加密、主机验证、密钥轮换和审计策略保留在您现有的 SSH 设置中。
## 贡献
欢迎提交 Issue 和拉取请求。详情请参阅 [CONTRIBUTING.md](https://github.com/kerlenton/mcpsnoop/blob/HEAD/CONTRIBUTING.md)。
## 许可证
[MIT](https://github.com/kerlenton/mcpsnoop/blob/HEAD/LICENSE)