mcpField
T
toolmenu

toolmenu

本地服务
github.com开发工具01 天前更新

关于 toolmenu

toolmenu 是用于测试 MCP 服务工具菜单的 Node.js 命令行工具和库,可跨会话、客户端与版本检查变化。它面向 MCP 服务开发者,以确定性检查在 CI 中发现兼容性问题,不使用大模型。

主要功能

  1. 01快照并验证工具菜单
  2. 02检测会话中的菜单变化
  3. 03比较版本差异与版本号
  4. 04检查客户端可见的工具
  5. 05扫描 MCP 客户端配置
  6. 06在 CI 中运行并生成 PR 报告

适用场景

  1. 01发布前发现破坏性工具变更
  2. 02跟踪会话期间的菜单变化
  3. 03检查特定客户端看到的工具
  4. 04批量审查已配置的 MCP 服务

MCP 相关

toolmenu 也可通过 stdio 作为 MCP 服务运行,向客户端提供菜单快照、差异比较、会话检查和规则说明工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"niksa90-toolmenu": {
"command": "npx",
"args": [
"-y",
"toolmenu"
]
}
}
}
启动命令npx -y toolmenu

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmtoolmenu0.27.1

工具

4

收录的工具列表 · 更新于 今天

参数类型必填说明
cwdstring否运行目录:服务的启动位置、读取 toolmenu.config.json 的位置,以及相对路径的解析基准。默认值:运行 toolmenu mcp 的位置。
envobject否命令服务器的环境变量。服务器会获得最小化环境,以及此处提供的变量。
urlstring否Streamable HTTP 端点:https://example.com/mcp。填写此项或 command。
clientenum否还要检查此客户端加载菜单时的情况:它会删减、发现和拒绝哪些内容。claude-code | claude-api-search | openai
catalogboolean否还要读取搜索工具或命令路由器背后的操作。
commandarray否启动 stdio 服务的命令,每项一个单词:["node", "dist/server.js"]。填写此项或 url。
headersobject否url 服务的 HTTP 标头:{"Authorization": "Bearer ..."}。
save_asstring否将菜单写入此处(基线文件为 menu.json),路径相对于 cwd。默认:不写入。

详细介绍

toolmenu 标志:一个插头和工具列表,其中一个工具已勾选

toolmenu

检查 MCP 服务的工具列表:在用户遇到之前,捕获会让 Agent 困惑、破坏提示词缓存或导致客户端出问题的更改。

npm ci license: MIT MCP: 2026-07-28 and 2025 toolmenu toolmenu MCP server on Glama

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

toolmenu 会话输出:工具被插入列表中间,一段描述被编辑,每项都关联到导致该变化的步骤

它能发现什么

会话中途改变的菜单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_menusnapshot:问题和令牌成本;仅在使用 save_as 时写入菜单文件
diff_menusdiff:两个菜单文件之间的破坏性更改、令牌变化和版本升级
check_sessionsession --auto:调用只读工具并报告每项菜单更改
explain_ruleexplain:规则检查的内容以及如何更改其级别

每个工具都会在子进程中运行 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 端点
headersurl 服务的 HTTP 标头,每行一个 Name: value。密钥请使用 secret:x-api-key: ${{ secrets.MCP_API_KEY }}
envcommand 服务的环境变量,每行一个 KEY=value
baselinemenu.json用于比较的已提交快照
scenario使用 session 运行的场景,或使用 auto 根据菜单生成步骤
catalogfalse设为 true 还会比较搜索工具背后的操作(snapshot --catalog)
baseline-fromsnapshot对于工具在解锁后才出现的服务,session 会比较场景中见过的所有工具(菜单并集)
releaseauto用于版本升级检查的版本号。auto 会读取基础分支和当前分支中的 package.json、pyproject.toml 或 Cargo.toml;推送标签时,则会将该标签与上一个标签进行比较。也可以自行设置为 "1.4.0..1.5.0",或设为 off
fail-onerror当发现的问题达到或高于此级别时,使作业失败
commenttrue发布并更新 PR 评论
sarif还要将发现的问题写入 SARIF 的文件夹,用于代码扫描(见下文)
versionAction 自身的版本来自 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-local2026-07-28 时为错误(HTTP)只有当前连接能看到的更改。2026-07-28:工具集不得“因连接而异,也不得作为该连接上其他请求的副作用”而变化
session/side-effect2026-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 个工具时实际遇到的故障,或直接来自规范。如果某条规则无法对应到其中之一,就不会纳入。

RuleDefaultCatches
menu/nondeterministicerror两次相同的 tools/list 调用返回不同的菜单(顺序、描述、架构、注释),并指出不同之处
menu/process-varianceerror以相同方式启动的第二个服务进程提供了不同的菜单,因此每次重启都会错过缓存。曾在 mcp-atlassian 中发现:默认值由 Python 集合生成,每次启动时顺序都会变化(FINDINGS F12)。toolmenu 使用 PYTHONHASHSEED=0 运行主进程,使 Python 服务保存的菜单可重现,再使用另一个种子运行第二个进程。--processes 1 可关闭此检查
menu/connection-varianceerror(2025 协议下为 warn)HTTP 上出现相同情况:使用相同凭据的第二个连接得到不同菜单。报告前会重试一次
spec/schemaerrortools/list 的结果不符合其协议版本的官方架构
spec/tools-capabilityerror服务未声明 tools 能力,因此客户端(包括官方 SDK)不会请求其工具
menu/duplicate-nameerror两个工具名称相同:只能调用其中一个,而且某些客户端会拒绝该列表
naming/routeerrorroutes.yml 中的预期失效:某个关键词现在匹配错误工具的程度至少与正确工具相同
description/buriedwarn面向 Agent 的指令(“改用 X”“不要重试”“绝不要猜一个”)位于客户端截断描述之后:Claude Code 中,通过工具搜索加载的工具(默认方式)上限为 16,384 个字符,预先加载的工具上限为 4,096 个字符,或使用你的 descriptionLimit。实际失败案例:客户端在 280 个字符处截断,而决定路由的那一行位于第 1,222 个字符处
description/cutinfo描述长度超过客户端发送的长度,且截断后没有看起来像指令的内容;汇总显示每条描述的截断位置。模型收到的前缀看起来可能是完整的
description/late-instructioninfo使用默认截断长度时,位于第 280 个字符之后的指令会被截短描述的客户端隐藏;汇总列出这些指令。设置 descriptionLimit 后,description/buried 会检查实际截断位置。(0.13 之前,此项属于 description/cut;该 ID 的设置仍然适用。)
naming/vague-idwarn参数仅命名为 id,未说明指的是哪种对象;汇总中会给出建议名称
ids/authoredinfoAgent 必须提供某个 ID,但没有工具似乎会返回该 ID,因此 Agent 可能会编造它;每种 ID 单独报告一项(启发式判断:URL 中的 ID 或其他工具文本输出中的 ID 看起来相同)
ids/handle-lifetimeinfo2026-07-28:创建句柄供另一个工具接收的工具(start_session → session_id)未说明句柄的有效时长(启发式判断)
write/unannotatedwarn名称类似写入操作(delete_、send_……)但没有注释的工具;按名称所暗示的注释分组汇总
naming/shared-wordinfo在不同工具中指代不同事物的名词(list_team_audits 与 get_audit_trail);汇总列出这些名词
write/read-only-mismatchwarn标记为 readOnlyHint: true,但名称表示写入操作(create_my_account)的工具:客户端会根据该提示采取行动,而 --auto 不会仅凭该提示调用它(启发式判断)
write/no-dry-runinfo没有 dry-run 参数或预览工具的破坏性工具;汇总列出这些工具
spec/discoverinfo服务仅支持 2025 协议,因此跳过 2026-07-28 规则
spec/deprecated, spec/cache-hintsinfo2026-07-28:仍在公开已弃用功能;ttlMs: 0;或接受了凭据的服务设置了 cacheScope: "public"
lists/variancewarn使用 --lists 时:两次调用之间,或与第二个进程或连接相比,prompts/list、resources/list 或 resources/templates/list 结果不同(2026-07-28:列表不再因连接而异)
lists/schemaerror使用 --lists 时:列表结果与官方架构不匹配(2026-07-28 要求每项都包含 ttlMs 和 cacheScope),或已声明的列表未能响应(warn;若为 Method not found 或缺少凭据等设置失败,则为 info)
naming/client-namewarn使用 --client 时:客户端 API 拒绝的工具名称。Claude API 要求 ^[a-zA-Z0-9_-]{1,128}$,OpenAI 的函数要求 ^[a-zA-Z0-9_-]+$ 且最多 128 个字符。MCP 允许 files.read 这类名称,但这两个客户端都无法直接接受
menu/auth-varianceinfo使用 --as(或配置中的 identities)时:工具集因身份而异;报告每个身份相对于第一个身份新增、缺少或定义不同的内容,以及其令牌。规范允许这种情况;以一个身份的菜单为基线无法覆盖其他身份。无法列出工具的身份也会在此报告
spec/cache-scope-publicwarn2026-07-28,使用 --as 时:工具集因身份而异,且 tools/list 声明了 cacheScope: "public",这会导致共享缓存将一个调用者的列表提供给另一个调用者
menu/capability-dependentinfo服务仅向支持 elicitation 的客户端,或仅向不支持 elicitation 的客户端列出的工具。snapshot 会以支持 elicitation 的客户端身份列出工具,因此菜单文件会包含这些工具(github-mcp-server 1.14 只向能够请求用户确认的客户端列出 delete_repository;0.22 之前基线会漏掉它),然后再以不支持的客户端身份列出一次
menu/network-dependentwarn使用 --offline-check(stdio)时:再次列出菜单,此时网络已断开(代理变量指向已关闭的端口,但回环地址不受影响);若菜单不同则报告:inite-visibility 离线时提供 3 个工具,在线时提供 10 个,因此通过网络生成的基线与无法联网的机器不匹配。无法离线启动的服务会报告为 info。忽略代理变量的服务仍可直接通过机器的网络连接;这种情况下不会报告任何问题,但这不代表检查通过
auth/invalid-acceptedwarn使用凭据的 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,096Claude 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

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。