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.
Files changed (77) hide show
  1. toolfit-0.1.0/.env.example +11 -0
  2. toolfit-0.1.0/.github/workflows/ci.yml +23 -0
  3. toolfit-0.1.0/.github/workflows/publish.yml +20 -0
  4. toolfit-0.1.0/.gitignore +8 -0
  5. toolfit-0.1.0/CHANGELOG.md +15 -0
  6. toolfit-0.1.0/CLAUDE.md +19 -0
  7. toolfit-0.1.0/LICENSE +21 -0
  8. toolfit-0.1.0/PKG-INFO +244 -0
  9. toolfit-0.1.0/README.md +216 -0
  10. toolfit-0.1.0/TODOS.md +16 -0
  11. toolfit-0.1.0/action.yml +66 -0
  12. toolfit-0.1.0/docs/corpus.md +83 -0
  13. toolfit-0.1.0/docs/designs/toolfit-v0-scope.md +295 -0
  14. toolfit-0.1.0/docs/examples/mcp-server-git/report.md +76 -0
  15. toolfit-0.1.0/docs/examples/mcp-server-git/toolfit-badge.svg +6 -0
  16. toolfit-0.1.0/docs/examples/mcp-server-git/toolfit-fixes.json +40 -0
  17. toolfit-0.1.0/docs/examples/server-filesystem/report.md +159 -0
  18. toolfit-0.1.0/docs/examples/server-filesystem/toolfit-badge.svg +6 -0
  19. toolfit-0.1.0/docs/examples/server-filesystem/toolfit-fixes.json +150 -0
  20. toolfit-0.1.0/docs/examples/server-memory/report.md +44 -0
  21. toolfit-0.1.0/docs/examples/server-memory/toolfit-badge.svg +6 -0
  22. toolfit-0.1.0/docs/examples/server-memory/toolfit-fixes.json +6 -0
  23. toolfit-0.1.0/docs/examples/toy-server/report.md +65 -0
  24. toolfit-0.1.0/docs/examples/toy-server/toolfit-badge.svg +6 -0
  25. toolfit-0.1.0/docs/examples/toy-server/toolfit-fixes.json +40 -0
  26. toolfit-0.1.0/docs/mcp-eval-harness-product-doc.md +213 -0
  27. toolfit-0.1.0/docs/superpowers/plans/2026-08-26-toolfit-spike.md +1589 -0
  28. toolfit-0.1.0/docs/superpowers/plans/2026-08-27-toolfit-m1.md +1334 -0
  29. toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m0-scan.md +528 -0
  30. toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m2.md +1292 -0
  31. toolfit-0.1.0/docs/superpowers/plans/2026-08-28-toolfit-m3a.md +878 -0
  32. toolfit-0.1.0/examples/toy_server.py +87 -0
  33. toolfit-0.1.0/pyproject.toml +49 -0
  34. toolfit-0.1.0/scripts/circularity_check.py +47 -0
  35. toolfit-0.1.0/scripts/openrouter_check.py +48 -0
  36. toolfit-0.1.0/scripts/run_spike.py +128 -0
  37. toolfit-0.1.0/src/toolfit/__init__.py +0 -0
  38. toolfit-0.1.0/src/toolfit/cli.py +346 -0
  39. toolfit-0.1.0/src/toolfit/connect/__init__.py +0 -0
  40. toolfit-0.1.0/src/toolfit/connect/client.py +56 -0
  41. toolfit-0.1.0/src/toolfit/fix/__init__.py +0 -0
  42. toolfit-0.1.0/src/toolfit/fix/fixer.py +179 -0
  43. toolfit-0.1.0/src/toolfit/gen/__init__.py +0 -0
  44. toolfit-0.1.0/src/toolfit/gen/schema_sampler.py +149 -0
  45. toolfit-0.1.0/src/toolfit/gen/taskgen.py +180 -0
  46. toolfit-0.1.0/src/toolfit/grade/__init__.py +0 -0
  47. toolfit-0.1.0/src/toolfit/grade/confusion.py +114 -0
  48. toolfit-0.1.0/src/toolfit/grade/grader.py +85 -0
  49. toolfit-0.1.0/src/toolfit/grade/mutator.py +101 -0
  50. toolfit-0.1.0/src/toolfit/grade/significance.py +63 -0
  51. toolfit-0.1.0/src/toolfit/lint/__init__.py +0 -0
  52. toolfit-0.1.0/src/toolfit/lint/rules.py +105 -0
  53. toolfit-0.1.0/src/toolfit/report/__init__.py +0 -0
  54. toolfit-0.1.0/src/toolfit/report/badge.py +57 -0
  55. toolfit-0.1.0/src/toolfit/report/render.py +197 -0
  56. toolfit-0.1.0/src/toolfit/run/__init__.py +0 -0
  57. toolfit-0.1.0/src/toolfit/run/adapters.py +225 -0
  58. toolfit-0.1.0/tests/test_adapters.py +404 -0
  59. toolfit-0.1.0/tests/test_adapters_eval.py +33 -0
  60. toolfit-0.1.0/tests/test_badge.py +98 -0
  61. toolfit-0.1.0/tests/test_cli.py +584 -0
  62. toolfit-0.1.0/tests/test_confusion.py +178 -0
  63. toolfit-0.1.0/tests/test_e2e_m2.py +52 -0
  64. toolfit-0.1.0/tests/test_e2e_spike.py +42 -0
  65. toolfit-0.1.0/tests/test_fix_loop.py +139 -0
  66. toolfit-0.1.0/tests/test_fixer.py +63 -0
  67. toolfit-0.1.0/tests/test_grader.py +90 -0
  68. toolfit-0.1.0/tests/test_lint.py +151 -0
  69. toolfit-0.1.0/tests/test_mutator.py +126 -0
  70. toolfit-0.1.0/tests/test_render.py +291 -0
  71. toolfit-0.1.0/tests/test_scan_toy_server.py +22 -0
  72. toolfit-0.1.0/tests/test_schema_sampler.py +242 -0
  73. toolfit-0.1.0/tests/test_significance.py +102 -0
  74. toolfit-0.1.0/tests/test_taskgen.py +141 -0
  75. toolfit-0.1.0/tests/test_taskgen_eval.py +108 -0
  76. toolfit-0.1.0/tests/test_toy_server.py +78 -0
  77. 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
@@ -0,0 +1,8 @@
1
+ .superpowers/
2
+ .claude/worktrees/
3
+ .venv/
4
+ __pycache__/
5
+ *.pyc
6
+ .pytest_cache/
7
+ .env
8
+ dist/
@@ -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`).
@@ -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
+ ![toolfit: 74%](docs/examples/toy-server/toolfit-badge.svg)
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.
@@ -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
+ ![toolfit: 74%](docs/examples/toy-server/toolfit-badge.svg)
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.