面向 MCP 的 Wireshark。 一个透明代理,可在终端中实时显示你的 AI 客户端与 MCP 服务器之间的每一次真实工具调用。
官方 MCP Inspector 以自身客户端身份连接,因此它永远看不到你的客户端(Cursor、Claude Code、Codex)实际发送给服务器的内容。而且,任何等待请求到达的工具都无法显示模型从未发起过的调用,或使用了错误参数的调用。当工具被静默跳过、能力不匹配,或调用只是挂起时,你只能翻查日志并不断猜测。
mcpsnoop 取而代之,置身于真实数据路径之中。 用它包装你的服务器命令,即可在真实客户端与服务器通信时,实时观察每一个 JSON-RPC 帧。
本页面同时也是 mcpsnoop GitHub Action 的列表页,因此这里涵盖了其全部内容。它会检查捕获的会话,将每一条发现作为代码扫描警报提交,并根据你所设定的门禁条件使任务失败。```yaml permissions: security-events: write contents: read
steps:
固定你想要的版本。最新版本在
[发布页面](https://github.com/kerlenton/mcpsnoop/releases)上。所有输入、
退出码的含义,以及如何在不使用该 action 的情况下进行配置,都记录在
下方的 [GitHub Action](#the-github-action) 部分。
## 快速开始
无需任何配置,立即查看效果。```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
会恢复该文件,并在没有任何服务器仍处于包装状态时删除备份。
执行任一操作后请重启 Claude Desktop,因为 MCP 服务器只在
启动时启动一次。
然后像往常一样使用你的客户端并打开 UI。```bash mcpsnoop
无需记住任何标志、套接字路径或启动顺序。垫片与界面会自动找到彼此,界面会从磁盘回填过往会话。
对于可流式 HTTP 服务器,可将 mcpsnoop 作为反向代理运行。```bash
mcpsnoop http --target http://localhost:3000/mcp --listen :7000
每条响应的 HTTP 状态都会显示在流中,因此即使响应本身不携带任何 JSON-RPC 消息,它仍然是一个可见的帧,而不是什么都没有:401 质询、因 Origin 被拒绝而返回的 403、确认通知的 202,以及目标完全无法访问时的 502。401 的 WWW-Authenticate 头会原样保留并显示在检查器中,因为它指明了认证方案以及下一步要访问的资源元数据。在 TUI 中可使用 status:401 按状态过滤,或使用 status:err 按任何失败过滤。4xx 或 5xx 均计为错误,因此默认的 mcpsnoop check 运行会因这些状态而失败。
没有自己的服务器?可针对已发布的测试服务器实际试用,由你自己的客户端驱动。若要在会话结束后进行检查,请参阅从日志中回顾过往会话。
如果你在项目中复用相同的 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>` | 代理流式 HTTP 服务器 |
| `mcpsnoop export` | 将会话渲染为 json、html、text、har 或 otlp |
| `mcpsnoop check` | 在错误、无效帧、警告、路由不匹配、挂起调用、延迟结果或延迟预算上使 CI 失败 |
| `mcpsnoop baseline` | 检查、接受或重置受信任的工具定义 |
| `mcpsnoop diff` | 比较两次捕获会话中的工具和调用 |
| `mcpsnoop open` | 在 TUI 中打开已保存的会话 |
| `mcpsnoop inventory` | 列出本机上所有通过 mcpsnoop 运行过的服务器 |
| `mcpsnoop stats` | 将每个存储的捕获折叠为每个服务器和工具一行 |
| `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 帧 | 否 | 是 |
| 在批准后检测工具定义漂移 | 否 | 是 |
| 交互式终端 UI | 否 | 是 |
| 零配置,无标志或顺序要求 | 否 | 是 |
| 能力检查器 | 部分 | 是 |
| 重放捕获的调用 | 否 | 是,支持 stdio 和 HTTP |
| 会话导出(json / html / text / otlp) | 否 | 是 |
| 单一二进制,无运行时依赖 | 否 | 是 |
## 安装
### npm
无需 Go 工具链。大多数 MCP 服务器是用 Node 或 Python 编写的,因此这是最快的入门方式。```bash
npx mcpsnoop -- node build/index.js
npm 包本身不包含任何代码。六个平台包各自携带一个构建版本,npm 会安装与你机器匹配的那一个,因此安装时无需下载任何内容,也无需在代理中解除封锁。若想保留它而不是每次运行时都重新获取,可使用 npm i -g mcpsnoop。
go install github.com/kerlenton/mcpsnoop/cmd/mcpsnoop@latest
### Homebrew```bash
brew install mcpsnoop
每个平台的预编译二进制文件都在 Releases 页面上。
mcpsnoop 附带 bash、zsh、fish 和 PowerShell 的补全。运行
mcpsnoop completion <shell> --help 查看设置步骤,其中涵盖启用
补全以及适用于你操作系统的安装路径。
mcpsnoop 在一个二进制文件中扮演两个角色。mcpsnoop -- <server> 是你的客户端启动的透明
垫片,逐字节转发数据,同时将每个帧的副本发送到中枢。不带参数的 mcpsnoop 就是该中枢及其实时 TUI。它们
通过一个众所周知的套接字和磁盘日志配对,因此两者都不需要先启动。
中枢默认加载最新的 100 个已保存会话,在保持启动工作
有界的同时不删除较旧的跟踪记录。使用 mcpsnoop --history-limit N 选择
其他限制,或使用 mcpsnoop --history-limit 0 加载完整历史记录。较旧的
会话仍可通过 mcpsnoop open <session-id> 和
mcpsnoop export <session-id> 访问。
历史记录限制约束了加载的会话数量。在会话内部,实时 TUI 受到双重约束,因为一个持续监视话痨服务器的中枢否则会不断增长直到被终止。它最多保留 64 MiB 的帧体,最先释放 最旧的,并且最多保留 200,000 个帧,超过该数量后完全丢弃最旧的。 第一个约束是大负载捕获会遇到的情况,第二个约束则是 长串小通知流会遇到的情况。
这两个约束都不会改变答案。帧体已被释放的帧会保留其行、
其判定及其在时间线中的位置,其检查器会显示帧体已消失
而不是显示空帧。被完全丢弃的帧会先将其工具调用的统计信息
计入运行总计,因此工具摘要以及服务器在上下文中消耗你的内容
会描述会话所做的每一次调用,而不仅仅是最近的调用。流页脚显示磁盘上还有多少较旧的帧
,而 r 会拒绝一个其参数不再持有的帧,而不是
重放其他内容。
mcpsnoop open <session-id> 读取日志并完整保留所有内容,而从
TUI 导出也会读取日志,因此两者都不受约束。check、export 和
diff 有意构建一个无界存储,因为一个在大型捕获上少报的闸门
比一个使用内存的闸门更糟糕。
历史记录限制约束了加载的内容。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` / `R` | 重放 / 编辑并重放 |
| `g` / `G` | 顶部 / 底部 | | `c` | 能力 |
| `ctrl-f` / `ctrl-b` | 翻页 | | `s` | 工具摘要 |
| `p` | 暂停 | | `y` | 复制 |
| `shift`+`<key>` | 按列排序 | | `e` | 导出 |
| `ctrl-d` | 删除会话 | | `f` | 跟随 |
| `?` | 帮助 | | | |
在应用中按 `?` 可查看完整列表。
## 过滤流
在会话中按 `/`,并组合以空格分隔的标记,采用 AND 逻辑。纯文本匹配方法、工具、ID 和负载。
| 标记 | 过滤依据 | 示例 |
|---|---|---|
| `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 跟踪上下文时将其与调用方跟踪连接,否则每个会话对应一个跟踪 |
MCP 并非 HTTP,因此 HAR 条目中的 URL、状态码和计时是对每次调用的有意
映射,而非线路传输记录。
对于 OTLP,请求的 `_meta.traceparent` 提供该调用的跟踪和父
span ID,而 `_meta.tracestate` 随 span 一并传递。当 traceparent 缺失或无效时,
mcpsnoop 保留会话派生的跟踪且不携带任何状态。
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 可写入标准输出,省略会话则取最新的,或传入 - 从标准输入读取 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` 会拒绝输出与输入同名文件的操作,并通过先写入临时文件再重命名到位的方式完成写入,因此运行失败时,之前的文件仍保持完整。
工具在 `tools/list` 结果中声明的 `inputSchema` 和 `outputSchema`,不会被 `--redact-key` 和 `--redact-secrets` 处理,原因有三。
- schema 中的名称是类型声明,而非值。
- 无论哪种方式,该名称本身都会保留在日志中。
- 清除名为 `token` 的属性下的子 schema,会连同工具自身的检查一并移除。
豁免仅限该位置,因此恰好名为 `inputSchema` 的参数会像其他参数一样被清除,并且处理止步于 `default`、`const`、`examples` 和 `enum`,这些字段承载的是数据而非结构。请使用 `--redact-path` 来指定 schema 内部的某个内容,或使用 `--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 traces 端点来发送 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 中针对回归进行门禁。当 after 会话出现以下情况时,它会以非零状态退出:
- 移除某个工具
- 更改工具的描述、标题、输入模式、输出模式或注解
- 存在状态变差的调用
- 出现变慢
图标变化不会触发退出,因为它只改变工具的外观而不改变其功能。改进情况(即新增工具、修复调用和加速)仍会以零状态退出。
## 在 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。
每个信号都会被计数,无论其是否用于门控,因此一次运行会先报告其发现的内容, 再由你决定哪些应导致失败。``` 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
退出码说明发生了两种情况中的哪一种,而 CI 包装器需要区分这两者。1 表示检查已运行且某项内容未通过门禁,因此发现的问题是真实存在的,值得发布。2 表示检查从未发生:路径不存在、文件不是会话日志、状态目录为空、标志无法解析。在退出码为 2 时不会向 stdout 写入任何内容,因此流水线绝不会将空报告当作判定结果上传。
除了信号计数之外,还要断言运行的结构。这些断言可以相互组合,也可以与 --fail-on 组合使用,任何失败都会以退出码 1 结束,该退出码表示检查已运行并发现了问题。
| 标志 | 失败条件 |
|---|---|
--max-duration <dur> | 一个或多个已完成的工具调用超出预算,报告其数量及最差调用 |
--expect-tool <name> | 指定的工具从未被调用(可重复) |
--forbid-tool <name> | 指定的工具被调用过(可重复) |
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 级别报告,因此报告与门禁永远不会不一致。
一条结果指向发现来源的日志,具体指向方式取决于日志的读取位置。
file:// URI。仅当该路径是所分析提交中的文件时,警报才会附带其周围行渲染,因此工作流生成到 artifacts/ 的捕获会打开一条携带消息、规则和行号但没有源码视图的警报。提交一份希望完整渲染的捕获是获得完整视图的唯一方式。
代码扫描会拒绝运行中结果超过 25,000 条的文件,并且只显示其接受的最高 5,000 条,因此报告上限为 5,000:先是门禁失败所针对的发现,然后是 mcpsnoop/report-truncated 结果,说明遗漏了多少条。文本和 junit 格式保持完整。
以下内容就是该操作为你所做的一切。它安装 mcpsnoop、检查捕获、将发现归档到 Security 选项卡,并根据你设置的门禁使作业失败。```yaml permissions: security-events: write contents: read
steps:
固定一个发布版本,选哪个都行。最新版在[发布页面](https://github.com/kerlenton/mcpsnoop/releases)上。刻意没有浮动`v1`。固定的发布版本也就是该操作安装的二进制版本,因此两者永远不会不一致,也不存在会过时的默认版本。
| 输入 | |
|---|---|
| `session` | 要检查的`.jsonl`捕获文件,相对于仓库根目录。必填 |
| `fail-on` | 同`--fail-on`,默认值与CLI默认值一致 |
| `args` | 其他`check`标志,按命令行方式加引号。拒绝`--format`,因为该操作会读取报告 |
| `upload-sarif` | 将报告发送到代码扫描。`true` |
| `category` | 代码扫描命名空间。`mcpsnoop`。矩阵中的每条分支请使用不同值,否则各分支会互相覆盖 |
| `fail-on-findings` | 发现问题时让任务失败。`true`。设为`false`可归档告警,让代码扫描的必需检查来决定 |
| `version` | 要安装的mcpsnoop版本。默认为你固定的发布版本 |
| `install` | 当mcpsnoop已在PATH中时设为`false`,这是在没有为其构建发布版本的平台上的接入方式 |
输出为`outcome`、`sarif`和`exit-code`。`outcome`取值为`passed`、`findings`或`error`,第三种值得单独处理。它表示什么都没检查到,这与什么都没发现不是一回事。**无论`fail-on-findings`怎么设置,无法进行检查的运行都会让任务失败**,因为一个验证了零内容却变绿的流水线,比一个失败的流水线更糟糕。
该任务需要`security-events: write`权限,否则上传会返回403。在没有代码扫描的仓库中,请设置`upload-sarif: false`。
### 或者自行接线
该操作只有四个步骤,没有任何魔法。手动操作需要同样的细心。上传必须在有报告的那些运行上执行,也就是退出码为0或1的运行,而不是退出码为2的运行;而让任务失败的步骤必须在其之后执行,否则发现的问题永远无法到达它们本该存在的那个标签页。```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
id: check
run: |
code=0
mcpsnoop check --format sarif artifacts/session.jsonl > mcpsnoop.sarif || code=$?
echo "exit-code=$code" >> "$GITHUB_OUTPUT"
# 2 means the check never happened, so there is no report to publish and
# nothing was verified. Stop here rather than uploading an empty file.
[ "$code" -le 1 ] || exit 1
- name: Upload mcpsnoop SARIF report
if: ${{ !cancelled() }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: mcpsnoop.sarif
category: mcpsnoop
- name: Fail on findings
# Separate, and after the upload, so the findings reach the Security tab on
# exactly the runs that have some.
if: ${{ !cancelled() && steps.check.outputs.exit-code == '1' }}
run: exit 1
在可流式 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 哨兵、布尔值以及数值等价的安全整数均被处理,不会产生字符串比较的误报。未知的参数头以及没有匹配工具定义的会话仅作观察记录。基于键和值的脱敏会在捕获的参数头值到达接收端之前应用,而 mcpsnoop 自行擦除的值永远不会被报告为不一致。
上述路由头是帧携带的唯一头,因此可流式 HTTP 传输其余强制要求的头没有到达任何可以检查它们的地方。Content-Type 是最尖锐的情况。响应侧已经读取它来区分 SSE 流和 JSON 请求体,然后将其丢弃。
HTTP 帧现在携带传输层规定规则的头,其中两条规则是可检查的。
| 规则 | 报告方式 |
|---|---|
客户端必须发送同时列出 application/json 和 text/event-stream 的 Accept |
这两句话在 2025-11-25 和 2026-07-28 中表述相同,因此与漂移和扩展检查不同,它们不需要修订版门控。Origin 也会被记录,因为服务器必须验证它,并且在无效时必须回答 403,但 mcpsnoop 无法知道您允许的来源,因此它显示该值而非进行判断。
通配符会被计算在内。发送 */* 的客户端已提供两种类型,永远不会被报告,Content-Type 上的 charset 参数会被忽略。在 mcpsnoop 记录这些头之前捕获的日志保持静默,而不是报告其中每一帧都缺少某个未被记录的头的帧,而 stdio 根本不会有这些头。
Authorization 被有意不捕获。将挑战转化为令牌事实本身就是一个问题,而将 bearer 令牌写入磁盘并不是解决方案。Mcp-Session-Id 和 Last-Event-ID 也不会被捕获。2026-07-28 修订版移除了这两者,并告诉服务器忽略它们,因此没有剩余规则需要检查。
为某个服务器标签观察到的第一个完整 tools/list 成为其可信基线。后续会话逐字段比较该基线:
新增或移除的工具也会被比较,这是集合比较而非字段比较。
注解最为重要,因为一个以 readOnlyHint 批准的工具后来声明自己具有破坏性,正是此检查存在的“抽地毯”场景,而规范告诉客户端将注解视为不可信。标题和图标被跟踪是因为它们是用户看到的内容,且规范将工具的 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 环境中,状态目录初始为空,因此一次运行没有可对比的内容,会记录基线而不是进行验证。**一次要求漂移即失败、但最终未验证任何内容的运行不会通过**,并会指出需要持久化的目录。这是唯一一种记录基线被视为失败的情况。若 `--fail-on` 中未包含 `drift`,记录基线属于常规操作,不会改变退出码。
因此,基线必须在多次运行之间得以保留,漂移门控才有意义。将 `--baseline` 指向一个已检入或缓存的目录,或将 `MCPSNOOP_HOME` 设置为持久化路径。```
recorded first-seen tool baseline (trusted, not verified)
check failed: drift
I need the actual content of chunk 53 to translate it. Please provide the Markdown text you want translated.```bash mcpsnoop check --fail-on drift --baseline .mcpsnoop/baselines session.jsonl
`drift` 对 `check` 是可选启用的。默认的 `error,invalid,warn` 门控保持不变。
### 捕获双方均未协商的功能
SEP-2133 将可选功能从核心协议中移出并放入扩展中,通过双方能力中的 `extensions` 映射进行通告。Tasks 就是其中之一,因此在 2026-07-28,当对方表示支持 Tasks 时,返回任务句柄的 `tasks/get`、`notifications/tasks` 或 `tools/call` 才有意义。
当对方未表示支持时,规范是明确的:支持方必须回退到核心行为或拒绝该请求。否则,功能看似已接通却悄然失效,读者在若干帧后得到的反而是 `-32601` 或 `-32021`,或是一个永不推进的任务。mcpsnoop 会在触及该扩展的帧上发出警告,并指出哪一方从未通告该功能。```
tool "slow" answered with a task handle uses the io.modelcontextprotocol/tasks
extension, which the client never advertised
这是一个 warn,因此默认的 check 运行会因其而失败。只要捕获无法显示协商结果——即握手之后开始的捕获,或你自己的编辑(redaction)清除了其能力信息的捕获——以及在 2026-07-28 之前的修订版本中(此时 tasks/* 属于核心协议,使用它们是正确的),它就会保持静默。
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 永远不会被读取。
r 会针对实时服务器重新发出捕获的调用。对于 stdio 捕获,命令记录在日志中,因此 mcpsnoop 会启动一个隔离副本并将请求发送到该副本。HTTP 捕获没有可启动的命令,并且它记录到的端点会被剥离其用户信息和所有查询值,因此它仅指明服务器名称,而并非可拨打的地址。
因此,由您指定重放的目标位置,而 mcpsnoop 绝不会因为有人按下了某个键就去拨通生产端点。```bash
mcpsnoop open --replay-target https://api.example.com/mcp session.jsonl
mcpsnoop open --replay-target https://api.example.com/mcp
--replay-header 'Authorization: Bearer sk-…' session.jsonl
如果没有 `--replay-target`,HTTP 会话会直接说明这一点,而不是提供一个无法生效的密钥。有了该参数后,`r` 仍然会在会话首次发送前询问,这与录制命令在执行前需要确认的方式相同。
凭据通过 `--replay-header` 到达服务器,除此之外不会通过其他途径。mcpsnoop 不会记录任何 `Authorization` 头,也不会重放任何此类头,因此重放过程中没有可泄露的捕获内容。
重放的 POST 携带的是传输层强制要求的头,而仅发送裸捕获正文的 POST 则不会携带这些头:`MCP-Protocol-Version`、同时列出 `application/json` 和 `text/event-stream` 的 `Accept`、`Mcp-Method`、规范要求时的 `Mcp-Name`,以及所有捕获到的 `Mcp-Param-*`。这些头会从捕获中逐字重发,包括 base64 哨兵值在内,因此它们不可能像重新推导那样与正文产生不一致。唯一不会被复制的头是协议版本,因为重放的正文声明了 mcpsnoop 所支持的修订版本,而该头必须与正文保持一致。
`Mcp-Name` 是根据正在发送的正文推导出来的,而不是直接复制,因为规范要求它来源于 `params.name` 或 `params.uri`,并规定服务器必须拒绝与正文不一致的头,否则重命名工具的编辑操作就会发送旧名称。`Mcp-Param-*` 头镜像了捕获的参数,因此经过编辑的重放不会发送其中任何一个,而不会对他人重写过的正文做出断言。捕获只能设置这一系列的头。日志是人们在手中传递的文件,如果允许它指定任意头,就可能覆盖强制性的头,或添加一个无人提供的凭据。
被脱敏规则清除的 `Mcp-Param-*` 会以原因说明的方式停止重放。发送占位符会把 mcpsnoop 自身的字节发送到实时服务器上,仿佛用户亲自输入了这些内容。
重定向会被拒绝而不是跟随。地址是你指定并确认过的,而跟随 307 会把这一选择交给远端,重新发送正文,并且在仅更改端口的跳转中,还会连同凭据一起重发。mcpsnoop 会报告服务器希望将请求发送到的位置,并让你决定是否改为指定该地址。
以单个 JSON 对象到达的应答和以事件流到达的应答都会被读取,失败会以名称而非编号来报告:
- 401 会报告服务器要求的认证方案
- `-32020` 会报告服务器反对的具体内容
- 非 JSON-RPC 的 400 或 404 会说明该地址不是此修订版本的 Streamable HTTP 端点
### 区分服务器的延迟与用户的延迟
在多轮往返请求下,一次工具调用会对应多个请求,而用户回答某个提示所花费的秒数就包含在该时间跨度内。这是有意为之,因为该间隔通常是你最想看到的,但这意味着一个数字无法同时回答两个问题。
在 `book_flight` 链中,服务器工作了 1.2 秒而用户耗时 37 秒时,`check --max-duration 5s` 会将 38.2 秒归咎于该工具。它仍然会这样做,因为改变该标志的含义会放宽所有已设置它的流水线。两个同级的选项则分别说明它们所测量的内容。```bash
mcpsnoop check --max-server-duration 1s session.jsonl # the server's share alone
mcpsnoop check --max-round-trips 2 session.jsonl # how chatty a tool is
# 克隆仓库
git clone https://github.com/yourusername/yourproject.git
# 进入项目目录
cd yourproject
# 安装依赖
pip install -r requirements.txt
python main.py --help
本项目采用 MIT 许可证。有关详细信息,请参阅 LICENSE 文件。``` assertion failed: 1 tool call exceeded the 1s server budget (worst: tool "book_flight" held for 1.2s) assertion failed: 1 tool call exceeded the 2 round trip budget (worst: tool "book_flight" took 3)
两者默认均处于关闭状态,因此默认的 `check` 运行不受影响,且两者都从帧时间戳和 mcpsnoop 已推断出的链接中读取,因此都不会臆测意图。
在 TUI 中按 `i` 查看明细,或读取 json、text 和 html 导出中的 `interactions`。每个条目代表一次逻辑操作,包含其往返次数、总耗时、服务器持有它的时间占比以及它在客户端等待的时间占比,另有一行按跳(hop)列出每个应答所请求的内容。按工具汇总中新增了 `TRIPS` 列,因此无需打开任何内容即可看出某个工具是否频繁通信。
`export --format har` 将服务器的份额放入 `wait`,其余放入 `blocked`,这正是该字段的用途,因此查看器不会再去绘制一段从未发生过的 38 秒服务器等待。
计数和两个份额是在帧到达时累积的,而非在您查询时才推导,因为实时存储会释放旧帧以保持在预算内,而推导出的答案会悄然变成一段窗口而非一条链。按跳明细从仍保留的帧中读取,并且当它只是其中一部分时会明确说明。`ServerTime + ClientTurnaround` 在构造上即等于总耗时,而非依赖任何人去信任的算术。
`--max-round-trips` 会判定一条仍在运行的链,因为其中已发出的每个请求都是可计数的,而服务器一再询问所产生的正是那种永远无人完成的操作。`--max-server-duration` 会等待一个结束,这正是 `--max-duration` 已适用的规则,因为一个仍处于打开状态的操作没有可供判定的延迟。
mcpsnoop 无法关联的操作会保留为独立的单跳条目。`matchRetry` 有意拒绝模糊的关联,而此视图不会填补这一空缺。
仅发出一次请求的操作不携带按跳明细,因为单跳会逐字重复上方的总计。一条链会为每个请求报告一跳,并且当存储不再保留每一帧,或当工作脱离构成一跳的请求与应答对(例如任务句柄的情况)时,会明确说明。
### 查看服务器向您的用户询问了什么
Elicitation 是 MCP 中唯一由人向服务器输入数据的路径,而在 MRTR 下,问题与答案不再是一次交换的两半。问题埋藏在 `InputRequiredResult` 中,答案则通过不同 id 在重试时的 `inputResponses` 内返回,而将它们联系在一起的唯一纽带正是 mcpsnoop 已推断出的链接。
没有这种配对,一次被拒绝的密码请求就会显示为普通的工具错误。```
tools/call login_legacy [form] creds: decline after 3s
password string
在 TUI 中按 l,或阅读 json、text 和 html 导出中的 elicitations。
每一行都列出了问题所打断的操作、模式、消息、
被询问的内容、用户做了什么以及他们花了多长时间。一个从未被任何重试回答的问题
显示为 pending,MRTR 将其视为普通结果而非错误,因为规范要求服务器
不要假设客户端一定会重试。
表单行列出 requestedSchema 属性名及其声明的类型。一个
其子模式被编辑规则替换的属性显示为未知类型而非占位符,
因为占位符不是服务器声明的内容。URL 行完整携带地址,规范要求客户端
在同意前显示该地址,并单独列出主机名,规范称应将其突出显示以防范
子域欺骗。
账本从不携带已提交的值。用户输入的内容保留在 捕获中供需要的人使用,而将其排除在专为导出和粘贴而构建的摘要表面之外, 正是使其完全脱离编辑故事的原因。这在 url 模式中最为重要, 因为规范有意在那里放置凭据。
一次重试只回答其发起的那一轮,而不回答其他轮。MRTR 告诉服务器, 当客户端省略部分被询问的内容时,应在新一轮中再次询问, 因此较早一轮中一个未回答的键与一个已回答的键并存属于普通流量, 而未回答的那一半保持 pending,而不是借用后一轮的答案。
一条被记录的问题是有界的。消息、url 和字段列表在会话期间 被保留,位于释放主体的帧预算之外,因此服务器无法让一个问题任意昂贵。 这些限制远高于任何真实问题,而被截断的消息会说明其已被截断。
这里没有任何内容发出警告,也没有任何内容改变 check 的退出码。账本
记录发生了什么。它不对其作出评判。
check 读取一个会话,而 diff 恰好读取两个,因此一个偶尔失败的工具
会一直不可见,直到有人手动打开捕获文件。在十六次捕获中,
某个服务器的 run_query 约有四分之一的时间返回 isError,check 如实报告
最新的一次为干净。```bash
mcpsnoop stats
mcpsnoop stats --since 7d --label prod
mcpsnoop stats --limit 20 --format json
请提供需要翻译的 Markdown 内容。```
read 16 logs of 16 in ~/.local/state/mcpsnoop/sessions
SERVER TOOL CALLS ERR PROTO FAIL% SESS p50 p95 p99 DEF
flaky-demo run_query 13 3 0 23.1% 3/13 434ms 519ms 519ms 195B
docs-mirror run_query 3 1 0 33.3% 1/3 357ms 434ms 434ms 195B
docs-mirror search_docs 12 0 0 0.0% 0/3 377ms 386ms 386ms 200B
flaky-demo search_docs 52 0 0 0.0% 0/13 42ms 58ms 59ms 200B
ERR 和 PROTO 是独立的列,因为规范将它们视为独立的事物。工具回答 isError 时,报告的是模型可以据此行动并重试的内容。JSON-RPC 错误则意味着请求或服务器本身出了问题。SESS 是看到失败的会话数占调用该工具的会话数的比例,这回答了“十次中一次”的问题,而按调用次数计算的比率无法回答这一点。
行以服务器和标签的组合为键。服务器是 stdio 记录的命令和工作目录,以及 HTTP 的端点,与 inventory 使用的身份相同。单独使用任何一半都会合并本不应合并的内容:仅用标签会合并两个派生自同一名称的服务器——每当一个项目的两个检出运行同一个入口点时就会发生这种情况;仅用身份则会合并同一条命令故意以 prod 运行、又以 staging 运行的情况。这两种错误都会将两个干净分布混成一个无法描述任何一方的分布。
当两行确实共享一个标签时,SERVER 单元格会携带工作目录或端点以区分它们,而 JSON 在每一行上都带有 command、cwd 和 endpoint。从未产生歧义的名称则保持原样,因此普通表格不变。
日志中的每个会话都会被折叠,而不仅仅是第一个,因此通过拼接捕获文件生成的日志会统计全部会话。
百分位数基于原始持续时间进行汇总。中位数的中位数毫无意义。一次多轮往返操作是一次调用、一个持续时间,无论它发起了多少次请求;仍在进行中的调用会计入 CALLS,但不贡献延迟。
同一时间只驻留一个捕获文件。日志被加载、折叠进运行中的计数器,然后在下一个日志打开前被丢弃,因此包含数百个文件的目录只耗费最大的单个捕获文件的内存,而非它们的总和。
--limit 默认读取最新的一百个日志,且头部会说明读取了多少个中的多少个,因此有界的结果永远不会被误认为完整结果。stats 只报告而不干预:它不写入任何内容、不触碰基线、不打开套接字,只要遍历成功就退出 0。
人们反复提及的关于 Shadow MCP 的发现是,组织发现实际运行的 MCP 服务器数量比任何人批准的要多出数倍,因为服务器往往只是某人添加到 IDE 插件中的一个依赖。同样的事情也会在一台笔记本电脑上小规模发生,而 mcpsnoop 一直在记录答案,却从未展示过它。```bash mcpsnoop inventory mcpsnoop inventory --tools # also count what each server last advertised mcpsnoop inventory --format json # for something else to read
每行对应一台服务器,而非每个会话。行键是记录的命令和工作目录,绝不是标签,因为标签取自命令的最后一个路径元素,而 `node ~/one/build/index.js` 与 `node ~/two/build/index.js` 都会派生出 `index.js`。HTTP 会话则改为以其代理的端点为键,因为 mcpsnoop 在那里没有启动任何东西。
读取时每个日志只读取一个信封,即代理最先写入的元帧,因此即使面对包含大量大型捕获文件的目录,这一操作也保持廉价。`--tools` 是例外,它每台服务器读取一个日志,即每台服务器最近一次运行,这也是它作为标志而非列存在的原因。即便如此,读取也是有界的,因为工具清单属于会话状态,存储会在处理过程中逐步并入,因此一个上百兆的捕获文件是通过固定窗口读取的,而非整体驻留内存来生成一个整数。
当没有计数时,该行会说明三种情况中的哪一种发生了,因为无法读取的日志并不等于未通告任何内容的服务器,若用一句话同时涵盖两者,会让 mcpsnoop 陈述虚假信息。
被 `--redact` 规则重写的命令会按记录原样打印并加以标记,而非当作实际运行的命令呈现。同一台服务器运行两次,一次被脱敏、一次未脱敏,会显示为两行。mcpsnoop 无法知道占位符替换了什么,合并它们就意味着猜测被隐藏的两半是匹配的。同一台服务器在两种 `--label` 值下运行,会显示为一行并同时携带两个名称,因为键是命令而非名称。
行内没有任何内容由 mcpsnoop 写入。命令来自安装服务器的人,工作目录取自文件系统,派生标签来自命令。包含控制字符的值会加引号而非原样打印,这样名称中含换行符的目录就无法闭合其所在字段,也不会让后续行被误读为从未运行过的服务器。包含空格的参数同样会加引号,因为 `node "~/My Project/build/index.js"` 否则无法与两个参数区分开来。
遍历过程中无法并入的任何内容都会在表头中列出,而非丢弃。空日志与损坏日志分开计数,因为零字节日志是 exec 失败或无人调用的 HTTP 代理所留下的常见残留物。
输出按名称而非时间排序,这样对同一目录的两次运行会产生相同的字节,这正是其可作为基线用于日后 diff 的原因。
两处缺口是刻意设计而非疏忽所致。使用 `--trace-file` 的运行会在会话目录之外写入,因此不会出现;而 `prune` 会删除日志,所以“首次出现”最多只能回溯到磁盘上仍存在的内容。mcpsnoop 报告的是通过它在这台机器上运行过什么。它不扫描网络,不读取未被指向的客户端配置,也不做任何评判。
### 区分故障服务器与拒绝响应的工具
返回 `result.isError` 的工具是正常工作的。它查找后未发现任何内容,或拒绝了输入。返回 JSON-RPC 错误的服务器则是故障的。两者在工具摘要中都曾是一个数字,这意味着一个行为良好、如实报告领域失败的工具看起来与故障服务器毫无二致,并且排序时还排在故障服务器之前。
`ERR` 列将两者区分开来。红色代表服务器侧,即 JSON-RPC 错误或未说明原因便以失败告终的任务。警告色代表工具自身的 `isError`。同时具备两者的工具会显示合并后的计数,红色在前,并且只要存在需要解释的警告数字,表格下方就会有一行文字列出两个总数。导出数据也以 `protocol_errors` 和 `tool_errors` 携带同样的区分,与它们始终相加得到的 `errors` 总数并列。
`check --fail-on error` 保持不变,仍会对任一情况触发,因为忽略其中一种的门禁,就等于服务器可以通过返回另一种来绕过的门禁。```bash
mcpsnoop export -T json | jq '.summary.tools[] | {name, errors, protocol_errors, tool_errors}'
工具定义会在每次对话时进入模型的上下文,工具结果则会在每次调用时进入。工具摘要(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 数量取决于模型,因此
测量 token 意味着要附带一个 tokeniser 并选择采用谁的。字节是
精确的,你可以应用自己的比率。未完成的 `tools/list` 会报告它
所看到的内容作为下限并如实说明,而不是将部分总和冒充为
总数。
### 检测篡改服务器状态的客户端
在多轮往返模式下,服务器向客户端提供一个不透明的
`requestState`,客户端必须在重试时原样回显它。服务器
被告知将其视为攻击者控制的输入,因为篡改它的客户端
可能试图改变服务器行为或绕过授权检查。
mcpsnoop 位于管道中,能看到该值离开并返回,因此它可以指出
契约何时被破坏。有三种破坏方式,每种都会在重试时报告为
协议警告。
| 报告内容 | 含义 |
|---|---|
| `MRTR retry changed requestState` | 客户端返回的内容与服务器发出的不同 |
| `MRTR retry is missing requestState` | 服务器发出了一个,但重试时省略了它 |
| `MRTR retry invented requestState` | 重试携带了服务器从未发出的值 |
这些是客户端的协议违规,而非我们的观察结果,因此它们
走普通的警告信号,并且**默认的 `check` 运行会在遇到其中一个时失败**。
这是有意为之。篡改服务器状态的客户端值得让构建停止。
该值本身从不显示或记录,也没有任何东西解码或解析它。
它可能是一个携带主体和 token 的加密 blob,而比较不透明
字节就是整个检查。
有一种情况无法触及。当服务器以 `requestState` 应答且没有
`inputRequests` 时,被篡改的重试无法匹配任何内容,也不应答任何键,
因此没有剩余的东西可将其与原始请求关联,它会被解读为
无关调用而非违规。
被放弃的交换不会干扰下一个交换,也不会被永久保留。
64 个打开的交换远超任何客户端同时持有的数量,因此一个会话
持有更多就意味着持有无人会完成的交换,最旧的会被淘汰,
因为规范告诉服务器给该状态一个短暂的过期时间并在之后拒绝它。
淘汰会被计数而非静默处理。流页脚显示 `N unlinked`,导出内容
携带 `session.retired_exchanges`,因为针对已淘汰操作到达的重试
会被解读为它自己的调用,而比较计数的读者理应被告知。
淘汰一个操作也让实时存储释放它。挂起的操作有意保持
pending 状态,因此其持续时间跨越整个交换,而存储拒绝
忘记 pending 调用,因为响应可能仍在到来。一旦上限淘汰了
某个操作,就没有什么能应答它,因此持有它会让一个没有读者
能触及的调用保持存活。会话报告的内容不会改变。它仍被
计为 pending,仍被计入 `N unlinked`,因为记录占用多少内存
与记录说了什么是不同的问题。
被放弃的交换不会干扰下一个交换。MRTR 告诉服务器它们
绝不能假设客户端会重试,因此用户拒绝提示会留下一个
后续帧永远不会结算的操作。mcpsnoop 首先在 `requestState`
存在性与重试一致的操作中查找,规范使这在两个方向上都成为
规则,因此符合规范的重试仍能找到它继续的那个操作,即使
同一工具上有一个被放弃的交换就在旁边。报告上述三种违规的
检查仅在没有任何一致时运行,因此真正不符合规范的重试
仍会被点名。
## 从另一台机器观看
将捕获保持在流量发生的机器本地,并使用 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 假定 Linux 主目录为 /home/<user>(取自你的 user@host),并且每当回退到该猜测值时,都会向 stderr 打印一条提醒。如果远程解析到其他位置,请指定那一个非默认的路径。```bash
mcpsnoop remote --remote-home /Users/remote-user remote-user@remote-host
mcpsnoop remote --remote-mcpsnoop-home /srv/mcpsnoop remote-user@remote-host
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 复制到您的会话目录中,然后照常运行 TUI。```bash
mkdir -p /.local/state/mcpsnoop/sessions
scp remote-user@remote-host:'/.local/state/mcpsnoop/sessions/*.jsonl'
~/.local/state/mcpsnoop/sessions/
mcpsnoop
## 安全
mcpsnoop 会运行你所包装的服务器命令,因此请只包装你信任的服务器,并在容器中运行不受信任的服务器。它绝不会执行你未放入客户端配置中的任何内容。
对于远程工作流,请使用 SSH 隧道或 SSH 文件传输,以便传输认证、加密、主机验证、密钥轮换和审计策略都保留在你现有的 SSH 设置中。
### 对捕获内容进行脱敏
捕获的帧可能包含提示词、工具参数、凭据和工具结果。如果负载可能携带机密信息,请选择启用脱敏来擦除所观察到的跟踪副本,而代理的字节仍会原样通过。
基于键的脱敏会替换匹配的 JSON 对象键下的整个值,并且同一组键会尽力应用于被包装服务器的命令行参数,因此 `--api-key=sk-x` 和 `--token sk-x` 会在 `--redact-secrets` 下被擦除。携带机密但没有可识别标志名称的参数无法被检测到。
HTTP 端点不属于上述任何范畴,因为它不是你选择发送的负载。`--target` 是你必须传入才能运行代理的标志,因此无论你的脱敏设置如何,其 URL 都会进入会话日志。mcpsnoop 会将其记录下来,并且始终通过构造而非模式来移除用户信息、每个查询值和片段。查询键会保留,因为它们正是区分同一主机上两个端点的依据;片段会被丢弃,因为它从一开始就不会到达服务器。所记录的内容用于标识服务器,而不是可供拨打的地址。
基于路径的脱敏仅替换由 JSONPath 表达式选中的值,这在某个常见键名在一个位置敏感但在另一个位置安全时非常有用。重复使用 `--redact-path` 可擦除多个位置。
基于值的脱敏会对观察到的字符串值、stderr 文本和非 JSON 文本帧应用正则表达式。
以上三种方式均为尽力而为。正则表达式可能遗漏机密、过度匹配无害文本,或无法识别经过转换或编码的值。
脱敏绝不会变成一种指控。每一项将观察到的事物与另一事物进行比较的检查——路由头与正文、`Mcp-Param` 值与其镜像的参数、工具模式与该修订版对工具的要求——都知道何时是 mcpsnoop 重写了字节,并保持沉默,而不是因用户自身的隐私设置而报告服务器。工具定义漂移是例外,而且是有意为之,因为启用脱敏会改变所记录的内容,从而改变基线所持有的内容。请参阅[检测工具定义漂移](#detect-tool-definition-drift)。```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+'
欢迎提交 Issue 和拉取请求。详情请参阅 CONTRIBUTING.md。
| 信号 | 失败条件 |
|---|
error | 调用以 JSON-RPC 错误应答、结果标记为 isError,或任务以失败告终 |
invalid | 协议通道上的帧不是有效的 JSON-RPC,通常是服务器向标准输出记录日志 |
warn | 帧违反了 MCP 或 JSON-RPC 规范设定的预期 |
mismatch | 路由头与正文不一致、搭载在批次中,或在修订版要求时缺失 |
pending | 捕获结束时请求仍处于打开状态,导致调用方一直等待 |
late-result | 响应在其请求被取消后才到达 |
drift | 基线获批后,广告中的工具定义发生变化 |
deprecated | 规范已弃用的功能 |
incomplete | 上游丢弃的帧,这使得其他所有计数成为下限而非总数 |
schema | 广告中的模式使用了在客户端之间传输不佳的构造或方言 |
在请求上报告 warn |
响应 JSON-RPC 请求的服务器必须返回 Content-Type: application/json 或 text/event-stream | 在响应上报告 warn |