工具大全
踩坑实录作者:Coocon2026年10月3日18 次阅读约 7 分钟阅读

Claude Code MCP 显示 Connected 却 0 个工具:Invalid result for tools/list(ttlMs / cacheScope)实测与修法

问题背景

MCP server 配好之后,claude mcp list 不是 ✘ Failed to connect,而是一个黄色感叹号:

roblox-like: /tmp/mcp-ttl/roblox-like-server  - ! Connected · tools fetch failed — Invalid result for tools/list: [ { "expected": "number", "code": "invalid_type", "path": [ "ttlMs" ], "message": "Invalid input: expected number, received undefined" }, { "code": "invalid_value", "values": [ "public", "private" ], "path": [ "cacheScope" ], "message": "Invalid option: expected one of \"public\"|\"private\"" } ]

会话里的表现是:server 状态显示 connected,但模型一个该 server 的工具都看不到。重启、新开会话、重启电脑都没用。

GitHub 上对应的是 anthropics/claude-code#97319(2026-09-26 提交,到 10-03 仍是 OPEN),触发者是 Roblox Studio 官方的 MCP bridge。报告人的判断是「tools/list 响应里带了新版协议的额外字段 ttlMs / cacheScope,Claude Code 的校验太严,把整个响应拒掉了」,评论区给出的两个绕过办法是「降级到 2.1.280」和「设 MCP_PROTOCOL_NEGOTIATION=legacy」。

这篇用自写的 stub server 把这条报错拆开:到底是字段多了还是少了,为什么同一个版本有人中招有人没事,降级为什么能好,以及用户侧和 server 侧各该怎么修。所有报错原文都来自 2026-10-03 在本机的真实运行。

问题分析

先看规范。MCP 的 2026-07-28 版 schema 里,ListToolsResult 同时继承了 PaginatedResult 和 CacheableResult:

export interface CacheableResult extends Result {
  ttlMs: number;                     // 客户端可缓存多少毫秒,0 表示立即过期
  cacheScope: "public" | "private";  // 类似 HTTP Cache-Control 的 public / private
}

export interface Result {
  _meta?: ResultMetaObject;
  resultType: ResultType;            // "complete" | "input_required" | string,本版起必填
  [key: string]: unknown;            // 允许任意额外字段
}

这里有两个关键点:

  1. ttlMs、cacheScope、resultType 在这一版里都是必填。
  2. Result 带 [key: string]: unknown,规范明确允许额外字段。

再看报错本身:"expected": "number" + invalid_type 指向 ttlMs,invalid_value + ["public","private"] 指向 cacheScope。字段如果是「多出来的未知字段」,校验器不会知道它该是 number、该在 public/private 里选。只有在按 2026-07-28 的 schema 校验、而 server 没给这两个字段时,才会是这个形状。2.1.288 的措辞更直白:expected number, received undefined。

所以要验证的假设是:server 和 Claude Code 协商到了 2026-07-28,但 server 的 tools/list 仍按旧版协议回包,漏了新版的必填字段。 还要回答两个问题:stdio server 什么时候会协商到 2026-07-28?降级到 2.1.280 为什么能好?

技术方案与选型

  • 被测对象:Claude Code 2.1.280(issue 评论里「最后一个能用的版本」)、2.1.285(本机日常版本)、2.1.288(10-02 发布的最新版)。2.1.280 和 2.1.288 用 npm install 装在隔离目录,不碰全局安装。
  • MCP server:自写 Node stdio stub,约 70 行,逐行记录收到的 JSON-RPC。用环境变量切换各种回包:initialize 回什么协议版本、是否实现 server/discover、tools/list 里给不给 resultType / ttlMs / cacheScope、给错类型、加未知字段。
  • 模型侧:本地假的 Messages API(固定回一句 STUB_OK),同时充当 HTTPS_PROXY,记录所有 CONNECT 并回 403。整个实验零真实 API 调用:stub 共记录到 97 次 CONNECT api.anthropic.com:443,全部被拦。
  • 读数:--output-format stream-json --verbose 第一行 init 里的 mcp_servers[].status 和 tools 里有没有 mcp__ttlstub__ping,加上 --debug-file 里该 server 的日志。每个 case 用独立的 HOME,cwd 放在仓库外。

实测过程

Case 0:默认配置下,stdio 根本不走新协议

不设任何环境变量,用 2.1.285 跑。stub 收到的第一条消息:

{"method":"initialize","params":{"protocolVersion":"2025-11-25", ...},"jsonrpc":"2.0","id":0}

序列是 initialize → notifications/initialized → tools/list,走的是老握手。客户端提出的版本是 2025-11-25,工具正常拿到。

也就是说,在代码默认值下(远程开关没打开),stdio MCP server 根本碰不到 2026-07-28。 开关在你的账号上开没开,本地看不到,下面 Case 2 讲它在哪。

Case 1:老握手里回 2026-07-28 会直接失败

如果 server 在 initialize 里不照抄客户端版本,而是硬回 2026-07-28:

MCP server "ttlstub": Connection failed after 29ms: Server's protocol version is not supported: 2026-07-28

这时状态是 failed,不是 connected。所以 issue 里那种「connected + 0 工具」不是从老握手进入的。

Case 2:打开协商的开关在哪

在 2.1.285 的二进制里检索 MCP_PROTOCOL_NEGOTIATION,可以找到决定协商模式的函数。整理后的逻辑如下:

// MCP_PROTOCOL_NEGOTIATION 只接受 'legacy' 或 'auto',其它值警告后忽略
if (env === "legacy") return { mode: "legacy" };
if (env === "auto")   return { mode: "auto", ... };      // http / claudeai-proxy / ccr-proxy / stdio
switch (transport) {
  case "http":  return flag("tengu_mcp_protocol_negotiation_http", true)   ? auto : legacy;
  case "stdio": return flag("tengu_mcp_protocol_negotiation_stdio", false) ? auto : legacy;
  // sse / ws / ide / in-process / sdk-control 一律 legacy
}

stdio 走不走新协议,取决于一个默认值为 false 的远程开关 tengu_mcp_protocol_negotiation_stdio。2.1.280 和 2.1.288 里这段逻辑完全相同,默认值也都是 false。环境变量 MCP_PROTOCOL_NEGOTIATION=auto 可以在本地强制打开它,下面的 case 都靠它进入新协议。

Case 3:打开协商,但 server 不支持 server/discover,会平稳回退

MCP_PROTOCOL_NEGOTIATION=auto,stub 对 server/discover 回 -32601 Method not found:

stub 收到: server/discover → initialize → notifications/initialized → tools/list
"protocolEra":"legacy","negotiatedProtocolVersion":"2025-11-25"

客户端先探测 server/discover,失败后回到老握手,工具正常。不声称支持新协议的 server 是安全的。

Case 4:server 声称支持 2026-07-28,tools/list 却按老格式回

stub 在 server/discover 里回 supportedVersions: ["2026-07-28"],tools/list 只回 { tools: [...] }:

"protocolEra":"modern","negotiatedProtocolVersion":"2026-07-28"
tools/list failed (Invalid result for tools/list: missing required resultType — servers implementing protocol revision 2026-07-28 MUST include it (the absent-means-complete bridge applies only to earlier-revision servers)); retrying in 250ms
...
[ERROR] Failed to fetch tools: Invalid result for tools/list: missing required resultType — ...
init: status=connected  mcp 工具数=0

这就是 issue 里的现象:连接成功,工具为 0。第一个被拦下的字段是 resultType。tools/list 一共请求了 4 次(首次加 250/500/1000ms 三次重试),之后整个会话不再尝试。

Case 5:补上 resultType,就是 issue 里那句原文

stub 补上 resultType: "complete",仍然不给 ttlMs / cacheScope:

Claude Code 2.1.280 / 2.1.285 / 2.1.288 在同一个 stub 上报出同一个 ttlMs / cacheScope 校验错误 图:同一个 stub(回 resultType,不回 ttlMs / cacheScope),MCP_PROTOCOL_NEGOTIATION=auto 下三个版本的读数。2.1.280 的措辞是 "Invalid input",和 issue 里贴的日志一字不差;2.1.285 / 2.1.288 多了 received undefined。三个版本都是 connected + 0 个工具。

这一组还能顺带回答「降级到 2.1.280 为什么能好」:2.1.280 的校验一点也不宽松,打开协商后它照样失败。降级之所以有用,只能是因为那台机器上 2.1.280 的远程开关是关的,于是走了老握手。这一点我在本机无法直接观察(本机账号的配置缓存里没有这类开关),属于推断。但它和代码逻辑、issue 评论里贴的 Roblox 开发者论坛帖子(链接标题是 roblox-studio-mcp-works-up-to-claude-code-21280-broken-from-21281)都对得上:同一段代码,远程开关按版本放量。

Case 6:逐个字段排查

都在 2.1.285 + MCP_PROTOCOL_NEGOTIATION=auto + discover 声称 2026-07-28 的条件下:

tools/list 回包 结果 报错关键片段
只有 tools 0 工具 missing required resultType
+ resultType 0 工具 ttlMs expected number, received undefined;cacheScope invalid_value
+ resultType + ttlMs: 60000 0 工具 只剩 cacheScope invalid_value
+ resultType + ttlMs: "60000"(字符串)+ cacheScope: "shared" 0 工具 ttlMs expected number, received string;cacheScope invalid_value
+ resultType + ttlMs: 60000 + cacheScope: "public" 正常 —
上一行再加一个未知字段 x-roblox-meta: {...} 正常 —
resultType: "complete" + ttlMs: 0 + cacheScope: "private"(2.1.288) 正常 —

倒数第二行直接否定了 issue 标题里的说法:额外的未知字段不会被拒,缺必填字段才会。 另外,server/discover 响应自身缺 ttlMs 时,客户端没有拒绝协商(这几组 stub 的 discover 都没给),问题只出在 tools/list 上。

Case 7:用户侧的两种关法

# 临时:只对这一次启动生效
MCP_PROTOCOL_NEGOTIATION=legacy claude

# 持久:写进 ~/.claude/settings.json
{ "env": { "MCP_PROTOCOL_NEGOTIATION": "legacy" } }

MCP_PROTOCOL_NEGOTIATION=auto 时 mcp list 显示 ! Connected · tools fetch failed,legacy 时 ✔ Connected 图:2.1.288,同一个「声称 2026-07-28 却漏字段」的 server,两种协商模式下 claude mcp list 的输出。

关于 settings.json,我做了两组验证(2.1.288):

  • 进程不设变量、settings.json 的 env 写 auto → 进入 modern,失败。说明 settings 里的这个变量会被读取。
  • 进程设 auto、settings.json 写 legacy → 走老握手,正常。说明 settings 里的值覆盖了进程环境变量。

所以持久化写 settings.json 是可靠的,比在 shell 配置里 export 更不容易漏(IDE 插件、桌面端通常不经过你的 shell 配置启动)。

实践效果

一句话结论

Invalid result for tools/list + ttlMs / cacheScope 的意思是:这个 server 告诉 Claude Code 它支持 MCP 2026-07-28,但回包没按 2026-07-28 的格式来。Claude Code 按规范拒收,并不是「过度严格」。

用户侧:先恢复工具,再等 server 更新

  1. 设 MCP_PROTOCOL_NEGOTIATION=legacy(推荐写进 settings.json 的 env)。立即生效,不用降级,也不怕自动更新把版本拉回去。代价是所有 stdio / http server 都走老握手,暂时用不上 2026-07-28 的新能力(比如 2.1.281 加入的 URL 模式 elicitation)。
  2. 升级 server。Roblox 这个案例里,评论区确认 Roblox Studio 0.740.19.7400003 已修复。
  3. 不建议靠降级:降级是否有效取决于远程开关,不受你控制;而且 2.1.280 打开协商后同样失败(Case 5)。

确认是不是这个问题,不用开 --debug。macOS 上 Claude Code 默认就会把每个 server 的连接日志写到这里:

ls ~/Library/Caches/claude-cli-nodejs/<项目路径>/mcp-logs-<server名>/
# Windows:%LOCALAPPDATA%\claude-cli-nodejs\Cache\<项目路径>\mcp-logs-<server名>\

日志里同时出现 "protocolEra":"modern" 和 Failed to fetch tools: Invalid result for tools/list,基本就能确诊。

server 作者:三个字段,或者先别声称支持

  • 在 server/discover 的 supportedVersions 里写了 2026-07-28,那么 tools/list(以及 resources/list、prompts/list 等 CacheableResult 系列)每个结果都要带上:
    { "resultType": "complete", "ttlMs": 0, "cacheScope": "private", "tools": [ ... ] }
    
    ttlMs: 0 表示「立即过期,客户端每次可重取」,private 表示「不跨授权上下文共享缓存」。这是最保守的取值,2.1.288 实测可用。
  • 还没实现新版协议,就让 server/discover 回 -32601(Case 3)。客户端会平稳回退到老握手。
  • 老握手的 initialize 里不要硬回 2026-07-28(Case 1),会直接 Server's protocol version is not supported。

速查表

看到的现象 原因 处理
! Connected · tools fetch failed — Invalid result for tools/list: ... ttlMs ... cacheScope server 协商到 2026-07-28,却漏了 ttlMs / cacheScope 用户:MCP_PROTOCOL_NEGOTIATION=legacy;server:补字段
missing required resultType — servers implementing protocol revision 2026-07-28 MUST include it 同上,漏的是 resultType 同上
ttlMs ... received string 字段给了,类型错(字符串) server 改成 number
Connection failed ... Server's protocol version is not supported: 2026-07-28 老握手里硬回新版本号 server 在 initialize 里回客户端提出的版本
同一个版本,同事好好的,你的 0 工具 stdio 协商由远程开关控制,各人放量不同 同第一行

踩坑

  • 别被 issue 标题带偏。 「strict validation of extra fields」这个说法很自然,因为字段名在新版协议里,看起来像是多出来的。但 zod 报错里的 expected number / values: [public, private] 说明客户端知道这些字段的定义,问题在于缺失。加未知字段那组实测是正常的。
  • 「降级就好了」不等于老版本没有这个问题。 同一段逻辑、同一个默认值,差别只在远程开关。拿「换版本前后」做对照的结论都要打折扣,变量不止版本这一个。
  • 重试只发生在连接建立时。 tools/list 失败后重试 3 次(250/500/1000ms),之后整个会话保持 0 工具。在 2.1.288 的 -p 里,失败 case 的墙钟约 2.46s,正常 case 约 0.36s,多出来的基本就是这几次重试。
  • 实验本身的坑:第一轮读 --debug-file 时,用 grep 过滤 MCP server "ttlstub",结果把报错行全漏了。这个版本把带换行的日志消息写成了 JSON 字符串("MCP server \"ttlstub\": ...",引号被转义),按原文 grep 匹配不上。后来改成先 JSON 解码再匹配。另外,第一个 case 的 cwd 放在了仓库里,Claude Code 加载了项目的 .claude/ 配置,那一组作废重跑,cwd 移到 /tmp。

相关阅读

这类实测,每周六汇总一封

订阅码农早餐:每天 8:00 一封 AI 编程早报,每周六另附本周 Claude Code / Codex / 本地模型的实测和踩坑汇总。

相关文章

Claude Code 安装失败实测:npm EACCES、镜像卡 600 秒、Node 20 静默装旧版、install.sh 返回地区拦截页、原生安装器顺手卸掉 npm 版——15 条报错原文与解法

Claude Code 安装失败实测:npm EACCES、镜像卡 600 秒、Node 20 静默装旧版、install.sh 返回地区拦截页、原生安装器顺手卸掉 npm 版——15 条报错原文与解法

在 macOS 上把 claude code 安装的失败面逐个真实复现,共 15 条报错原文,全部带墙钟和 exit code。npm 全局装到 /usr/local:EACCES,exit 243。cache 目录只是 0555,npm 却报 root-owned 并建议 sudo chown。本机 npmmirror 两次分别用了 147 秒、超过 600 秒,官方源两次都在 11–12 秒(仅 2 个样本)。Node 20 不钉版本号会静默装到 2.1.197,没有任何警告。境内直连 claude.ai/install.sh 时 curl exit 0,拿到的却是一张 447KB 的地区拦截页 HTML。原生安装器会调用 npm uninstall -g,把已有的 npm 版删掉,终端里不提示——本次实测真的删掉了本机的全局安装。

claude-codenodejs+6
pitfalls2026年9月29日11 min
131
DeepSeek harness 实测:同一个模型换三种外壳——Claude Code 15/15、Codex CLI 15/15、裸 API 0/15 还谎报 5 次完成

DeepSeek harness 实测:同一个模型换三种外壳——Claude Code 15/15、Codex CLI 15/15、裸 API 0/15 还谎报 5 次完成

同一个 deepseek-v4-pro,三种 harness,读 / 写 / 改 / 跑命令 / 多步五个任务各 3 轮,副作用一律落盘核验。Claude Code 走 DeepSeek 的 Anthropic 端点:15/15,中位 4.17 秒,每轮 ¥0.159。Codex CLI 0.157.1 走 Responses 端点:15/15,中位 15.52 秒,每轮 ¥0.022——只有前者的 1/7。裸 chat/completions:0/15,其中 5 轮回复 DONE / EDITED,磁盘上什么都没有。差异全在 harness 不在模型:DeepSeek 的 Anthropic 端点按 metadata.user_id 分区缓存,Claude Code 每次 claude -p 都冷启动、约 15K token 全价;Codex 根本没有文件工具,读写改全走 shell;注入 500 时 Claude Code 重试 10 次约 175 秒、Codex 30 次约 25 秒放弃,429 时 Codex 不重试;120KB 输出时 Claude Code 只给模型看头部 2KB,Codex 保留头尾。另:Codex 0.157.1 已移除 wire_api = "chat",DeepSeek 必须走 responses。

claude-codedeepseek+6
hands-on2026年9月28日12 min
177
Claude Code「bash denied by auto mode」怎么办:被拦原因、could not evaluate 与 unavailable for this model 逐条实测

Claude Code「bash denied by auto mode」怎么办:被拦原因、could not evaluate 与 unavailable for this model 逐条实测

auto 模式下 Bash 被拦,常见的有三种提示:denied by auto mode、Auto mode could not evaluate this action、auto mode unavailable for this model。本文在 Claude Code 2.1.280 上跑了 40 多次真实会话。结论:denied 是分类器判定你的命令越权,最常见的理由是 [Code from External],也就是执行了你没有点名的外部代码,重试没用,要么在 prompt 里点名来源,要么在用户级 settings 的 autoMode.environment 里声明信任;could not evaluate 是分类器没给出可解析的结论;unavailable for this model 是模型比 claude-opus-4-6 旧,claude -p 下会被静默降级、只在 debug 日志里留一行 WARN。另外,2.1.280 的判定已经改到服务端,随主请求一起返回。经过「只允许 Claude Code 客户端」的中转网关时,本地兜底的分类器请求会被 503 拒绝,这就是「temporarily unavailable (server error)」的一个固定成因。

claude-codeauto-mode+4
pitfalls2026年9月27日9 min
186
Claude Code 报错 Invalid API key · Fix external API key 怎么排查:Not logged in、Credit balance is too low、API Error 401/429/529 原文与重试实测

Claude Code 报错 Invalid API key · Fix external API key 怎么排查:Not logged in、Credit balance is too low、API Error 401/429/529 原文与重试实测

用本地 stub 模拟 Messages API,在 Claude Code 2.1.280 上跑了 28 组 case、61 次 claude -p。结论:401 会重试 10 次、约 3 分钟后才报 Invalid API key · Fix external API key;key 含中文或中间换行则在本地就被拦,0 个请求、0.28 秒。所有报错都打在 stdout,stderr 为 0 字节,exit 1;JSON 里 subtype 仍是 success。CLAUDE_CODE_MAX_RETRIES=0 让 401 在 0.28 秒内失败。KEY 和 TOKEN 同时设置时两个 header 都会发。

claude-codeclaude-code-lab+4
pitfalls2026年9月27日10 min
138