通过在MCPstdio管道上挂代理抓包,发现某客户端在字节到达服务器前就已内部失败,但表象像极了模型尝试不足。
在上一篇文章中(2026-08-18),我发布了 14 个 MCP 服务器在 agent 执行任何工作之前消耗多少上下文窗口的数据,并说下一步是 Tier 2:真实客户端、真实任务、每一帧都记录在案。这部分已经完成。九十次试验、三个服务器、两个客户端、十五个脚本任务,每个任务三次试验,全程通过 MCP stdio 管道上的代理。
值得单独发一篇文章的发现不在 token 表格里。它来自同一天更早的调试阶段,运行的是被这次测试替换掉的客户端版本:其中有一个客户端在调用内部就失败了,在一个字节都没到达服务器的情况下,从表面看完全像一个尝试很少且回答错误的模型。
87 次试验完成,3 次未完成的失败方式各不相同
矩阵是 filesystem、playwright 和 github,各五个脚本任务,每个任务每个客户端跑三次,套件版本 1.0.1。九十次试验,87 次成功。服务器分别固定在 @modelcontextprotocol/server-filesystem@2026.7.10、@playwright/mcp@0.0.79,以及 ghcr.io/github/github-mcp-server 容器(使用未打 tag 的方式启动,报告自身版本为 v1.9.0)。客户端:Claude Code 2.1.235 运行在 claude-sonnet-5 上,Gemini CLI 0.55.1 运行在 gemini-2.5-flash 上,两个模型的 ID 都从每次试验各自的客户端 JSON 中读取而非从 flag 推断。Claude Code 每次试验还会调用 claude-haiku-4-5-20251001 来处理自身的记账工作,输入 token 在一千以内。这部分从不接触 MCP 管道,也不出现在下方的任何图表中,但因为它存在于 manifest 中,所以值得知道它在那里。
三次 Gemini 失败分别是三个不同的服务器,且失败原因各不相同:FS-04 少算了一行、GH-05 在六次调用到达服务器后被自身的工具层拒绝响应,以及 PW-01 的最终消息声明任务完成但没有复述校验所需的报价。
每个单元格三次试验只能得到单元格内的计数,没有统计意义,所以这些始终是计数而非比率,参见 spec 3.3 节:Claude Code 完成了 45 次试验中的 45 次,Gemini CLI 完成了 45 次中的 42 次。87 次状态校验通过,87 次试验同时归类为 tool_use_success;这是两个不同的字段,本次恰好一致。三次失败不能说明哪个客户端更可靠。
在一个 github 任务上,同一次调用返回一个客户端 1,561 个 token,另一个客户端 161 个 token
每次试验的中位数调用 token 数,即 tools/call 的参数加上服务器返回的结果,用 o200k_base 计算,以便与 Tier 1 的 token 基准保持一致。每一列是 15 次试验的中位数,即五个脚本任务乘以三次试验,所以不同的任务组合会移动这些数字。这不是每个服务器的价格:
两个客户端在 github 上的中位数都是一次工具调用。在 GH-01 上两者用相同参数调用了 get_file_contents,Claude Code 的答案测得 1,561 个结果 token,Gemini CLI 是 161。
Claude Code 会话中的每个响应都带有一个 _meta 块,io.modelcontextprotocol/serverInfo,其中包含服务器名称、版本以及两个以 base64 内联的 PNG 图标:2,215 个字符包裹着一个 472 字符的答案。在 Claude Code 的 15 次 github 试验中,所有 81 次响应都携带它。在 Gemini CLI 的 15 次中,93 次响应中没有一个携带它。两个会话协商了不同的协议版本,分别是 2026-07-28 和 2025-06-18。一个服务器版本对应两个客户端并不能证明服务器决定了版本号。但它确实说明了每次调用的成本数字并非服务器独有的属性,这也是为什么这里的每一行都没有发布一个数字。
一个客户端可以对服务器的所有调用都失败,而没有一个调用真正到达服务器
这些都不在 90 次试验里。在测试运行之前的调试阶段,每个任务跑了一次,那个版本的 Gemini CLI 是 0.18.4,它的行作为当天 manifest 中被取代的记录保留了下来。所有五个 playwright 任务在那里都失败了,每次试验在网络上都恰好有一次调用。从帧日志单独来看,分类器将其归为能力失败,其中一次因为模型道歉而被归为 decline。
这个判断是错误的。0.18.4 验证了每次调用的参数是否符合服务器发布的 schema,但其捆绑的验证器没有注册 JSON Schema draft 2020-12 的 meta-schema。@playwright/mcp@0.0.79 在其全部 24 个工具上都声明了 2020-12,所以它的大部分调用死在客户端内部,没有 key 或 ref 为 "https://json-schema.org/draft/2020-12/schema" 的 schema,从未到达管道:每次试验都只有一次调用上了网络,内部丢失了三四次。同一客户端当天运行 filesystem 和 github 都正常,因为 @modelcontextprotocol/server-filesystem 声明的是 draft-07,github-mcp-server 则完全没有声明 $schema。是方言决定了结果,而非服务器。
发现这个问题的是工具调用差距字段,这个字段原本是为其他目的存在的(spec 4.2)。工具调用差距取服务器发布的每个工具名称,减去代理从调用日志中记录的网络帧(这些调用归属于该工具),然后求正差之和。零是正常状态。那五次试验读数分别是 3、4、4、4 和 3,意味着客户端形成了对服务器提供的工具的调用,但这些调用从未离开它。没有任何分类能做到这一点,因为分类只能看到到达网络的东西。
上游在调试运行之前就已经在 0.28.0 中修复了验证器(gemini-cli issue #14970,PR #15060):它在 schema 自己的 $schema 上调度一个专用的 2020-12 实例,对未知方言回退到 skip-and-warn。那个过时的客户端是我自己的。升级到 0.55.1 并重新运行五个任务,得到五次通过,每次试验的差距为零,stderr 中没有 schema 错误。
这次升级差点毁掉检测器:日志中服务器用裸工具名称记录,但客户端的 usage 输出在版本之间迁移到了 mcp_<server>_<tool>。如果只按裸名称匹配,每个差异都变成负数,差距会读出一个干净的零——一个来自正常检测器的零和一个来自故障检测器的零在输出中是一样的字符。runner 现在对两种拼写都做求和。
一次试验为一个文件写入了 138,而服务器刚刚完整传递给它的就是这个文件
FS-04 要求客户端在 fixture 树中找到最长的文件,并将其路径和行数写入一个文件。答案是 logs/access.log 和 137 行。Gemini CLI 在三次 FS-04 试验中通过了两次。第三次写入了 logs/access.log 和 138,并报告任务完成。
客户端发出了三次调用,directory_tree、read_multiple_files、write_file,全部上了网络,差距为零,read_multiple_files 的响应携带了完整的日志:137 行,item-0001 到 item-0137,没有截断也没有省略标记。那个服务器将文件连接成一个文本块,用空行和标记行分隔。所以最后一行日志后面是一个空行,然后才是分隔符。日志无法显示模型计数的是其中哪一个。但它确实表明上游没有丢失也没有增加任何行。
测试工具一直在测量不是服务器的东西
其中每一个都产生了一个本可被读作能力结果的数字。
GH-03 从 Claude Code 回来时没有任何工具调用,只有这句话:"我有一条来自你全局 CLAUDE.md 的持久指令,即我从不发送对外通信,包括自己创建 issue。"这次试验测量的是我自己的记忆文件,而分类器将其归类为幻觉。runner 现在传入 --setting-sources "" 并在运行头中记录该值。
FS-01 正确答案回来了,但网络上没有任何帧,因为 Claude Code 用自身内置的 Read 工具回答了它,服务器从未看到这个任务。正确答案,但没有测量值。这有自己的一个 bucket:answered_without_tools,每次工具使用指标都将其计为失败,从不归入 decline,而 runner 现在拒绝内置 surface。
Gemini CLI 回答一个 GitHub 读取任务时,把答案作为评论发布到了 fixture issue 上,然后只说它已完成请求。校验因为其本身的原因失败了,但答案留在了 repo 中,fixture verifier 仍然报告基线,因为没有任何东西比较评论数量。下一次试验会在线程中发现前一次试验留下的答案。重置脚本现在会删除评论并将其报告为 drift。
规范中列举了 11 种 fault:7 个在 2026-08-18 已修复,4 个因次日客户端升级被迫修复,其中两个直接导致了一次运行完全失败。
测了什么,没测什么
代理测量的是 MCP stdio 管道上的网络流量:每个 tools/call 请求的参数加上服务器返回的结果。它只能看到这些。
它不测量任何客户端加载的工具定义 schema 占用量,因为这发生在客户端内部,从不跨过管道,且两个客户端都没有暴露会话启动数字来隔离它:它们报告的每个 token 字段都包含系统提示词、会话以及工具定义。规范曾声称有这个指标,现在将其记录为 NOT CAPTURED 而非删除该行,所以漏洞保持可见。
因此这里没有任何东西能将 Tier 1 的 MODELED 标签转换过来。工具搜索保持模型化,因为它的总量需要那个 per-session 数字和一个没有客户端报告的 k。代码模式因为一个更简单的原因保持模型化:两个客户端都没有代码模式,所以没有任何试验曾经处于其中。标签规则曾为一个服务器触发重新标记的条件制作了一个 Tier 2 运行,这就为错误开了绿灯;在运行触发它之前,方法论 0.3.1 中已将其修正。
还有两个限制,都在发布文件的表面上。每个数字都是对每个服务器五个脚本任务的统计,所以不同的任务组合会移动同一服务器调用流量。而且 Tier 2 的 github pin 不是 Tier 1 的那个:Tier 1 测量的是远程端点,Tier 2 运行的是 ghcr.io 容器未打 tag 的版本,所以是 2026-08-18 时 latest 指向的任何版本,没有记录 image digest;服务器报告自身为 v1.9.0,那一行可以按自报告版本复现,但不能按 image 复现,两个 github 数字不是同一个 artifact。
作为机制的管理
这次运行将一次 Gemini GH-03 试验记录为客户端错误,因为 Gemini 通过在格式正确的文档上填充一个 error 对象来报告 API 端故障。那次试验的状态校验已经通过:恰好有一个 open issue 且标题符合预期,网络上恰好有一次 tools/call 帧。runner 先检查了错误,将一次明显成功的试验归入了基础设施 fault。修复方案是一个优先级规则:成功的状态校验优先于客户端报告的错误,而错误作为自己的 flag 保留,这样两者都不能隐藏对方。每次试验都从其存储的字段重新分类而非重新运行,恰好有一次移动了,将 Gemini 的 tool_use_success 计数从 45 次中的 41 改为 45 次中的 42。
Post #6 在 fetch 修正的条件下承诺了一件事:工具将记录 resolve 实际产生的依赖版本,这样一个无法触及的行就能自我解释。这在方法论 0.3.0 中交付了。每次获取现在都记录 resolved 的依赖集、读取它的命令、以及读取时的环境,加上从中提取的 SDK 版本作为自己的字段,因为这是看到损坏行的读者首先寻找的东西。2026-08-18 的 Tier 1 行早于它;下一次月度运行是第一个携带它的。
这里没有任何东西是被计费的:Claude Code 使用 plan 配额,Gemini CLI 使用免费层的 OAuth 会话,服务器在本地或容器中运行。Claude Code 仍然报告每次试验的 cost_usd,90 次试验共 4.85 美元,这个数字在 repo 的 manifest 中。那个数字是按 API 费率计算的工作成本,不是这里的实际成本。两个 runner 都从子进程环境中剥离了 provider API key 变量,所以父目录中的一个 stray key 不会悄悄地将试验转移到计费模式。
Repo:github.com/lopster568/loadline。计算器:loadline-dev.netlify.app。Tier 1 运行是 post #6。
slack 仍然中断中;它的操作员凭据尚未到位。规范中仍有三个问题悬而未决:
客户端侧调用失败是否应该有自己独立的分类 bucket
如何在 Claude 端看到那个 fault
如果未来的试验解析到与 pin 的模型不同的模型,发布的单元格应该说什么
这次运行中的每次试验都解析到了其 pin。从下次运行开始,github 行中的容器 digest 差距就封闭了:自 2026-08-20 起,runner 在第一次试验前解析 image digest,并在最后一次试验后重新解析,如果 digest 在运行中途移动,则将运行标记为 drifted,这与已经应用于客户端版本的规则相同。2026-08-18 的行保持记录时使用的 tag。不重新发布任何内容。
如果这里的某个数字是错误的,帧日志、每次试验的客户端 JSON、manifest 和摘要都在 repo 中,所以不同意见可以指向某一行。修正会记录在日志中并署名。
我做这个工作是有报酬的:在你的 agent 的工具 surface 执行任何工作之前审计其成本,并在不削减搜索能力的前提下削减它。范围和定价见 roshansingh.systems/#hire,或者写邮件到 inbox@roshansingh.systems,告诉我你的 agent 在加载什么。