关于 toolmenu
toolmenu 是用于测试 MCP 服务工具菜单的 Node.js 命令行工具和库,可跨会话、客户端与版本检查变化。它面向 MCP 服务开发者,以确定性检查在 CI 中发现兼容性问题,不使用大模型。
主要功能
- 01快照并验证工具菜单
- 02检测会话中的菜单变化
- 03比较版本差异与版本号
- 04检查客户端可见的工具
- 05扫描 MCP 客户端配置
- 06在 CI 中运行并生成 PR 报告
适用场景
- 01发布前发现破坏性工具变更
- 02跟踪会话期间的菜单变化
- 03检查特定客户端看到的工具
- 04批量审查已配置的 MCP 服务
MCP 相关
toolmenu 也可通过 stdio 作为 MCP 服务运行,向客户端提供菜单快照、差异比较、会话检查和规则说明工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
4收录的工具列表 · 更新于 今天
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cwd | string | 否 | 运行目录:服务的启动位置、读取 toolmenu.config.json 的位置,以及相对路径的解析基准。默认值:运行 toolmenu mcp 的位置。 |
| env | object | 否 | 命令服务器的环境变量。服务器会获得最小化环境,以及此处提供的变量。 |
| url | string | 否 | Streamable HTTP 端点:https://example.com/mcp。填写此项或 command。 |
| client | enum | 否 | 还要检查此客户端加载菜单时的情况:它会删减、发现和拒绝哪些内容。claude-code | claude-api-search | openai |
| catalog | boolean | 否 | 还要读取搜索工具或命令路由器背后的操作。 |
| command | array | 否 | 启动 stdio 服务的命令,每项一个单词:["node", "dist/server.js"]。填写此项或 url。 |
| headers | object | 否 | url 服务的 HTTP 标头:{"Authorization": "Bearer ..."}。 |
| save_as | string | 否 | 将菜单写入此处(基线文件为 menu.json),路径相对于 cwd。默认:不写入。 |
详细介绍
toolmenu
检查 MCP 服务的工具列表:在用户遇到之前,捕获会让 Agent 困惑、破坏提示词缓存或导致客户端出问题的更改。
MCP 服务的工具列表是每个 Agent 行动前都会查看的菜单。toolmenu 会按照客户端和 Agent 实际看到菜单的方式进行检查:跨调用、连接、客户端和版本发布。整个过程不会涉及大模型,因此相同的输入始终会得到相同的结果,也适合在 CI 中运行。
npx toolmenu snapshot -- node dist/server.js # or: npx toolmenu snapshot https://example.com/mcp
npx toolmenu init -- node dist/server.js # CI: a baseline, a workflow, a comment on every PR
它能发现什么
| 会话中途改变的菜单 | session 会调用你的工具并监视工具列表。对话中途新增的工具可能会重写整个缓存提示词:根据我的日志,一次解锁就耗费了 40–57K 个缓存令牌。 |
| 破坏性发布 | diff 会比较两个菜单:已移除的工具、新增的必需参数、收窄的类型、令牌增长。它还会判断你的版本号增幅是否足够。 |
| 2026-07-28 协议 | 工具集不得因连接而异,结果必须包含 resultType。还会检查 Elicitation、多轮往返输入和 server/discover,以及 2025 年的握手。 |
| MCP Apps、Tasks、Server Cards | 它会像宿主一样读取 ui:// 资源,并检查哪些人可以看到每个工具。它能发现发给无法轮询任务的客户端的任务,以及与实时服务矛盾的 Server Cards。 |
| 客户端看到的内容 | --client claude-code | claude-api-search | openai 会显示每个客户端加载、裁剪、能找到和拒绝的内容。 |
| Agent 容易误读的内容 | 它会标记含糊的 id、任何工具都不会返回的 ID、描述中位置靠后的指令,以及缺少注释或标记为只读的写操作。 |
| 会路由到错误工具的词语 | routes.yml 会固定每个关键词必须(以及不得)路由到哪个工具,其方式与关键词或 BM25 工具搜索查看菜单的方式相同。如果修改描述导致 get_audit_trail 在“audit”搜索中胜出,就无法通过检查;整个过程无需模型,也不消耗评估预算。更多信息 |
| 不断漂移的菜单 | 它会发现由时钟或哈希顺序生成的默认值、因进程、连接或离线状态而异的菜单,以及取决于请求者身份的列表。 |
每条发现都会说明问题所在、位置和 toolmenu 看到的内容,并以 → Next: 步骤结尾。
它既可在 22 个服务的 bench 上运行,也可在托管服务(GitHub、Atlassian、Sentry、
Notion、Supabase、Vercel……)上运行。它在真实服务上发现的问题见
docs/FINDINGS.md。
状态:0.25。 规范见 docs/SPEC.md, 所有规则见 docs/RULES.md,变更见 CHANGELOG.md。
为什么“变化发生时”很重要
我找到的 MCP 检查工具只评估菜单的单个快照:工具名称、描述、 架构。toolmenu 还会检查菜单何时发生变化:
- 会话中途。 大多数客户端会在提示词开头放入工具列表,因此列表中的任何
变化,即使只是在末尾新增一个工具,也意味着之后的所有内容都要重新处理。
关于这一点以及各类配置的基准测试,详见
这篇文章。
session会将每项 变更关联到导致变更的步骤,并指出变更发生在列表中的什么位置。 - 每次连接。 2026-07-28 规定工具集不得因连接或其他请求的副作用而变化(2025 年
版本允许这样做)。每个会话解锁工具的设计(例如我构建的那种)已被排除。
session会告诉你自己的服务是否属于这种设计。它还会检查客户端实际会显示的菜单: 失效的x-mcp-header或客户端无法解析的$ref会隐藏工具,或导致对该工具的每次调用都失败。 当工具暂停以向用户提问时,session会回答“否”,并说明该工具是否能够接收该输入。 - 不同发布版本之间。
diff会发现破坏性变更和令牌增长,并检查版本号增幅。history会对软件包已发布的各个版本进行同样的检查。它会使用当前依赖安装旧版本, 因此记录的是旧版本如今提供的内容(FINDINGS F4)。
针对参考服务 @modelcontextprotocol/server-everything 运行时,它不会报告误报(见 bench/)。
需要 Node 22 或更高版本。如果运行器上的 Node 版本较旧,GitHub Action 会自带一个版本; 但仅对 toolmenu 而言:你的服务和作业后续步骤仍使用作业的 Node。 你的服务可以使用任何语言:toolmenu 会启动它(stdio),或连接到它(HTTP)。
一条命令即可设置 CI:它会将你的服务快照保存到 menu.json(基线),并为你的项目
(Node、Python、Go 或 Rust)写入 .github/workflows/toolmenu.yml。
提交这两个文件,创建拉取请求,toolmenu 就会在其中发表评论。
npx toolmenu init -- node dist/server.js # add --with-session to also run session --auto
你通过 --env 或 --header 传递的每个值都会以
${{ secrets.… }} 的形式写入工作流,而不会作为文本写入(像 DATABASE_URL 这样的名称,并不能说明其中保存的是密码);init 会说明需要添加哪些密钥。它绝不会覆盖文件。
可在仓库中的任何位置运行它。.github/workflows/toolmenu.yml 会写入 git
根目录,而 CI 会在你运行 init 的文件夹中启动服务,menu.json 也会保存在该目录中。这就是单仓库如何支持多个包:在
packages/server 中运行 init。仓库中文件的路径会转换为相对路径,因此在
运行器上也能正常使用。init 会列出所有无法在运行器上使用的项目(仓库外的文件、localhost URL、运行器上没有的虚拟环境),并说明每项该如何处理。
手动操作:
npx toolmenu snapshot -- node dist/server.js # the menu, its findings, menu.json
npx toolmenu diff menu.json new-menu.json # what changed between two releases
npx toolmenu session --auto -- node dist/server.js # call read-only tools, watch the menu
HTTP 服务的工作方式相同:npx toolmenu snapshot https://example.com/mcp --header "Authorization: Bearer $TOKEN"。
对于使用 OAuth 的服务(大多数托管服务:GitHub、Atlassian、Sentry 等):在浏览器中登录一次,此后 snapshot 和 session 就会使用该登录,并在需要时刷新:
npx toolmenu auth login https://mcp.example.com/mcp
npx toolmenu snapshot https://mcp.example.com/mcp
登录信息按服务分别存储在 ~/.config/toolmenu/auth 中(只有你能读取);
auth list 和 auth logout <url> 用于管理登录信息。不允许动态注册的服务需要预先注册的应用:使用 --client-id(以及 --client-secret)。通过 SSH 或在容器中操作时,auth login --no-browser 会打印登录 URL,而不是打开它:在任意设备上打开该 URL 并登录,然后粘贴浏览器最终停留页面的地址(无法加载的 localhost 页面是正常的)。在无人登录的 CI 中,请将令牌作为标头传入。
两代协议均受支持:2026-07-28(server/discover,无状态)以及 2025 年的 initialize 握手,基于官方 TypeScript SDK。
每条发现和错误都会说明问题、位置、toolmenu 检测到的内容,并以
→ 下一步:操作建议结尾。标记为 · 不确定 的发现属于启发式判断(依据名称和词语):它们会说明检测到了什么,而不会推断其含义。toolmenu explain <rule> 会说明规则的检查内容、默认级别以及如何更改(所有规则都列在 docs/RULES.md 中)。
每条命令都有自己的帮助信息:toolmenu history --help。
使用代理(HTTPS_PROXY)时,stdio 服务会获得代理和 CA 设置
(HTTPS_PROXY、NO_PROXY、NODE_EXTRA_CA_CERTS、SSL_CERT_FILE、REQUESTS_CA_BUNDLE 等),
以及最小化的环境,并设置 NODE_USE_ENV_PROXY=1,让 Node 服务的
fetch 使用代理。如果没有这些设置,调用 API 的服务将无法连接,并会像服务本身已损坏一样失败。
扫描你使用的每个服务:scan
npx toolmenu scan ~/Library/Application\ Support/Claude/claude_desktop_config.json
npx toolmenu scan .mcp.json --format markdown
scan 会对 MCP 客户端配置(Claude Desktop、Claude
Code 的 .mcp.json、Cursor、VS Code:mcpServers 或 servers 对象)中的每个服务,或
文本文件(每行一个服务,可填 URL 或命令,也可先填 name<TAB>)中的每个服务运行 snapshot,每次处理 4 个(--concurrency),并排打印结果,然后打印每个服务的发现,如下所示:
server tools tokens findings
github 42 ~18K 1 error · 2 warn
filesystem 11 ~2.1K no findings
linear – – not checked: LINEAR_API_KEY is not set (the config refers to it)
配置中的 ${VAR}、${VAR:-default} 和 ${env:VAR} 会从环境中填入。未设置变量的服务不会以空凭据启动,而是显示为一行并说明原因。无法启动或读取的服务会导致退出代码为 2,因为它们未经过检查;标记为 disabled 的服务会列出但不做处理。打印 URL 时会省略查询部分,因为其中可能包含密钥。除非使用 --out-dir 指定了用于保存各服务菜单文件的文件夹,否则不会写入任何内容。
为构建你的服务的 Agent:mcp
toolmenu mcp 会将 toolmenu 作为 MCP 服务运行(stdio),因此 Claude Code、Cursor 或其他任何
客户端都可以在工作期间检查它正在构建的服务。在 Claude Code 中,可作为插件使用(包含服务,以及用于检查更改、检查发布和设置 CI 的技能):
/plugin marketplace add niksa90/toolmenu
/plugin install toolmenu@toolmenu
或者在任何客户端中仅使用服务:claude mcp add toolmenu -- npx -y toolmenu mcp,或在 mcpServers 配置中使用
{"command": "npx", "args": ["-y", "toolmenu", "mcp"]}。
| 工具 | 运行 |
|---|---|
snapshot_menu | snapshot:问题和令牌成本;仅在使用 save_as 时写入菜单文件 |
diff_menus | diff:两个菜单文件之间的破坏性更改、令牌变化和版本升级 |
check_session | session --auto:调用只读工具并报告每项菜单更改 |
explain_rule | explain:规则检查的内容以及如何更改其级别 |
每个工具都会在子进程中运行 CLI,并返回其文本报告,因此选项、
toolmenu.config.json 和已存储的 OAuth 登录信息都与在命令行中使用时一致。
相对路径基于 toolmenu mcp 的运行目录,或调用的 cwd 解析。
snapshot_menu 和 check_session 会像 CLI 一样启动你指定的命令;
除非你允许,否则客户端会在每次工具调用前征求同意。toolmenu 自身的菜单在两种协议和会话中均通过检查,没有发现问题(测试套件会进行验证)。
作为库使用
CLI 的所有功能都通过 toolmenu 包导出,并附带类型,供你自己的测试
套件和脚本使用:
import { snapshot, diffMenus, loadMenu, session, loadScenario } from 'toolmenu';
// The menu and its findings, as `toolmenu snapshot` gives them.
const { menu, findings } = await snapshot({ kind: 'stdio', command: 'node', args: ['dist/server.js'] });
// or { kind: 'http', url: 'https://example.com/mcp', headers: { Authorization: `Bearer ${token}` } }
// What changed since the committed baseline, and the version bump it needs.
const diff = diffMenus(await loadMenu('menu.json'), menu);
console.log(diff.suggestedBump, diff.tokens);
if (diff.findings.some((f) => f.severity === 'error')) process.exitCode = 1;
snapshot、diffMenus、session 和 history 接受与对应命令相同的选项(SnapshotOptions、DiffOptions、SessionOptions、HistoryOptions),并返回报告中打印的问题:每个 Finding 都包含其 rule、severity、
message、tool,以及显示为 → Next: 的 fix。再往下,compareMenus(before, after) 会返回每个工具的更改(ToolChange[]),而 runRules 会对你构建的菜单运行菜单规则。
menu.json 格式对应 Menu 类型(toolmenu: 1);
后续格式会升级该数字。库函数不会打印或退出:这些是 CLI 的职责。
在 CI 中:一条 PR 评论
# .github/workflows/toolmenu.yml
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
toolmenu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 22 }
- run: npm ci && npm run build
- uses: niksa90/[email protected]
with:
command: node dist/server.js
baseline: menu.json # your committed snapshot
scenario: scenario.yml # optional: run a session too
它会为 PR 的构建生成快照,将其与 PR 基础分支上的基线(或在推送标签时与上一个标签)进行比较,运行场景,并发布一条在每次推送时都会更新的评论:其中包含破坏性变更、令牌变化(“此 PR 会为每次对话增加约 1,240 个令牌”)、建议的版本升级,以及 session 捕获的所有内容。同一份报告也会显示在作业摘要中。
| 输入 | 默认值 | |
|---|---|---|
command 或 url | 如何通过 stdio 启动服务,或其 Streamable HTTP 端点 | |
headers | url 服务的 HTTP 标头,每行一个 Name: value。密钥请使用 secret:x-api-key: ${{ secrets.MCP_API_KEY }} | |
env | command 服务的环境变量,每行一个 KEY=value | |
baseline | menu.json | 用于比较的已提交快照 |
scenario | 使用 session 运行的场景,或使用 auto 根据菜单生成步骤 | |
catalog | false | 设为 true 还会比较搜索工具背后的操作(snapshot --catalog) |
baseline-from | snapshot | 对于工具在解锁后才出现的服务,session 会比较场景中见过的所有工具(菜单并集) |
release | auto | 用于版本升级检查的版本号。auto 会读取基础分支和当前分支中的 package.json、pyproject.toml 或 Cargo.toml;推送标签时,则会将该标签与上一个标签进行比较。也可以自行设置为 "1.4.0..1.5.0",或设为 off |
fail-on | error | 当发现的问题达到或高于此级别时,使作业失败 |
comment | true | 发布并更新 PR 评论 |
sarif | 还要将发现的问题写入 SARIF 的文件夹,用于代码扫描(见下文) | |
version | Action 自身的版本 | 来自 npm 的 toolmenu 版本:默认使用与你固定的 Action 标签对应的版本(最新版本使用 latest)。Action 会从 npm 安装 toolmenu,因此需要已发布的版本 |
在 Security 标签页中:代码扫描
设置 sarif 后,发现的问题也会发送到 GitHub 代码扫描:在 Security 标签页中显示警报,每个警报对应 menu.json 中其工具所在的行,并在拉取请求上显示代码扫描检查。只有当拉取请求修改了该行时,GitHub 才会在拉取请求中内嵌显示警报,因此大多数警报会保留在 Security 标签页和检查中。也请在推送到默认分支时运行此工作流,以便 GitHub 有基线可用于比较拉取请求的警报。公共仓库可免费使用代码扫描;私有仓库则需要 GitHub Advanced Security。
on:
pull_request:
push:
branches: [main] # the base GitHub compares a pull request's alerts with
permissions:
contents: read
pull-requests: write
security-events: write # to upload SARIF
steps:
# … checkout, build, as above
- uses: niksa90/[email protected]
with:
command: node dist/server.js
sarif: toolmenu-sarif
- uses: github/codeql-action/upload-sarif@v4
if: always() # findings fail the toolmenu step; upload them anyway
with:
sarif_file: toolmenu-sarif
在 Action 之外,toolmenu snapshot --sarif toolmenu.sarif -- node dist/server.js 会将文件写入常规报告所在目录(diff 和 session 也支持 --sarif),或者使用 --format sarif 直接输出。
其他 CI 系统:JUnit 测试报告
GitLab、Jenkins、Azure DevOps、CircleCI 和 Buildkite 会将 JUnit XML 显示为测试结果。--junit <path> 会将 snapshot、diff 或 session 的发现结果以 JUnit 格式写入常规报告所在目录(使用 --format junit 则直接输出)。此功能在 0.25.1 之后新增。每个发现结果都是一个以工具命名的测试用例,属于一个以规则命名的类(toolmenu.diff.diff/removed),因此 CI 系统可以跨运行跟踪它。达到或高于 --fail-on 级别的发现结果会判定为失败,其余则作为通过项并附带相应文本,因此测试报告与退出代码一致。运行无问题时会显示一个通过的测试用例。
# .gitlab-ci.yml
toolmenu:
image: node:22
script:
- npm ci && npm run build
# Exit 1 means findings: keep going so both reports are written, then fail.
- npx toolmenu snapshot --out new-menu.json --junit snapshot.xml -- node dist/server.js || code=$?
- npx toolmenu diff menu.json new-menu.json --junit diff.xml || code=$?
- exit ${code:-0}
artifacts:
when: always
reports:
junit: [snapshot.xml, diff.xml]
为 README 添加徽章
npx toolmenu snapshot --badge toolmenu-badge.json -- node dist/server.js
会写入一个 shields.io 端点文件(工具、
令牌,以及错误或警告,分别显示为红色、黄色或绿色),并输出对应的 README 行。将其与
menu.json 一起提交:基线变化时它也会变化,因此 CI 无需提交任何内容。本 README
顶部的徽章展示了 toolmenu 自己的 MCP 服务(toolmenu mcp),由其自身检查。
服务需要身份验证,或 PR 来自分支仓库和 Dependabot?请参阅 docs/github-action.md。
服务会按需解锁工具?请为基线快照完整菜单,并在默认菜单上运行会话:会解锁工具的服务。
没有密钥的 PR 会被跳过,跳过的检查仍会通过;skipped 输出会告知你。
session:Agent 工作期间不断变化的菜单
session 会运行一个脚本化会话(场景本身就是 Agent),每个步骤后都会列出菜单,
并报告每项变化及其触发步骤。
# scenario.yml
allow_writes: false # refuse to call tools not marked readOnlyHint
steps:
- list
- call: search_forms
args: { query: onboarding }
- call: unlock_toolset
args: { toolset: audits }
- wait_for: tools_list_changed # optional; timeout_ms defaults to 5000
--init 会根据实时菜单写入一个入门场景:它会调用每个无需参数的只读工具,
然后重复调用其中一个。如果某个工具看起来能解锁更多工具(带有枚举值的 domains
或 toolset 参数、名称中包含“unlock”或“capabilities”,或描述中提到启用或加载工具),
入门场景会解锁每个值,每次解锁后列出菜单,然后重复第一个值,以便会话看到解锁
后可用的所有工具。值来自参数的枚举;若没有枚举,则来自服务自身的只读列表工具
(list_toolsets)。仅用于描述工具集的查询(list_toolsets、get_toolset_tools)
不算解锁;未标记为只读的工具只会被建议,不会调用。无法填入的内容会保留为注释掉
的步骤。如果菜单中没有命名的解锁方式,例如加入一个能带来更多工具的房间,则需要
你自行添加步骤。场景的质量取决于其步骤,因此入门场景只是起点,而非上限。
在启用 --dynamic-toolsets 的 github-mcp-server 1.0.5 上(初始有 3 个工具、
19 个工具集),解锁每个工具集后可达到 107 个工具:包括 --toolsets all 提供的
全部 81 个工具,以及仅动态模式才有的 23 个 issue 和拉取请求工具。
| 规则 | 默认级别 | 捕获的情况 |
|---|---|---|
session/mid-insert、session/reorder、session/remove、session/edit | 错误 | 会话进行期间在列表末尾以外的位置更改菜单,并指出更改起始位置和更改的值。工具列表的任何更改都会使缓存的提示词失效:整个工具列表(估算其大小)及其后的对话都会重新处理 |
session/append | 警告 | 在列表末尾添加工具。如果客户端在提示词开头发送工具(Claude 的 Messages API 就是如此),对话仍会缓存未命中;只有客户端在缓存内容之后添加新工具时才不会影响缓存,例如工具搜索(延迟加载) |
session/connection-local | 2026-07-28 时为错误(HTTP) | 只有当前连接能看到的更改。2026-07-28:工具集不得“因连接而异,也不得作为该连接上其他请求的副作用”而变化 |
session/side-effect | 2026-07-28 时为警告(stdio) | 同上;stdio 无法区分更改是针对单个连接还是全局的 |
menu/process-variance、menu/connection-variance | 错误 | 在第一步之前检查,方式与下面的 snapshot 相同 |
session/unannounced | 警告 | 菜单发生更改,但服务器声明了 listChanged,却未发送 notifications/tools/list_changed |
session/untested | 警告;若没有任何调用成功则为错误 | 调用在到达工具前失败:身份验证失败、本机缺少某项内容(没有 Chrome)、网络故障。每次运行只产生一条发现,而不是每一步一条,因此过期的 CI 密钥不会让一次什么都没测试的运行通过 |
session/refused、session/step-failed | 错误 | 场景不允许的写操作,或因其他原因失败的调用 |
session/tool-error | 警告 | 工具因其他原因返回错误(isError):实际测试的内容少于表面所示。若多个步骤出现相同错误,则合并为一条发现并列出这些步骤 |
session/known-variance | 信息 | 会话中途的更改仅涉及每次 tools/list 之间本就存在差异的值:原因与其所指的发现相同,不会重复计数 |
session/assumed-read-only | 信息 | 根据你的指示调用的工具(--assume-read-only、assume_read_only),但服务器没有将其标记为 readOnlyHint |
session/unlock-coverage | 警告 | 某个解锁工具的枚举列出了多个值,但调用时只用了其中一部分:其余值对应的工具从未被发现,因此基于该会话生成的基线会漏掉它们。使用 --union-out 或 --auto 时,还包括完全没有调用的解锁工具,以及 --auto 跳过它的原因 |
session/nothing-called | 警告 | --auto 没有调用任何工具(全部为开放世界工具、缺少值或非只读工具):此次运行只列出了菜单 |
session/session-lost | 错误 | toolmenu 使用相同凭据打开第二个连接后,服务器结束了会话(每个客户端仅有一个会话的服务器;仅适用于 2025 年左右的服务器)。运行会在此处停止;使用 --processes 1 重新运行 |
session/handle-error | 警告 | 2026-07-28:--auto 使用一个不可能存在的句柄(toolmenu-unknown-session)调用了只读工具,工具返回的是协议错误,而不是模型可读取的工具错误 |
session/error-not-flagged | 警告 | 工具将其背后的 API 返回的 HTTP 错误({"status":401,"object":"error",…})作为普通结果返回,且未标记 isError,因此读取 isError 的客户端会将调用视为成功(Notion 的服务器)。身份验证错误也计入 session/untested。接收 URL 的工具不在此列:其响应来自页面(启发式判断) |
session/server-unresponsive | 错误 | 某一步之后服务器停止响应:连 tools/list 都超时。运行会在此处停止,只产生这一条发现,而不会让后续每一步都等待超时(例如在工具调用中轮询设备码登录的服务器) |
session/handle-roundtrip | 警告 | 使用 --create-handles 时:创建会话、篮子或购物车的工具已被调用,但接收该句柄的工具拒绝了刚才返回的句柄(若其结果中没有可查找的句柄,则为信息) |
session/rate-limited | 错误 | toolmenu 等待约一分钟后,服务器仍因请求过多(429)而持续拒绝请求;或者未标记 readOnlyHint 的工具报告遭到速率限制(不会再次调用)。运行会在此处停止,并列出未检查的步骤;为此次运行单独提供一个服务器实例,或提高其限制 |
session/scope-unchecked | 信息 | 会话中途无法启动第二个服务器进程或连接(例如服务器占用了文件或端口),因此未能检查更改是否为全局更改。每次运行仅报告一次 |
没有场景?使用 session --auto 会根据菜单构建步骤:先调用所有必填参数可从架构本身获知(通过 default、examples、enum、类型或格式)的只读工具,按成本从低到高排序;然后调用每个只读解锁工具的每个取值(不受 --max-calls 限制,并将第一个取值调用两次:第二次相同的解锁不应产生任何变化);最后再次调用第一个工具。它绝不会猜测 ID:需要 owner、repo 或议题键的工具会被跳过,报告会指出可用哪个标志填入参数:--value repo_path=/src/app 会为每个需要该参数的工具填入该值,--value get_issue.issue_key=ABC-1 则只为一个工具填入;--values-file values.yml 可通过映射同时填入两者。未将工具标记为 readOnlyHint 的服务(DeepWiki,以及所有 2024-11-05 的服务)不会调用任何工具,除非你通过名称明确授权:--assume-read-only read_wiki_structure,ask_wiki_question。只接受精确名称:使用模式也会涵盖后续版本新增的工具。标记为写入操作或被指定为写入操作的工具绝不会被调用,报告会说明哪些工具是在你的授权下运行的。 | ||
在 2026-07-28 启用 --create-handles 时,--auto 还会调用用于创建会话、篮子或购物车的工具(start_session;这是写入操作,因此仅在指定该标志时调用,且绝不会调用标记为 destructiveHint 的工具;不会创建锁、租约或结账),并将它返回的句柄传给接收该句柄的工具,同时也会传入虚构句柄。 | ||
--save-scenario auto.yml 会将步骤写入文件,并将跳过的工具注释掉,供你填写。标记为 openWorldHint: true 的工具(网页搜索、抓取、爬取,可能会消耗 API 额度)仅在指定 --open-world 时调用,但解锁工具除外:会话会监视菜单的变化,而且解锁不会消耗搜索额度,因此即使服务将所有工具都标记为开放世界,解锁工具仍会运行。如果运行最终没有调用任何工具,结果将是 session/nothing-called(警告),并说明每个工具未被调用的原因,而非通过检查。在 bench/ 中的 22 个服务上,--auto 可覆盖 262 个只读工具中的 60 个;指定 --open-world 时可覆盖 84 个。 |
session 会实际调用工具。 如果没有 allow_writes: true,它会拒绝调用任何未标记为 readOnlyHint: true 的工具。--plan 会在不建立连接的情况下打印步骤。
diff:比较版本
npx toolmenu diff menu.json new-menu.json
$ npx toolmenu diff --release 2026.1.14..2026.8.31 fs-2026.1.14.json fs-2026.8.31.json
toolmenu diff secure-filesystem-server 2026.1.14 → 2026.8.31
changes 1 breaking (1 tool) · 0 minor · 3 notice (13 tools)
version suggested bump: major · not checked (calendar version)
tokens ~1,640 → ~1,664 (+24, estimate): this release adds ~24 tokens to every conversation that loads the menu
tools 14 → 14
ERROR diff/safety-hint
`move_file` was marked additive-only and is now destructive (destructiveHint false → true; unset means destructive). Clients that auto-approve additive tools may now ask first.
→ Next: If the tool still never deletes or overwrites, set destructiveHint: false; if it does now, say so in the release notes.
INFO diff/annotations
Annotations changed the same way (openWorldHint (unset) → false) in 13 of 14 tools: read_file, read_text_file, read_media_file, read_multiple_files, write_file, edit_file, … (+7).
…
这是官方文件系统服务的两个真实版本。它的日历版本号不保证兼容性,因此不会判定版本升级本身,但破坏性变更仍然会报错。变更分类方式与 OpenAPI 破坏性变更检查器相同:
| 类别 | 变更 | 规则 |
|---|---|---|
| breaking(错误) | 工具被移除或重命名、参数被移除、新增必填参数、类型变得更狭窄或不同、枚举范围缩小、不再接受未列出的属性(新增 additionalProperties: false)、工具安全性降低(不再是只读,或从增量式变为破坏性);两侧均启用 --lists 时,提示词被移除或新增必需参数,资源模板被移除 | diff/tool-removed、diff/tool-renamed、diff/param-removed、diff/param-required、diff/param-type、diff/enum-narrowed、diff/properties-closed、diff/safety-hint、diff/prompt-removed、diff/prompt-argument-required、diff/resource-template-removed |
| minor(信息) | 新增工具、新增可选参数、参数现在变为可选、类型或枚举范围扩大、现在接受未列出的属性(移除 additionalProperties: false)、新增提示词或资源模板 | diff/tool-added、diff/param-added、diff/param-relaxed、diff/type-widened、diff/enum-widened、diff/properties-opened、diff/prompt-added、diff/resource-template-added |
| notice(信息) | 移除可选参数(仍允许额外属性)、描述变更(根据文本差异:这会改变 Agent 的行为)、输出架构、注解、其他字段、顺序、架构重组但接受相同输入、仅声明的 $schema 方言变更、提示词的描述或可选参数、资源新增或移除(属于数据,因此无需提升版本) | diff/param-dropped、diff/description、diff/schema-other、diff/annotations、diff/other、diff/order、diff/schema-equivalent、diff/schema-dialect、diff/prompt-changed、diff/resources |
相同规则也适用于参数内部:对象参数的字段、数组项以及 anyOf/oneOf 联合类型的选项(zod 的联合类型、判别联合类型和 .nullable())都会在每一层进行比较,并标出其路径(gen.body.text、search.filters[].field、gen.block(kind="image").url)。联合类型选项根据它们共同固定的判别字段值配对;若没有判别字段(普通的对象 z.union),则先匹配完全相同的选项,因此重新排序不算变更,然后再按类型和共有属性名称配对。选项被移除属于 breaking,新增选项则会扩大范围。同一变更通过一个共享定义出现在多个位置时,只会报告一条发现,并列出这些位置(--json 中的 places)。本地 $ref(#/$defs/…、#/definitions/…)会先展开,递归引用展开一层,因此比较的是架构接受的内容,而非其写法:将重复的区块移入 $defs 会产生一条 diff/schema-equivalent 提示,并附上令牌变化(“接受相同输入:少了约 899 个令牌”);$defs 内的 breaking 变更也会被标记为 breaking。任何未被规则归类的架构变更都不会被静默忽略:会标记为 diff/schema-other,并提示“需要审核”。
在 mongodb-mcp-server 2.1.2 → 3.0.0 的比较中,这会发现联合类型内的 aggregate.pipeline[]…$vectorSearch.numCandidates 和 .limit 从 number 收窄为 integer;过去整个 pipeline 都只会标记为“需要审核”。
同一架构的不同写法会被视为相同架构(additionalProperties 缺省、为 true 或 {};zod 4 的安全整数边界),而每个工具都共有的变更只会在菜单中报告一次:从 zod 3 升级到 zod 4 的服务会显示实际变更(某个字段变为必填、zod 4 为 email 和 uuid 新增的模式),再加上一条 diff/properties-opened(“27 个工具现在接受未列出的属性……很可能是架构生成器升级所致”),而不会对每个对象都提示“需要审核”。
同一变更影响多个工具时只报告一次,并列出受影响的工具(在 chrome-devtools-mcp 1.8.0 中,29 个工具里的 25 个新增了必填的 pageId);计数按变更计算。当某个工具移除一个参数,又新增一个名称相近的必填参数时,diff/param-renamed(警告,不确定)会指出这可能是重命名;两项变更仍都属于 breaking。每条 breaking 发现的后续步骤都会在版本提升幅度不足时注明应发布的版本。
它还会报告每个工具的令牌变化,并建议语义化版本升级。要检查升级幅度,请传入 release 版本:--release 1.4.0..1.5.0(npm、一个 git 标签)。
diff/version-bump 随后会在发布版本的升级幅度较小时发出警告。低于 1.0.0 时,它按 npm 的 caret 规则解读版本(^0.2.3 接受任何 0.2.x):破坏性变更需要升级次版本号,新功能需要升级补丁版本号;低于 0.1.0 时则不作限制。
diff/version-backwards 会在版本号降低时发出警告。日历版本和预发布版本不作判定。快照中的版本是服务报告的版本(serverInfo.version),通常并非发布版本:filesystem 服务有 19 次发布都报告 0.2.0(FINDINGS F5)。该版本会显示,且只有使用 --server-version-is-release 时才会检查。
diff 还会强制执行可选的 tokenBudget(diff/token-budget)。此处顺序变化只会显示提示:在不同发布版本之间,提示词缓存会重建。顺序在 同一个 会话中很重要。
搜索工具背后的操作(--catalog)
大型服务越来越多地把大多数操作藏在菜单之外:一个小型固定菜单、一个搜索工具和一个执行工具(Sentry 的 search_sentry_tools + execute_sentry_tool,Atlassian 的 discover + executeRead)。其中某个操作发生破坏性变更时,菜单中不会显示。snapshot --catalog 会向搜索工具发送一组固定查询,并将其返回的操作保存在 menu.json 中;随后,diff 会按照与工具相同的规则比较两个快照中都找到的操作。
npx toolmenu snapshot --catalog -- node dist/server.js
搜索只返回最匹配的结果,因此 toolmenu 会用自己的词汇遍历目录:它从搜索工具自身的示例短语和你的工具名称中的名词开始,然后搜索每个找到或被提及的操作,直到连续 20 个查询都没有发现新内容(最多 200 个)。在 Atlassian 托管服务上,这样可以找到其 21 个工具背后的全部 154 个操作;在 Sentry 上,则能找到 9 个工具背后的 65 个操作,每次运行结果都相同。查询会按间隔发送,并在遇到速率限制时等待。某个操作本次未找到时,会显示提示(diff/catalog-missing:“未由相同查询返回”),绝不会标记为“已移除”;仅本次找到的操作会标记为 diff/catalog-added;使用不同查询读取的目录或读取不完整的目录(某个查询失败)会标记为 diff/catalog-queries。要调整搜索方式,请在 toolmenu.config.json 中设置 "catalog": { "queries": [...], "maxQueries": 200, "crawl": true }(FINDINGS F15)。
通过搜索进行遍历只能得到一个下限,并不能证明结果完整:如果目录中没有任何条目提及某个操作,且没有查询使用该操作的词汇,就无法找到它。在 @sentry/mcp-server 0.42(stdio)上,它会找到该软件包目录中的 64 个操作中的 63 个;只有按名称查询时才会返回 whoami。请将这类查询添加到 catalog.queries。
命令路由器。 有些服务会通过路由器工具来提供操作:每个区域对应一个工具,该工具接受 command 及其 parameters,并在调用时传入 learn: true 来列出其命令(Azure 的 MCP 服务在约 410 个操作前提供了 71 个此类工具)。--catalog 会检测这类工具(包含 command 参数、arguments 对象、布尔型列表标志,且描述中提到“router”或“sub commands”),并以列表模式各调用一次,不传入命令,因此不会执行任何操作。若检测遗漏了某个路由器?请指定名称:"catalog": { "routers": ["keyvault", "storage"] }。路由器会列出它包含的所有内容,因此与搜索不同,目录是完整的,但失败的路由器除外(catalog/failed 会说明是哪些路由器及失败原因)。
history:深入调研软件包的各个版本
npx toolmenu history @modelcontextprotocol/server-filesystem --versions 19 --arg /tmp --csv fs.csv
它会将每个已发布版本安装到临时目录中(关闭安装脚本), 为其创建快照,并与上一个可正常运行的版本进行差异比较。对于每个版本, 它都会记录协议、菜单、发现的问题,以及实际安装的依赖版本:今天安装的旧版本会在今天解析其依赖,因此提供的菜单可能与发布时附带的菜单不同。
尽力而为:需要环境变量、参数或身份验证的软件包会记录为失败,并注明原因,绝不跳过。它会安装并运行第三方代码。请在容器中运行。
Token 的去向
snapshot 还会展示菜单的 token 都花在了哪里:最大的工具(拆分为描述和架构)、大型枚举,以及可以删减的冗余内容:
- 重复区块:同一段架构在一个工具内多处重复(可用一个
$defs条目只保留一份:“在 content_generateMessageHtml 中,一个约 230 token 的区块重复 5 次”),或在多个工具间重复(每个工具都要付费:mongodb-mcp-server 3.0.4 在 3 个工具中重复了一个约 2,200 token 的pipeline架构)。只会列出最大的重复区块,不包括其中的组成部分。 - 未使用的
$defs:工具中没有任何内容引用的定义。Notion 的服务 2.5.2 在全部 24 个工具中都附带相同的 9 个定义,占其菜单的 72%。
菜单文件中包含每个工具的估算值,适用于摘要中未列出的工具:
jq -r '.tools[] | "\(.tokens)\t\(.name)"' menu.json | sort -rn。
snapshot 规则
每条规则都源于我为一个 MCP 服务构建 115 个工具时实际遇到的故障,或直接来自规范。如果某条规则无法对应到其中之一,就不会纳入。
| Rule | Default | Catches |
|---|---|---|
menu/nondeterministic | error | 两次相同的 tools/list 调用返回不同的菜单(顺序、描述、架构、注释),并指出不同之处 |
menu/process-variance | error | 以相同方式启动的第二个服务进程提供了不同的菜单,因此每次重启都会错过缓存。曾在 mcp-atlassian 中发现:默认值由 Python 集合生成,每次启动时顺序都会变化(FINDINGS F12)。toolmenu 使用 PYTHONHASHSEED=0 运行主进程,使 Python 服务保存的菜单可重现,再使用另一个种子运行第二个进程。--processes 1 可关闭此检查 |
menu/connection-variance | error(2025 协议下为 warn) | HTTP 上出现相同情况:使用相同凭据的第二个连接得到不同菜单。报告前会重试一次 |
spec/schema | error | tools/list 的结果不符合其协议版本的官方架构 |
spec/tools-capability | error | 服务未声明 tools 能力,因此客户端(包括官方 SDK)不会请求其工具 |
menu/duplicate-name | error | 两个工具名称相同:只能调用其中一个,而且某些客户端会拒绝该列表 |
naming/route | error | routes.yml 中的预期失效:某个关键词现在匹配错误工具的程度至少与正确工具相同 |
description/buried | warn | 面向 Agent 的指令(“改用 X”“不要重试”“绝不要猜一个”)位于客户端截断描述之后:Claude Code 中,通过工具搜索加载的工具(默认方式)上限为 16,384 个字符,预先加载的工具上限为 4,096 个字符,或使用你的 descriptionLimit。实际失败案例:客户端在 280 个字符处截断,而决定路由的那一行位于第 1,222 个字符处 |
description/cut | info | 描述长度超过客户端发送的长度,且截断后没有看起来像指令的内容;汇总显示每条描述的截断位置。模型收到的前缀看起来可能是完整的 |
description/late-instruction | info | 使用默认截断长度时,位于第 280 个字符之后的指令会被截短描述的客户端隐藏;汇总列出这些指令。设置 descriptionLimit 后,description/buried 会检查实际截断位置。(0.13 之前,此项属于 description/cut;该 ID 的设置仍然适用。) |
naming/vague-id | warn | 参数仅命名为 id,未说明指的是哪种对象;汇总中会给出建议名称 |
ids/authored | info | Agent 必须提供某个 ID,但没有工具似乎会返回该 ID,因此 Agent 可能会编造它;每种 ID 单独报告一项(启发式判断:URL 中的 ID 或其他工具文本输出中的 ID 看起来相同) |
ids/handle-lifetime | info | 2026-07-28:创建句柄供另一个工具接收的工具(start_session → session_id)未说明句柄的有效时长(启发式判断) |
write/unannotated | warn | 名称类似写入操作(delete_、send_……)但没有注释的工具;按名称所暗示的注释分组汇总 |
naming/shared-word | info | 在不同工具中指代不同事物的名词(list_team_audits 与 get_audit_trail);汇总列出这些名词 |
write/read-only-mismatch | warn | 标记为 readOnlyHint: true,但名称表示写入操作(create_my_account)的工具:客户端会根据该提示采取行动,而 --auto 不会仅凭该提示调用它(启发式判断) |
write/no-dry-run | info | 没有 dry-run 参数或预览工具的破坏性工具;汇总列出这些工具 |
spec/discover | info | 服务仅支持 2025 协议,因此跳过 2026-07-28 规则 |
spec/deprecated, spec/cache-hints | info | 2026-07-28:仍在公开已弃用功能;ttlMs: 0;或接受了凭据的服务设置了 cacheScope: "public" |
lists/variance | warn | 使用 --lists 时:两次调用之间,或与第二个进程或连接相比,prompts/list、resources/list 或 resources/templates/list 结果不同(2026-07-28:列表不再因连接而异) |
lists/schema | error | 使用 --lists 时:列表结果与官方架构不匹配(2026-07-28 要求每项都包含 ttlMs 和 cacheScope),或已声明的列表未能响应(warn;若为 Method not found 或缺少凭据等设置失败,则为 info) |
naming/client-name | warn | 使用 --client 时:客户端 API 拒绝的工具名称。Claude API 要求 ^[a-zA-Z0-9_-]{1,128}$,OpenAI 的函数要求 ^[a-zA-Z0-9_-]+$ 且最多 128 个字符。MCP 允许 files.read 这类名称,但这两个客户端都无法直接接受 |
menu/auth-variance | info | 使用 --as(或配置中的 identities)时:工具集因身份而异;报告每个身份相对于第一个身份新增、缺少或定义不同的内容,以及其令牌。规范允许这种情况;以一个身份的菜单为基线无法覆盖其他身份。无法列出工具的身份也会在此报告 |
spec/cache-scope-public | warn | 2026-07-28,使用 --as 时:工具集因身份而异,且 tools/list 声明了 cacheScope: "public",这会导致共享缓存将一个调用者的列表提供给另一个调用者 |
menu/capability-dependent | info | 服务仅向支持 elicitation 的客户端,或仅向不支持 elicitation 的客户端列出的工具。snapshot 会以支持 elicitation 的客户端身份列出工具,因此菜单文件会包含这些工具(github-mcp-server 1.14 只向能够请求用户确认的客户端列出 delete_repository;0.22 之前基线会漏掉它),然后再以不支持的客户端身份列出一次 |
menu/network-dependent | warn | 使用 --offline-check(stdio)时:再次列出菜单,此时网络已断开(代理变量指向已关闭的端口,但回环地址不受影响);若菜单不同则报告:inite-visibility 离线时提供 3 个工具,在线时提供 10 个,因此通过网络生成的基线与无法联网的机器不匹配。无法离线启动的服务会报告为 info。忽略代理变量的服务仍可直接通过机器的网络连接;这种情况下不会报告任何问题,但这不代表检查通过 |
auth/invalid-accepted | warn | 使用凭据的 HTTP 连接:使用相同方案中的无效凭据再次列出菜单。若服务仍提供菜单而不是返回 401,令牌已过期的客户端就没有理由重新登录;Agent 只会发现可用工具变少(Atlassian Rovo 对任何 Authorization 标头都提供 4 个工具)。--processes 1 可关闭此检查 |
包含 diff 和 session 规则在内的完整列表见 docs/RULES.md。 |
命名和 ID 规则是启发式规则,经过真实服务(FINDINGS F7 和 F10)以及一个包含 115 个工具的私有服务调优。它们只是额外规则;变更检查才是重点。
使用 routes.yml 固定路由。 两个工具争用同一个词时,需要添加回归测试,而不是改个名字后就忘了:
audit:
must_match: [list_team_audits]
must_not_match: [get_audit_trail]
audit trail:
must_not_match: [get_store_scores] # on its own: must not match at all
匹配采用关键词匹配,就像关键词搜索或 BM25 工具搜索查看菜单时一样:路由中的每个词都出现在工具名称或描述中,该工具就会匹配(完整短语的得分更高)。它无法识别“不是”,这些搜索也一样:像“不是审计跟踪”这样的免责声明,会把“审计跟踪”放进你本想避开的那个工具中,toolmenu 会指出这一点。
配置(可选的 toolmenu.config.json):
{
"rules": { "naming/shared-word": "off", "ids/authored": "error" },
"ignore": ["debug_*"],
"routes": "routes.yml",
"tokenBudget": 4000,
"descriptionLimit": "claude-code",
"fullDescriptions": ["search_capabilities", "lookup_*"],
"offlineCheck": true
}
offlineCheck 会为每个 stdio 服务的 snapshot 启用 --offline-check。
descriptionLimit 用于指定客户端截断工具描述的位置:可以是一个数字,也可以是一个已报告截断长度的客户端:"claude-code"(默认值;根据 Claude Code 自身文档 code.claude.com/docs/en/mcp,读取日期为 2026-11-02:通过默认启用的工具搜索加载的工具为 16,384 个字符,预先加载且标记为 _meta["anthropic/alwaysLoad"]: true 的工具为 4,096 个字符;v2.1.296 之前的截断长度为 2,048)、"claude-code-upfront"(关闭工具搜索时,Claude Code 对每个工具均为 4,096 个字符)或 "amazon-q"(10,024 个字符,报告见 makenotion/notion-mcp-server#145)。完整发送描述的客户端无需额外设置。如果你自己的客户端截断得更短(我的客户端曾截断到 280 个字符),请设置相应数字。fullDescriptions 列出未截断发送的工具,因此描述规则会跳过这些工具。
检查某个客户端(--client)
Agent 看到的内容取决于其客户端。snapshot --client <name>(或配置中的 "client")会根据该客户端自身文档所述检查菜单;toolmenu 使用的每个值都会注明来源和读取日期(src/clients.ts)。客户端未说明的内容会被省略,不会进行猜测。
--client | 预先加载 | 搜索 | 描述截断长度 | 名称 |
|---|---|---|---|---|
claude-code | 工具名称;标记为 _meta["anthropic/alwaysLoad"] 的工具会加载完整定义 | 读取最多 4,096 个字符的描述 | 搜索时为 16,384,预先加载时为 4,096 | Claude API 的规则 |
claude-api-search | 不加载你延后加载的工具(保持最常用的 3–5 个工具处于加载状态) | 名称、描述、参数名称和描述;默认返回 5 个结果 | 未说明 | ^[a-zA-Z0-9_-]{1,128}$ |
openai | 每个延后加载的函数都会加载名称和描述(其架构按需加载);延后加载的 MCP 服务只显示其自身的名称和描述 | 未说明结果数量和字段,因此不会运行 search/findable | 未说明 | ^[a-zA-Z0-9_-]+$,最多 128 个字符 |
snapshot 会添加一行,例如 as Claude Code: up front ~524 tokens · on demand ~344 per tool a search loads (largest: update_contacts, ~2,093);--json 则会添加一个 client 对象。对于会延后加载工具的客户端,无论菜单大小如何,都会运行 search/findable。
toolmenu 无法发现的问题
toolmenu 只能看到服务端发送的内容。很多 Agent 故障发生在其他地方:
- 路由到没有工具的功能。 如果 Agent 想用的功能不在你的菜单中,菜单检查就无法发现这个缺口。
- 客户端如何处理菜单。 截断描述、对工具排序或筛选、合并多个服务端。描述规则采用 Claude Code 报告的截断长度,除非你设置了自己的限制(
descriptionLimit);它们无法看到你的客户端。 - 你的代码和日志。 对两种不同操作都只回答是或否的能力检查,或者无论某件事是否发生、内容都相同的日志行。这些都在客户端或服务端代码中。
- Agent 是否真的选对了工具。 这需要使用模型进行评测。toolmenu 特意保持确定性。
选项
--out <path> where to write the menu (default: menu.json)
--no-write don't write the menu file
--routes <path> routes.yml
--config <path> config file (default: toolmenu.config.json, if present)
--format <fmt> text (default), json, github (annotations), markdown, sarif
(code scanning) or junit (CI test reports)
--junit <path> snapshot, diff, session: also write the findings as JUnit XML
--fail-on <level> error (default), warn or info
--header <k: v> HTTP header, repeatable
--env <K=V> env var for a stdio server, repeatable (it only gets a
minimal environment otherwise)
--timeout <ms> per-request timeout (default: 30000)
--protocol <p> auto (default), legacy (2025 handshake only) or modern
(2026-07-28 only; an error if the server lacks it)
--processes <n> server processes or connections to compare (default: 2);
1 opens no second one, session's scope check included
--lists <kinds> snapshot: also read and check prompts, resources (comma-separated);
saved in the menu file beside the tools
--client <name> snapshot: check the menu for one client, from its own docs:
claude-code, claude-api-search or openai
--as <name>:<cred> snapshot: list the menu as this identity too, repeatable:
"reader:Authorization: Bearer …" (HTTP) or "admin:ROLE=admin"
(stdio). The first is the menu saved; the others are compared
--offline-check snapshot (stdio): also list the menu with the network cut off
--concurrency <n> scan: servers checked at a time (default: 4)
--out-dir <dir> scan: write each server's menu file there, as <name>.json
退出代码:0 无问题 · 1 存在达到或超过 --fail-on 阈值的问题 · 2 无法连接(对于 scan,表示有服务端未经过检查)或用法错误。toolmenu <command> --help 会列出该命令自己的选项。
局限性
- 服务端顺序不等于客户端最终顺序。 客户端会合并来自多个服务端的工具,并且可能对它们排序或筛选。稳定的服务端菜单是缓存命中的必要条件,但并非充分条件。toolmenu 衡量的是服务端可控的部分。
- 令牌数是估算值(
o200k_base),用于估算模型读取的内容:每个工具的名称、描述和输入架构。输出架构、注释、图标和_meta由客户端处理,不计入统计。不同供应商的分词、缓存和计费方式各不相同。diff会用运行它的 toolmenu 重新统计两个菜单,因此旧版本生成的基准数据仍可公平比较;其他地方引用的数字(例如提交消息或仪表板中的数字)应注明使用哪个版本的 toolmenu 统计(0.7.1 调整了含图标菜单的统计结果,最多相差 5 倍,FINDINGS F7)。 - 启发式规则会在消息中注明。
- 它不是安全扫描器,也不是完整的规范符合性套件。其他工具在这些方面表现出色。
关于
由 Nikša Perović 开发。当时他在运行一个包含 115 个工具的 MCP 服务,并看到 Agent 被其菜单绊倒。每条规则都源自其中一次故障,相关研究见 docs/FINDINGS.md。开发过程的故事,以及它曾经出错的地方,见 niksa.me。
发现了误报,或者 toolmenu 应该发现但未能发现的故障?
请提交问题,附上菜单(menu.json 即可)和你的预期。变更记录见
CHANGELOG.md。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。