whichtool

whichtool

Enables measuring and reporting which tool a model selects from an MCP server's tool list, without ever executing tools. Provides inspection, trial runs, and confusion analysis to benchmark routing accuracy.

Category
访问服务器

README

whichtool

<p align="center"> <img src="assets/whichtool.png" alt="One tool surface, four tools, one selected." width="160"> </p>

Does the model actually pick the right tool from your MCP server?

Italiano

[!WARNING] Publication is temporarily paused. Automated releases are disabled, and the npm package may be unavailable while the public GitHub repository remains online. The registry and Action instructions below are intentionally retained for a possible future republication. To use the current source now:

git clone https://github.com/mattagame/whichtool.git
cd whichtool
bun install
bun run ./src/cli/main.ts inspect ./tools.json

An MCP server can have valid schemas and still be unreadable to a model. Ship list_users and search_users with similar descriptions and the model guesses. Schema validation still passes. Integration tests pass too, because they call the right tool by construction.

whichtool puts that surface in front of a real model and reports which tool gets picked and which pairs get confused.

whichtool never executes a tool. It reads tools/list, records what the model would have called, and stops.

It is deliberately a single-turn routing benchmark. It measures the model's tool-selection decision on a prepared set of intents; it does not evaluate multi-step agent execution, semantic argument correctness beyond a shallow schema check, tool results, recovery, or the quality of a final answer.

Every call proposed in that turn is retained in the JSON report's trials[].calls; the first-call fields remain a compatibility view, not a reason to discard additional calls.

It does two jobs:

  • inspect — token budget, contradictory annotations, near-identical descriptions, invalid x-mcp-header values. No model call or model-provider key; a live target may still require its own authorization.
  • run — trials, permuted tool order, confusion matrix, rates with Wilson 95% intervals.

inspect warns when a surface exposes more than 6 tools. Real CLI, MCP, and GitHub Action runs stop before calling a model above that default. After reviewing the surface, an operator can raise the limit with --max-tools N, trials.maxTools, the MCP startup flag, or the Action's max-tools input; 1,000 is the hard maximum. Six is a cautious default, not a universal rule: more tools can increase ambiguity and prompt size, but the right number depends on the model, schemas, descriptions, and tasks. Also set --max-context-tokens so a small number of unusually large tools cannot bypass the context budget.

Those Wilson intervals describe trial-level stability on the tasks in the file. Repeating a task measures whether that same routing decision is stable; it does not estimate how the model will perform on unseen intents.

Install

npx whichtool inspect ./tools.json
# or: bunx whichtool inspect ./tools.json
npm install --save-dev whichtool

Requires Node 20.11+ or Bun 1.3+. Zero runtime dependencies.

Standalone binaries are not published yet. Bun-compiled executables embed third-party runtime components, so distribution stays disabled until their redistribution notices have been reviewed and can ship with every binary. This is separate from the temporary package publication pause above; use the source checkout while that pause is in effect.

Quick start

# 1. Look at the surface (no model-provider key)
whichtool inspect ./tools.json
whichtool inspect https://example.com/mcp
whichtool inspect --transport stdio "bun run ./src/server.ts"

# Capture once, work offline afterwards
whichtool inspect --transport stdio "npx -y @modelcontextprotocol/server-filesystem ." \
  --save-snapshot ./tools.json

Snapshots may be { "tools": [ … ] }, a JSON-RPC tools/list envelope, or a bare array.

# 2. Write a task set (whichtool.tasks.yaml)
version: 1
tasks:
  - id: users.list.basic
    prompt: 'Show me all the users in the workspace'
    expected: list_users
  - id: users.search.byname
    prompt: "Find the user whose name contains 'rossi'"
    expected: search_users
  - id: distractor.delete
    prompt: 'Permanently delete the account belonging to Rossi'
    expected: null

expected must be written even when it is null. Full format: docs/task-sets.md.

# Or draft one instead of writing step 2 by hand, then edit and commit the result
# (do not regenerate on every run). It refuses to overwrite without --force.
whichtool tasks generate ./tools.json --provider ollama --model qwen3:4b --out whichtool.tasks.yaml

# Seeded robustness variants, no model
whichtool tasks mutate --out whichtool.tasks.mutated.yaml --seed 0

# 3. Lint before spending anything
whichtool tasks lint ./tools.json --tasks ./whichtool.tasks.yaml

# 4. Preview the workload (no model call)
whichtool run ./tools.json --provider ollama --model qwen3:4b --repeat 5 --dry-run

# 5. Measure
whichtool run ./tools.json --provider ollama --model qwen3:4b --repeat 5
OPENAI_API_KEY=sk-… whichtool run ./tools.json --provider openai --model gpt-4.1-mini

--repeat defaults to 5 per selected task, so total trials are the tasks left after --only / --skip, multiplied by repeat. A real run refuses more than 50 total trials by default. After reviewing --dry-run, raise that budget with --max-trials N or trials.maxTrials; 1,000 is an absolute, non-overridable maximum.

The dry-run prompt-token figure is a lower bound, not a price estimate. Output and reasoning tokens are additional and can be much larger. Automatic retries are disabled by default for the built-in HTTP providers.

During whichtool run, press Ctrl+C to abort in-flight provider requests. The command exits with code 130 and does not write a partial report. MCP evaluations remain cancellable through the MCP protocol.

Exit codes: 0 execution was healthy and thresholds held, 1 a quality threshold failed, 2 an execution error (including an incomplete run or too many provider failures). By default a run needs at least one scored trial and permits at most a 10% provider-error rate; override these with --min-scored and --max-error-rate.

# 6. Re-render, gate, compare
whichtool run … --format json --out run.json
whichtool report run.json --format markdown
whichtool report run.json --format html --out report.html
whichtool diff base-run.json head-run.json --max-accuracy-drop 0.05

diff refuses to subtract runs that used a different model, endpoint, non-secret provider request fingerprint, temperature, seed, repeat count, permutation setting, or task set. It matches outcomes by task and trial index, then uses an exact two-sided paired sign test (p <= 0.05) to decide whether a movement is distinguishable. A distinguishable increase in unexpected multi-call behaviour is a regression even when the first picks did not move.

Commands

Command What it does
whichtool inspect <target> Surface lint. No model call or model-provider key.
whichtool mcp Expose prepared routing-evaluation operations over MCP.
whichtool tasks lint [target] Validate a task set.
whichtool tasks generate <target> Draft a task set from the tool descriptions.
whichtool tasks mutate Seeded robustness variants. No model.
whichtool run <target> Execute trials and write a report.
whichtool report <run.json> Re-render a saved run.
whichtool diff <base> <head> Compare two saved runs.
whichtool cache info|clear Inspect or clear the trial cache.

whichtool <command> --help lists flags. Main flags on run:

--tasks --provider --model --repeat --max-trials --max-tools --concurrency --temperature --seed
--min-scored --max-error-rate
--permute / --no-permute --format --out --min-accuracy --max-over-trigger
--max-context-tokens --only --skip --dry-run --seconds-per-trial --reasoning-effort
--cache / --no-cache --cache-dir

Formats: terminal, json, markdown, html, junit, badge.

Environment: an HTTP-target credential needs both WHICHTOOL_HTTP_AUTHORIZATION and the exact allowed origin in WHICHTOOL_HTTP_AUTHORIZATION_ORIGIN (for example https://mcp.example). Remote credentials require HTTPS. Provider keys come from ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, TOGETHER_API_KEY, and WHICHTOOL_PROVIDER_API_KEY for an openai-compatible endpoint. NO_COLOR / FORCE_COLOR are honoured.

Transport Notes
snapshot Captured tools/list on disk. What CI should use.
http Streamable HTTP (MCP 2026-07-28).
stdio Locally launched server.
legacy-sse Refused. Deprecated since MCP 2025-03-26.

Providers: anthropic, ollama, openai, openai-chat, openrouter, together, vllm, any openai-compatible endpoint, and a deterministic mock. openai uses the OpenAI Responses API. Select openai-chat explicitly for OpenAI Chat Completions; the other OpenAI-compatible presets continue to use their chat-completions endpoints.

anthropic speaks the Messages API rather than a chat-completions dialect. That provider does not send temperature or seed and records those capabilities as unsupported, so its runs lean on --repeat and the trial-level intervals instead.

Configuration

import { defineConfig } from 'whichtool'

export default defineConfig({
  target: { transport: 'stdio', command: 'bun run ./src/server.ts' },
  tasks: './whichtool.tasks.yaml',
  provider: { name: 'ollama', model: 'qwen3:4b' },
  trials: {
    repeat: 5,
    maxTrials: 50,
    maxTools: 6,
    permute: true,
    temperature: 0,
    concurrency: 4,
  },
  thresholds: {
    minAccuracy: 0.9,
    maxOverTrigger: 0.05,
    maxContextTokens: 4000,
    maxErrorRate: 0.1,
    minScored: 1,
  },
  report: { formats: ['terminal', 'json'], out: './whichtool-report' },
})

whichtool.config.json works too. API keys are never a config field. The normal CLI can also discover JavaScript or TypeScript config; the MCP server intentionally does not, as explained below.

CI

- uses: mattagame/whichtool@v0.1.0
  with:
    target: ./tools.json
    tasks: ./whichtool.tasks.yaml
    provider: openai
    model: gpt-4.1-mini
    max-trials: '50'
    max-tools: '6'
    min-accuracy: '0.9'
    max-over-trigger: '0.05'

Trial caching in the composite action is disabled by default because a cache can contain prompts, tool definitions, and provider responses. Set cache: 'true' only when that material is non-sensitive and GitHub-hosted persistence is acceptable.

The Action blocks a measured invocation above 6 tools by default; max-tools can raise the limit only up to 1,000. Its max-trials budget applies to each measured invocation. A comparison workflow that measures both the head and base revisions can therefore use the trial budget once for each run; with the default, that is at most 50 trials for head and 50 for base.

Omit provider to run only the free static pass: inspect, plus tasks lint when a task set is present. A full workflow (including a base-branch comparison written to the job summary) is in examples/github-action.

As an MCP server:

{
  "mcpServers": {
    "whichtool": {
      "command": "npx",
      "args": ["-y", "whichtool", "mcp", "--config", "whichtool.config.json"]
    }
  }
}

The MCP server is deliberately capability-limited by its startup arguments. It does not auto-discover or execute JavaScript/TypeScript config: pass a reviewed JSON file explicitly with --config. Tool calls use the configured target and cannot replace it with an arbitrary path, URL, or subprocess. Agent-selected task/report inputs must stay in the working directory.

The intended agent workflow starts from evaluation artifacts you have already prepared and reviewed: inspect_surface, validate_task_file, run_evaluation, then diff_saved_results on saved runs. The MCP surface does not generate or mutate task sets. It exposes the same single-turn routing benchmark; it is not an evaluator or executor for a complete agent workflow. run_evaluation can always produce a dry-run plan, but cannot contact a provider unless the operator starts the server with --allow-paid-runs. The operator-owned real-run budget is 50 total trials by default; only the startup --max-trials flag or trials.maxTrials in the reviewed config can raise it, up to the absolute maximum of 1,000. The agent cannot override that budget. The same operator-owned rule applies to the 6-tool default through startup --max-tools or trials.maxTools, with an absolute maximum of 1,000. repeat and concurrency also have caps. A full run returns a compact summary. Add --result-file ./latest-run.json to keep the complete report outside model context. --allow-dynamic-targets exists for isolated development setups and should be treated as an unsafe opt-in. Provider/model overrides are likewise config-only unless the operator adds --allow-provider-overrides. Persistent trial caching is off in MCP mode; the operator must add --cache explicitly after deciding that prompts, calls and responses may be written to disk.

Examples

Example What it shows
quickstart The full loop on a surface you can run locally.
ambiguous-server A deliberately unreadable surface.
ollama-qwen3 A local-model run that disagrees with the static lint.
github-action CI wiring with a base-branch diff.

On reasoning models such as qwen3, a single trial can take tens of seconds of thinking tokens whichtool never reads. Measure one trial, then pass --dry-run --seconds-per-trial. Its prompt-token total remains a lower bound, not a price estimate; output and reasoning tokens are additional.

Development

Bun is the toolchain; Node is the distribution target. src/core/ is portable TypeScript (no Bun/Node builtins).

bun install
bun test
bun run typecheck
bun run lint
bun run build
docker run --rm -v "$PWD:/work" ghcr.io/mattagame/whichtool inspect ./tools.json

Patches welcome: CONTRIBUTING.md lists the constraints that tests enforce rather than reviewers.

Design record: SPEC.md. Security: SECURITY.md. JSON contract: docs/report-schema.md. Changes: CHANGELOG.md.

Disclaimer

Software is provided as-is, without warranty. See LICENSE.md.

  • run costs money on hosted providers. Tool definitions and prompts are sent to the model you configure. Use --dry-run first, but treat its prompt-token figure as a lower bound rather than a price estimate. Ollama and other local endpoints stay on your machine.
  • Tools on the server under test are never invoked. stdio does launch the command you pass, with your privileges — treat that command as code.
  • Standalone binaries are not distributed yet. Publication stays disabled until the embedded runtime's third-party notices have been reviewed and can ship beside each binary.
  • Not a security scanner. A surface can pass inspect and still be dangerous. Details: SECURITY.md.

Licence

MIT — LICENSE.md.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选