toolfit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toolfit-0.1.0/.env.example +11 -0
- toolfit-0.1.0/.github/workflows/ci.yml +23 -0
- toolfit-0.1.0/.github/workflows/publish.yml +20 -0
- toolfit-0.1.0/.gitignore +8 -0
- toolfit-0.1.0/CHANGELOG.md +15 -0
- toolfit-0.1.0/CLAUDE.md +19 -0
- toolfit-0.1.0/LICENSE +21 -0
- toolfit-0.1.0/PKG-INFO +244 -0
- toolfit-0.1.0/README.md +216 -0
- toolfit-0.1.0/TODOS.md +16 -0
- toolfit-0.1.0/action.yml +66 -0
- toolfit-0.1.0/docs/corpus.md +83 -0
- toolfit-0.1.0/docs/designs/toolfit-v0-scope.md +295 -0
- toolfit-0.1.0/docs/examples/mcp-server-git/report.md +76 -0
- toolfit-0.1.0/docs/examples/mcp-server-git/toolfit-badge.svg +6 -0
- toolfit-0.1.0/docs/examples/mcp-server-git/toolfit-fixes.json +40 -0
- toolfit-0.1.0/docs/examples/server-filesystem/report.md +159 -0
- toolfit-0.1.0/docs/examples/server-filesystem/toolfit-badge.svg +6 -0
- toolfit-0.1.0/docs/examples/server-filesystem/toolfit-fixes.json +150 -0
- toolfit-0.1.0/docs/examples/server-memory/report.md +44 -0
- toolfit-0.1.0/docs/examples/server-memory/toolfit-badge.svg +6 -0
- toolfit-0.1.0/docs/examples/server-memory/toolfit-fixes.json +6 -0
- toolfit-0.1.0/docs/examples/toy-server/report.md +65 -0
- toolfit-0.1.0/docs/examples/toy-server/toolfit-badge.svg +6 -0
- toolfit-0.1.0/docs/examples/toy-server/toolfit-fixes.json +40 -0
- toolfit-0.1.0/docs/mcp-eval-harness-product-doc.md +213 -0
- toolfit-0.1.0/docs/superpowers/plans/2026-08-26-toolfit-spike.md +1589 -0
- toolfit-0.1.0/docs/superpowers/plans/2026-08-27-toolfit-m1.md +1334 -0
- toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m0-scan.md +528 -0
- toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m2.md +1292 -0
- toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m3a.md +878 -0
- toolfit-0.1.0/examples/toy_server.py +87 -0
- toolfit-0.1.0/pyproject.toml +49 -0
- toolfit-0.1.0/scripts/circularity_check.py +47 -0
- toolfit-0.1.0/scripts/openrouter_check.py +48 -0
- toolfit-0.1.0/scripts/run_spike.py +128 -0
- toolfit-0.1.0/src/toolfit/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/cli.py +346 -0
- toolfit-0.1.0/src/toolfit/connect/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/connect/client.py +56 -0
- toolfit-0.1.0/src/toolfit/fix/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/fix/fixer.py +179 -0
- toolfit-0.1.0/src/toolfit/gen/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/gen/schema_sampler.py +149 -0
- toolfit-0.1.0/src/toolfit/gen/taskgen.py +180 -0
- toolfit-0.1.0/src/toolfit/grade/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/grade/confusion.py +114 -0
- toolfit-0.1.0/src/toolfit/grade/grader.py +85 -0
- toolfit-0.1.0/src/toolfit/grade/mutator.py +101 -0
- toolfit-0.1.0/src/toolfit/grade/significance.py +63 -0
- toolfit-0.1.0/src/toolfit/lint/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/lint/rules.py +105 -0
- toolfit-0.1.0/src/toolfit/report/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/report/badge.py +57 -0
- toolfit-0.1.0/src/toolfit/report/render.py +197 -0
- toolfit-0.1.0/src/toolfit/run/__init__.py +0 -0
- toolfit-0.1.0/src/toolfit/run/adapters.py +225 -0
- toolfit-0.1.0/tests/test_adapters.py +404 -0
- toolfit-0.1.0/tests/test_adapters_eval.py +33 -0
- toolfit-0.1.0/tests/test_badge.py +98 -0
- toolfit-0.1.0/tests/test_cli.py +584 -0
- toolfit-0.1.0/tests/test_confusion.py +178 -0
- toolfit-0.1.0/tests/test_e2e_m2.py +52 -0
- toolfit-0.1.0/tests/test_e2e_spike.py +42 -0
- toolfit-0.1.0/tests/test_fix_loop.py +139 -0
- toolfit-0.1.0/tests/test_fixer.py +63 -0
- toolfit-0.1.0/tests/test_grader.py +90 -0
- toolfit-0.1.0/tests/test_lint.py +151 -0
- toolfit-0.1.0/tests/test_mutator.py +126 -0
- toolfit-0.1.0/tests/test_render.py +291 -0
- toolfit-0.1.0/tests/test_scan_toy_server.py +22 -0
- toolfit-0.1.0/tests/test_schema_sampler.py +242 -0
- toolfit-0.1.0/tests/test_significance.py +102 -0
- toolfit-0.1.0/tests/test_taskgen.py +141 -0
- toolfit-0.1.0/tests/test_taskgen_eval.py +108 -0
- toolfit-0.1.0/tests/test_toy_server.py +78 -0
- toolfit-0.1.0/uv.lock +1282 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copy this file to .env and fill in your own keys. .env is gitignored — never commit real keys.
|
|
2
|
+
|
|
3
|
+
# Required for `toolfit eval` / `scripts/run_spike.py` — get one at https://platform.claude.com/settings/keys
|
|
4
|
+
ANTHROPIC_API_KEY=
|
|
5
|
+
|
|
6
|
+
# Optional — only needed if `--model` names an OpenAI model (gpt-*, o1-*, o3-*, o4-*)
|
|
7
|
+
OPENAI_API_KEY=
|
|
8
|
+
|
|
9
|
+
# Optional — only needed if `--model` names an OpenRouter vendor/model (e.g. qwen/qwen-2.5-72b-instruct),
|
|
10
|
+
# or for scripts/openrouter_check.py (OpenRouter compatibility check)
|
|
11
|
+
OPENROUTER_API_KEY=
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: uv sync --extra dev
|
|
20
|
+
- run: uv run pytest -q
|
|
21
|
+
- run: uv build
|
|
22
|
+
# The free path must keep working against the toy server with no key present.
|
|
23
|
+
- run: uv run toolfit scan examples/toy_server.py
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # PyPI trusted publishing (OIDC) — no token stored anywhere
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv sync --extra dev
|
|
18
|
+
- run: uv run pytest -q
|
|
19
|
+
- run: uv build
|
|
20
|
+
- run: uv publish --trusted-publishing always
|
toolfit-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-05)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `toolfit scan <server>`: free static lint over `tools/list` — missing, too-short, duplicated, and self-declared-deprecated descriptions. No model calls.
|
|
8
|
+
- `toolfit eval <server>`: inverted task generation (sample schema-valid arguments first, then ask a generator to write the request), structural grading with canonicalisation, confusion matrix, per-tool pass rates with Wilson 95% intervals, leakage and solvability guardrails reported as warnings.
|
|
9
|
+
- `--mutate 'tool:new description'`: paired re-run of that tool's own tasks against a catalog with one description patched; exact one-sided McNemar p-value; one Bonferroni correction across everything re-measured in the run.
|
|
10
|
+
- `--fix` / `--fix-tool NAME`: propose a rewrite per failing tool (rewriter sees the real parameters and neighbour descriptions), re-measure, report accepted and rejected alike; `toolfit-fixes.json` with description text only.
|
|
11
|
+
- `--badge`: SVG coloured by pass rate with model, generator, seeds, and task-suite hash embedded. `--strict` / `--strict-threshold`: exit codes for CI.
|
|
12
|
+
- Servers as a `.py` script (`uv run`), any command line (`npx -y …`), or an `http(s)://` URL; subprocess inherits the environment.
|
|
13
|
+
- Adapters for Anthropic, OpenAI, and OpenRouter, inferred from `--model`; retry with backoff on 429/5xx for every model call.
|
|
14
|
+
- Composite GitHub Action (`action.yml`); repo CI on 3.10/3.13; PyPI publish on `v*` tags via trusted publishing.
|
|
15
|
+
- Evidence: toy server and three public servers evaluated end-to-end (`docs/examples/`), twenty public servers scanned (`docs/corpus.md`).
|
toolfit-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
## Skill routing
|
|
3
|
+
|
|
4
|
+
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
|
|
5
|
+
|
|
6
|
+
Key routing rules:
|
|
7
|
+
- Product ideas/brainstorming → invoke /office-hours
|
|
8
|
+
- Strategy/scope → invoke /plan-ceo-review
|
|
9
|
+
- Architecture → invoke /plan-eng-review
|
|
10
|
+
- Design system/plan review → invoke /design-consultation or /plan-design-review
|
|
11
|
+
- Full review pipeline → invoke /autoplan
|
|
12
|
+
- Bugs/errors → invoke /investigate
|
|
13
|
+
- QA/testing site behavior → invoke /qa or /qa-only
|
|
14
|
+
- Code review/diff check → invoke /review
|
|
15
|
+
- Visual polish → invoke /design-review
|
|
16
|
+
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
|
17
|
+
- Save progress → invoke /context-save
|
|
18
|
+
- Resume context → invoke /context-restore
|
|
19
|
+
- Author a backlog-ready spec/issue → invoke /spec
|
toolfit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sreshta Talluri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
toolfit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: toolfit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Finds the specific places your MCP server confuses models, and proves a fix with a before-and-after eval.
|
|
5
|
+
Project-URL: Repository, https://github.com/sreshtalluri/toolfit
|
|
6
|
+
Author: Sreshta Talluri
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,eval,llm,mcp,model-context-protocol,testing
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: anthropic>=0.40
|
|
20
|
+
Requires-Dist: mcp<3,>=2.0
|
|
21
|
+
Requires-Dist: openai>=1.50
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# toolfit
|
|
30
|
+
|
|
31
|
+
toolfit finds the specific places your MCP server confuses models, rewrites the tool
|
|
32
|
+
descriptions to fix them, and proves the fix with a before-and-after eval.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
uvx toolfit scan "npx -y @modelcontextprotocol/server-github" # free, static, seconds
|
|
36
|
+
uvx toolfit eval examples/toy_server.py --seeds 10 --fix --badge # model-graded, minutes, your key
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Every existing MCP grader stops at a number and a list of complaints. toolfit measures which
|
|
40
|
+
tool pairs a model actually confuses, proposes a description rewrite for each failing tool,
|
|
41
|
+
re-runs the same tasks against the rewrite, and reports the delta with a p-value — accepted
|
|
42
|
+
*and* rejected. The rejected ones are what make the accepted ones believable.
|
|
43
|
+
|
|
44
|
+
## What a run looks like
|
|
45
|
+
|
|
46
|
+
`toolfit eval examples/toy_server.py --seeds 10 --fix --badge`, model under test `claude-sonnet-5`
|
|
47
|
+
(full output: [`docs/examples/toy-server/`](docs/examples/toy-server/)). The toy server has two
|
|
48
|
+
deliberately confusable pairs: `create_task`/`update_task` share the description *"Add a new task."*,
|
|
49
|
+
and `list_tasks`/`count_tasks` share *"Get tasks by status."*
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
## Confusion Matrix
|
|
53
|
+
|
|
54
|
+
| Intended \ Called | count_tasks | create_reminder | create_task | list_tasks | update_task |
|
|
55
|
+
|---|---|---|---|---|---|
|
|
56
|
+
| count_tasks | 0 | 0 | 0 | 10 | 0 |
|
|
57
|
+
| create_reminder | 0 | 10 | 0 | 0 | 0 |
|
|
58
|
+
| create_task | 0 | 0 | 10 | 0 | 0 |
|
|
59
|
+
| list_tasks | 0 | 0 | 0 | 10 | 0 |
|
|
60
|
+
| update_task | 0 | 0 | 0 | 0 | 10 |
|
|
61
|
+
|
|
62
|
+
## Pass Rates
|
|
63
|
+
- count_tasks: 0/10 (0%), 95% CI [0%, 28%]
|
|
64
|
+
- create_reminder: 8/10 (80%), 95% CI [49%, 94%]
|
|
65
|
+
- create_task: 9/10 (90%), 95% CI [60%, 98%]
|
|
66
|
+
- list_tasks: 10/10 (100%), 95% CI [72%, 100%]
|
|
67
|
+
- update_task: 10/10 (100%), 95% CI [72%, 100%]
|
|
68
|
+
|
|
69
|
+
## Proposed Fixes
|
|
70
|
+
|
|
71
|
+
### create_task — REJECTED
|
|
72
|
+
- Before: 'Add a new task.'
|
|
73
|
+
- After: 'Creates a brand-new task by specifying its title and priority, distinct from
|
|
74
|
+
update_task (which modifies existing tasks) or create_reminder (...)'
|
|
75
|
+
- Pass rate: 9/10 → 7/10, p-value 1.0000
|
|
76
|
+
- Reason: rejected: made things worse
|
|
77
|
+
|
|
78
|
+
### count_tasks — REJECTED
|
|
79
|
+
- Before: 'Get tasks by status.'
|
|
80
|
+
- After: 'Return the number of tasks matching a given status, rather than the tasks
|
|
81
|
+
themselves, by accepting a required status argument.'
|
|
82
|
+
- Pass rate: 0/10 → 0/10, p-value 1.0000
|
|
83
|
+
- Reason: rejected: no change
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+
Three things this run shows, none of them flattering, all of them the point:
|
|
89
|
+
|
|
90
|
+
- **The matrix names the real problem.** `count_tasks` is called *zero* times out of ten; every
|
|
91
|
+
request meant for it goes to `list_tasks`. The solvability check flagged the identical
|
|
92
|
+
`create_task`/`update_task` descriptions on 14 of 20 tasks — yet Sonnet 5 still picked the right
|
|
93
|
+
one every time, because it reads `task_id` in the schema. Descriptions aren't the whole story,
|
|
94
|
+
and toolfit measures what the model does, not what the text says.
|
|
95
|
+
- **The fix loop refused to claim a win.** Three rewrites were proposed and measured; one made
|
|
96
|
+
things worse, two changed nothing. They are printed anyway. Every failure here is at the
|
|
97
|
+
argument level or in the generator's "count vs. list" phrasing — things a description rewrite
|
|
98
|
+
can't fix — and the numbers say so instead of the tool saying so.
|
|
99
|
+
- **Every number carries its uncertainty.** `9/10` and `7/10` overlap almost entirely at n=10;
|
|
100
|
+
the exact test gives p=1.0 for a change in the wrong direction. Nothing gets reported as a
|
|
101
|
+
finding because it looked good once.
|
|
102
|
+
|
|
103
|
+
## On real servers
|
|
104
|
+
|
|
105
|
+
Same command against three public servers, model under test `claude-sonnet-5`, 10 seeds per tool
|
|
106
|
+
(full reports in [`docs/examples/`](docs/examples/)):
|
|
107
|
+
|
|
108
|
+
| Server | Tools | Pass | What the matrix showed |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `@modelcontextprotocol/server-memory` | 9 | **90/90** | Perfect diagonal. Nine crisp descriptions, zero warnings, nothing to fix. |
|
|
111
|
+
| `mcp-server-git` | 12 | **112/120** | `git_commit` 4/10: five requests went to `git_add` first. The rewrite that stressed "commits *staged* changes" measured worse, 4→2, and was rejected. |
|
|
112
|
+
| `@modelcontextprotocol/server-filesystem` | 14 | **77/140** | `read_file` 0/10 — its description says *DEPRECATED, use read_text_file* and the model obeys (`scan` now flags this). Thirty-plus requests across seven tools went to `list_allowed_directories` first. |
|
|
113
|
+
|
|
114
|
+
The git and filesystem results share a cause that a description can't fix: **the model takes a
|
|
115
|
+
correct precondition step** — stage before commit, check allowed directories before touching a
|
|
116
|
+
path — and a single-step eval scores it as the wrong tool. That column in the confusion matrix is
|
|
117
|
+
the finding; it tells you which tools need their precondition stated ("paths are validated for
|
|
118
|
+
you") or a multi-step harness. toolfit does not paper over it by grading the first call leniently.
|
|
119
|
+
|
|
120
|
+
Twelve rewrites were proposed for the filesystem server. Five improved the number (`8→10`,
|
|
121
|
+
`6→8`); none were accepted, because one Bonferroni correction across twelve proposals at n=10
|
|
122
|
+
sets α=0.004 and the report says so. Run `--fix-tool list_directory --fix-tool move_file
|
|
123
|
+
--seeds 20` on the tools the matrix names, not `--fix` on the whole catalog at once.
|
|
124
|
+
|
|
125
|
+
## Two commands, two budgets
|
|
126
|
+
|
|
127
|
+
| | `scan` | `eval` |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| What | Static lint over `tools/list`: missing, too-short, and duplicated descriptions | Live model behaviour: which tool it calls, with which arguments, for a request that should lead to each tool |
|
|
130
|
+
| Cost | Free — no model calls, no key | Your API key: roughly `tools × seeds × 3` calls, plus `seeds` per proposed fix |
|
|
131
|
+
| Time | Under a second after the server starts | Minutes |
|
|
132
|
+
| Output | Findings list (never a letter grade) | Confusion matrix, per-tool pass rates with 95% CIs, mutation/fix verdicts, optional badge and `toolfit-fixes.json` |
|
|
133
|
+
|
|
134
|
+
They are deliberately separate. `scan` is the zero-config front door; on mature servers it finds
|
|
135
|
+
little ([1 finding across 166 tools on 15 public servers](docs/corpus.md)) because the bug it
|
|
136
|
+
catches is the copy-paste class. The confusion is what `eval` is for.
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
uvx toolfit --help # zero-install, one-off
|
|
142
|
+
pipx install toolfit # persistent / CI
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Python 3.10+. Talks to servers over the MCP protocol via the official `mcp` SDK, so the
|
|
146
|
+
server can be in any language.
|
|
147
|
+
|
|
148
|
+
## Pointing it at a server
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
toolfit scan path/to/server.py # run via `uv run`
|
|
152
|
+
toolfit scan "npx -y @modelcontextprotocol/server-filesystem ." # any command line
|
|
153
|
+
toolfit scan https://your-host/mcp # Streamable HTTP
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The subprocess gets your environment (minus toolfit's own `*_API_KEY`s, which a third-party server
|
|
157
|
+
binary has no business seeing), so servers that read a token from `GITHUB_TOKEN` or
|
|
158
|
+
`STRIPE_SECRET_KEY` work unchanged. toolfit never calls a tool — it only ever asks for the
|
|
159
|
+
catalog (`tools/list`), then asks a model what it *would* call. Nothing touches your backend.
|
|
160
|
+
|
|
161
|
+
## How the measurement works
|
|
162
|
+
|
|
163
|
+
The credibility problem with LLM-generated evals is circularity: if one model writes the task
|
|
164
|
+
and also decides the expected answer, you are measuring agreement with that model. toolfit
|
|
165
|
+
inverts it:
|
|
166
|
+
|
|
167
|
+
1. Sample a concrete, schema-valid argument set for one tool (`gen/schema_sampler.py`),
|
|
168
|
+
honouring `required`, enums, formats, and nullables.
|
|
169
|
+
2. Ask a generator model to write the sentence a user would type that leads to *exactly those
|
|
170
|
+
arguments*, without naming the tool. Ground truth is the sampled tuple, not the model's opinion.
|
|
171
|
+
3. Send that sentence plus the whole catalog to the model under test.
|
|
172
|
+
4. Grade structurally (`grade/grader.py`): right tool, and arguments equal after canonicalising
|
|
173
|
+
dates, case, whitespace, and array order. No LLM judge, ever.
|
|
174
|
+
|
|
175
|
+
Guardrails: a second pass checks each task for tool-name leakage and for solvability against
|
|
176
|
+
the catalog; both are reported as warnings, never silently dropped. Tools whose schema the
|
|
177
|
+
sampler can't handle are excluded and listed — a partial number is never printed as a complete one.
|
|
178
|
+
|
|
179
|
+
**Mutation testing** is the same grader run twice. `--mutate 'tool:new description'` re-runs a
|
|
180
|
+
tool's *own* base tasks against a catalog where only that description is patched (protocol-level;
|
|
181
|
+
your source is never touched) and compares pass rates on paired trials. `--fix` does the same with
|
|
182
|
+
a proposed rewrite for every failing tool.
|
|
183
|
+
|
|
184
|
+
**Significance** is an exact one-sided McNemar test on the discordant pairs, Bonferroni-corrected
|
|
185
|
+
across everything re-measured in one run. It is deterministic and honest at small n: with 5 seeds
|
|
186
|
+
the smallest attainable p-value is 1/32, so `--seeds 10` is the practical floor for a verdict —
|
|
187
|
+
the CLI warns if you go lower. Every rate carries `n` and a Wilson 95% interval.
|
|
188
|
+
|
|
189
|
+
## CI
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
- uses: sreshtalluri/toolfit@main
|
|
193
|
+
with:
|
|
194
|
+
server: "npx -y @modelcontextprotocol/server-github"
|
|
195
|
+
eval: true # omit for the free scan only
|
|
196
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`scan --strict` exits 1 on any finding; `eval --strict` exits 1 if any tool's pass rate is below
|
|
200
|
+
`--strict-threshold` (default 0.9). Tools excluded by a schema warning are named on stderr and
|
|
201
|
+
*not* counted — they don't fail the gate, but you'll see them.
|
|
202
|
+
|
|
203
|
+
`--badge` writes `toolfit-badge.svg` with the pass rate (or the before→after delta of a single
|
|
204
|
+
mutation or accepted fix), coloured by rate, with the model, generator, seed count, and a hash of
|
|
205
|
+
the task suite embedded so the number is never separable from what produced it.
|
|
206
|
+
|
|
207
|
+
## Models
|
|
208
|
+
|
|
209
|
+
`--model` picks the model under test and the provider is inferred from the name: `claude*` →
|
|
210
|
+
Anthropic, `gpt*`/`o*` → OpenAI, `vendor/model` → OpenRouter. Keys come from `ANTHROPIC_API_KEY`,
|
|
211
|
+
`OPENAI_API_KEY`, `OPENROUTER_API_KEY`. Task generation and fix proposals always use Anthropic, so
|
|
212
|
+
that key is required for `eval` regardless of `--model`.
|
|
213
|
+
|
|
214
|
+
**Data handling.** The tool catalog and the generated task text are sent to whichever provider you
|
|
215
|
+
configure, with your key. toolfit itself stores nothing and phones nowhere. If your server is
|
|
216
|
+
internal, that is the one place its schema leaves your machine.
|
|
217
|
+
|
|
218
|
+
## Landscape
|
|
219
|
+
|
|
220
|
+
| Tool | What it does | Where it stops |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| mcpgrade | Static lint + single-step eval, A–F grade | No re-measured fix |
|
|
223
|
+
| mcpx | ESLint-style schema lint, CI gate | Never runs a model |
|
|
224
|
+
| MCProbe | Usability rules + input fuzzing | Fuzzing, not task performance |
|
|
225
|
+
| lastmile-ai/mcp-eval | Assertion framework with LLM judges | You write the tests |
|
|
226
|
+
| MCPJam Inspector | Hosted evals with cross-model comparison | GUI product, not a CLI |
|
|
227
|
+
| MCP-Atlas / MCP-Bench | Benchmarks ranking *models* | Not pointable at your server |
|
|
228
|
+
|
|
229
|
+
toolfit is not a model leaderboard and does not test for prompt injection. One server per run.
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
uv sync --extra dev
|
|
235
|
+
uv run pytest -q
|
|
236
|
+
uv run toolfit scan examples/toy_server.py
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`examples/toy_server.py` has two deliberately confusable tool pairs — `create_task`/`update_task`
|
|
240
|
+
share a description, `list_tasks`/`count_tasks` share a vague one — so the fix loop has something
|
|
241
|
+
real to find. Design history and the methodology decisions behind every number are in
|
|
242
|
+
[`docs/designs/toolfit-v0-scope.md`](docs/designs/toolfit-v0-scope.md).
|
|
243
|
+
|
|
244
|
+
MIT.
|
toolfit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# toolfit
|
|
2
|
+
|
|
3
|
+
toolfit finds the specific places your MCP server confuses models, rewrites the tool
|
|
4
|
+
descriptions to fix them, and proves the fix with a before-and-after eval.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
uvx toolfit scan "npx -y @modelcontextprotocol/server-github" # free, static, seconds
|
|
8
|
+
uvx toolfit eval examples/toy_server.py --seeds 10 --fix --badge # model-graded, minutes, your key
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Every existing MCP grader stops at a number and a list of complaints. toolfit measures which
|
|
12
|
+
tool pairs a model actually confuses, proposes a description rewrite for each failing tool,
|
|
13
|
+
re-runs the same tasks against the rewrite, and reports the delta with a p-value — accepted
|
|
14
|
+
*and* rejected. The rejected ones are what make the accepted ones believable.
|
|
15
|
+
|
|
16
|
+
## What a run looks like
|
|
17
|
+
|
|
18
|
+
`toolfit eval examples/toy_server.py --seeds 10 --fix --badge`, model under test `claude-sonnet-5`
|
|
19
|
+
(full output: [`docs/examples/toy-server/`](docs/examples/toy-server/)). The toy server has two
|
|
20
|
+
deliberately confusable pairs: `create_task`/`update_task` share the description *"Add a new task."*,
|
|
21
|
+
and `list_tasks`/`count_tasks` share *"Get tasks by status."*
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
## Confusion Matrix
|
|
25
|
+
|
|
26
|
+
| Intended \ Called | count_tasks | create_reminder | create_task | list_tasks | update_task |
|
|
27
|
+
|---|---|---|---|---|---|
|
|
28
|
+
| count_tasks | 0 | 0 | 0 | 10 | 0 |
|
|
29
|
+
| create_reminder | 0 | 10 | 0 | 0 | 0 |
|
|
30
|
+
| create_task | 0 | 0 | 10 | 0 | 0 |
|
|
31
|
+
| list_tasks | 0 | 0 | 0 | 10 | 0 |
|
|
32
|
+
| update_task | 0 | 0 | 0 | 0 | 10 |
|
|
33
|
+
|
|
34
|
+
## Pass Rates
|
|
35
|
+
- count_tasks: 0/10 (0%), 95% CI [0%, 28%]
|
|
36
|
+
- create_reminder: 8/10 (80%), 95% CI [49%, 94%]
|
|
37
|
+
- create_task: 9/10 (90%), 95% CI [60%, 98%]
|
|
38
|
+
- list_tasks: 10/10 (100%), 95% CI [72%, 100%]
|
|
39
|
+
- update_task: 10/10 (100%), 95% CI [72%, 100%]
|
|
40
|
+
|
|
41
|
+
## Proposed Fixes
|
|
42
|
+
|
|
43
|
+
### create_task — REJECTED
|
|
44
|
+
- Before: 'Add a new task.'
|
|
45
|
+
- After: 'Creates a brand-new task by specifying its title and priority, distinct from
|
|
46
|
+
update_task (which modifies existing tasks) or create_reminder (...)'
|
|
47
|
+
- Pass rate: 9/10 → 7/10, p-value 1.0000
|
|
48
|
+
- Reason: rejected: made things worse
|
|
49
|
+
|
|
50
|
+
### count_tasks — REJECTED
|
|
51
|
+
- Before: 'Get tasks by status.'
|
|
52
|
+
- After: 'Return the number of tasks matching a given status, rather than the tasks
|
|
53
|
+
themselves, by accepting a required status argument.'
|
|
54
|
+
- Pass rate: 0/10 → 0/10, p-value 1.0000
|
|
55
|
+
- Reason: rejected: no change
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
Three things this run shows, none of them flattering, all of them the point:
|
|
61
|
+
|
|
62
|
+
- **The matrix names the real problem.** `count_tasks` is called *zero* times out of ten; every
|
|
63
|
+
request meant for it goes to `list_tasks`. The solvability check flagged the identical
|
|
64
|
+
`create_task`/`update_task` descriptions on 14 of 20 tasks — yet Sonnet 5 still picked the right
|
|
65
|
+
one every time, because it reads `task_id` in the schema. Descriptions aren't the whole story,
|
|
66
|
+
and toolfit measures what the model does, not what the text says.
|
|
67
|
+
- **The fix loop refused to claim a win.** Three rewrites were proposed and measured; one made
|
|
68
|
+
things worse, two changed nothing. They are printed anyway. Every failure here is at the
|
|
69
|
+
argument level or in the generator's "count vs. list" phrasing — things a description rewrite
|
|
70
|
+
can't fix — and the numbers say so instead of the tool saying so.
|
|
71
|
+
- **Every number carries its uncertainty.** `9/10` and `7/10` overlap almost entirely at n=10;
|
|
72
|
+
the exact test gives p=1.0 for a change in the wrong direction. Nothing gets reported as a
|
|
73
|
+
finding because it looked good once.
|
|
74
|
+
|
|
75
|
+
## On real servers
|
|
76
|
+
|
|
77
|
+
Same command against three public servers, model under test `claude-sonnet-5`, 10 seeds per tool
|
|
78
|
+
(full reports in [`docs/examples/`](docs/examples/)):
|
|
79
|
+
|
|
80
|
+
| Server | Tools | Pass | What the matrix showed |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `@modelcontextprotocol/server-memory` | 9 | **90/90** | Perfect diagonal. Nine crisp descriptions, zero warnings, nothing to fix. |
|
|
83
|
+
| `mcp-server-git` | 12 | **112/120** | `git_commit` 4/10: five requests went to `git_add` first. The rewrite that stressed "commits *staged* changes" measured worse, 4→2, and was rejected. |
|
|
84
|
+
| `@modelcontextprotocol/server-filesystem` | 14 | **77/140** | `read_file` 0/10 — its description says *DEPRECATED, use read_text_file* and the model obeys (`scan` now flags this). Thirty-plus requests across seven tools went to `list_allowed_directories` first. |
|
|
85
|
+
|
|
86
|
+
The git and filesystem results share a cause that a description can't fix: **the model takes a
|
|
87
|
+
correct precondition step** — stage before commit, check allowed directories before touching a
|
|
88
|
+
path — and a single-step eval scores it as the wrong tool. That column in the confusion matrix is
|
|
89
|
+
the finding; it tells you which tools need their precondition stated ("paths are validated for
|
|
90
|
+
you") or a multi-step harness. toolfit does not paper over it by grading the first call leniently.
|
|
91
|
+
|
|
92
|
+
Twelve rewrites were proposed for the filesystem server. Five improved the number (`8→10`,
|
|
93
|
+
`6→8`); none were accepted, because one Bonferroni correction across twelve proposals at n=10
|
|
94
|
+
sets α=0.004 and the report says so. Run `--fix-tool list_directory --fix-tool move_file
|
|
95
|
+
--seeds 20` on the tools the matrix names, not `--fix` on the whole catalog at once.
|
|
96
|
+
|
|
97
|
+
## Two commands, two budgets
|
|
98
|
+
|
|
99
|
+
| | `scan` | `eval` |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| What | Static lint over `tools/list`: missing, too-short, and duplicated descriptions | Live model behaviour: which tool it calls, with which arguments, for a request that should lead to each tool |
|
|
102
|
+
| Cost | Free — no model calls, no key | Your API key: roughly `tools × seeds × 3` calls, plus `seeds` per proposed fix |
|
|
103
|
+
| Time | Under a second after the server starts | Minutes |
|
|
104
|
+
| Output | Findings list (never a letter grade) | Confusion matrix, per-tool pass rates with 95% CIs, mutation/fix verdicts, optional badge and `toolfit-fixes.json` |
|
|
105
|
+
|
|
106
|
+
They are deliberately separate. `scan` is the zero-config front door; on mature servers it finds
|
|
107
|
+
little ([1 finding across 166 tools on 15 public servers](docs/corpus.md)) because the bug it
|
|
108
|
+
catches is the copy-paste class. The confusion is what `eval` is for.
|
|
109
|
+
|
|
110
|
+
## Install
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
uvx toolfit --help # zero-install, one-off
|
|
114
|
+
pipx install toolfit # persistent / CI
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Python 3.10+. Talks to servers over the MCP protocol via the official `mcp` SDK, so the
|
|
118
|
+
server can be in any language.
|
|
119
|
+
|
|
120
|
+
## Pointing it at a server
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
toolfit scan path/to/server.py # run via `uv run`
|
|
124
|
+
toolfit scan "npx -y @modelcontextprotocol/server-filesystem ." # any command line
|
|
125
|
+
toolfit scan https://your-host/mcp # Streamable HTTP
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The subprocess gets your environment (minus toolfit's own `*_API_KEY`s, which a third-party server
|
|
129
|
+
binary has no business seeing), so servers that read a token from `GITHUB_TOKEN` or
|
|
130
|
+
`STRIPE_SECRET_KEY` work unchanged. toolfit never calls a tool — it only ever asks for the
|
|
131
|
+
catalog (`tools/list`), then asks a model what it *would* call. Nothing touches your backend.
|
|
132
|
+
|
|
133
|
+
## How the measurement works
|
|
134
|
+
|
|
135
|
+
The credibility problem with LLM-generated evals is circularity: if one model writes the task
|
|
136
|
+
and also decides the expected answer, you are measuring agreement with that model. toolfit
|
|
137
|
+
inverts it:
|
|
138
|
+
|
|
139
|
+
1. Sample a concrete, schema-valid argument set for one tool (`gen/schema_sampler.py`),
|
|
140
|
+
honouring `required`, enums, formats, and nullables.
|
|
141
|
+
2. Ask a generator model to write the sentence a user would type that leads to *exactly those
|
|
142
|
+
arguments*, without naming the tool. Ground truth is the sampled tuple, not the model's opinion.
|
|
143
|
+
3. Send that sentence plus the whole catalog to the model under test.
|
|
144
|
+
4. Grade structurally (`grade/grader.py`): right tool, and arguments equal after canonicalising
|
|
145
|
+
dates, case, whitespace, and array order. No LLM judge, ever.
|
|
146
|
+
|
|
147
|
+
Guardrails: a second pass checks each task for tool-name leakage and for solvability against
|
|
148
|
+
the catalog; both are reported as warnings, never silently dropped. Tools whose schema the
|
|
149
|
+
sampler can't handle are excluded and listed — a partial number is never printed as a complete one.
|
|
150
|
+
|
|
151
|
+
**Mutation testing** is the same grader run twice. `--mutate 'tool:new description'` re-runs a
|
|
152
|
+
tool's *own* base tasks against a catalog where only that description is patched (protocol-level;
|
|
153
|
+
your source is never touched) and compares pass rates on paired trials. `--fix` does the same with
|
|
154
|
+
a proposed rewrite for every failing tool.
|
|
155
|
+
|
|
156
|
+
**Significance** is an exact one-sided McNemar test on the discordant pairs, Bonferroni-corrected
|
|
157
|
+
across everything re-measured in one run. It is deterministic and honest at small n: with 5 seeds
|
|
158
|
+
the smallest attainable p-value is 1/32, so `--seeds 10` is the practical floor for a verdict —
|
|
159
|
+
the CLI warns if you go lower. Every rate carries `n` and a Wilson 95% interval.
|
|
160
|
+
|
|
161
|
+
## CI
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
- uses: sreshtalluri/toolfit@main
|
|
165
|
+
with:
|
|
166
|
+
server: "npx -y @modelcontextprotocol/server-github"
|
|
167
|
+
eval: true # omit for the free scan only
|
|
168
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`scan --strict` exits 1 on any finding; `eval --strict` exits 1 if any tool's pass rate is below
|
|
172
|
+
`--strict-threshold` (default 0.9). Tools excluded by a schema warning are named on stderr and
|
|
173
|
+
*not* counted — they don't fail the gate, but you'll see them.
|
|
174
|
+
|
|
175
|
+
`--badge` writes `toolfit-badge.svg` with the pass rate (or the before→after delta of a single
|
|
176
|
+
mutation or accepted fix), coloured by rate, with the model, generator, seed count, and a hash of
|
|
177
|
+
the task suite embedded so the number is never separable from what produced it.
|
|
178
|
+
|
|
179
|
+
## Models
|
|
180
|
+
|
|
181
|
+
`--model` picks the model under test and the provider is inferred from the name: `claude*` →
|
|
182
|
+
Anthropic, `gpt*`/`o*` → OpenAI, `vendor/model` → OpenRouter. Keys come from `ANTHROPIC_API_KEY`,
|
|
183
|
+
`OPENAI_API_KEY`, `OPENROUTER_API_KEY`. Task generation and fix proposals always use Anthropic, so
|
|
184
|
+
that key is required for `eval` regardless of `--model`.
|
|
185
|
+
|
|
186
|
+
**Data handling.** The tool catalog and the generated task text are sent to whichever provider you
|
|
187
|
+
configure, with your key. toolfit itself stores nothing and phones nowhere. If your server is
|
|
188
|
+
internal, that is the one place its schema leaves your machine.
|
|
189
|
+
|
|
190
|
+
## Landscape
|
|
191
|
+
|
|
192
|
+
| Tool | What it does | Where it stops |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| mcpgrade | Static lint + single-step eval, A–F grade | No re-measured fix |
|
|
195
|
+
| mcpx | ESLint-style schema lint, CI gate | Never runs a model |
|
|
196
|
+
| MCProbe | Usability rules + input fuzzing | Fuzzing, not task performance |
|
|
197
|
+
| lastmile-ai/mcp-eval | Assertion framework with LLM judges | You write the tests |
|
|
198
|
+
| MCPJam Inspector | Hosted evals with cross-model comparison | GUI product, not a CLI |
|
|
199
|
+
| MCP-Atlas / MCP-Bench | Benchmarks ranking *models* | Not pointable at your server |
|
|
200
|
+
|
|
201
|
+
toolfit is not a model leaderboard and does not test for prompt injection. One server per run.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
uv sync --extra dev
|
|
207
|
+
uv run pytest -q
|
|
208
|
+
uv run toolfit scan examples/toy_server.py
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`examples/toy_server.py` has two deliberately confusable tool pairs — `create_task`/`update_task`
|
|
212
|
+
share a description, `list_tasks`/`count_tasks` share a vague one — so the fix loop has something
|
|
213
|
+
real to find. Design history and the methodology decisions behind every number are in
|
|
214
|
+
[`docs/designs/toolfit-v0-scope.md`](docs/designs/toolfit-v0-scope.md).
|
|
215
|
+
|
|
216
|
+
MIT.
|
toolfit-0.1.0/TODOS.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# TODOS
|
|
2
|
+
|
|
3
|
+
## eval
|
|
4
|
+
|
|
5
|
+
- **Multi-step grading (source doc M5).** On real servers the dominant off-diagonal mass is precondition calls (`git_add` before `git_commit`, `list_allowed_directories` before any path op) — correct behaviour a single-step grader scores as confusion. Grade the sequence, or let a tool declare its preconditions. **Priority:** P1
|
|
6
|
+
- **Separate `(error)` column.** max_tokens truncation, malformed tool-call JSON, and empty `choices` are tallied with genuine no-calls. Add `error` to `ToolCall` and a column so a flaky provider doesn't read as a confusing catalog. **Priority:** P2
|
|
7
|
+
- **Non-Anthropic model under test on the corpus.** `OPENROUTER_API_KEY` was empty during the scenario runs; a small model is far likelier to confuse the toy pairs and give `--fix` something to accept. **Priority:** P2
|
|
8
|
+
- **`--strict` on schema-excluded tools.** Currently warn-only (decided 2026-09-05). Revisit if a real CI user is surprised by a green gate on an unevaluated server. **Priority:** P3
|
|
9
|
+
|
|
10
|
+
## gen
|
|
11
|
+
|
|
12
|
+
- **Constraints the schema can't express.** `read_text_file` accepts `head` OR `tail`, not both; the sampler draws both 25% of the time and the solvability check rejects the task. Consider honouring `oneOf`/`not` at the object level, or a per-server task-rejection budget. **Priority:** P3
|
|
13
|
+
|
|
14
|
+
## Completed
|
|
15
|
+
|
|
16
|
+
- v0.1.0 (2026-09-05): scan, eval, mutation testing, fix loop, badge, strict, generic launch, Action, corpus, three real-server scenarios.
|