misroute 0.2.2__tar.gz → 0.2.4__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 (84) hide show
  1. misroute-0.2.4/PKG-INFO +273 -0
  2. misroute-0.2.4/README.md +237 -0
  3. {misroute-0.2.2 → misroute-0.2.4}/pyproject.toml +6 -1
  4. {misroute-0.2.2 → misroute-0.2.4}/pyproject.toml.orig +6 -1
  5. misroute-0.2.4/src/misroute/agent_server.py +210 -0
  6. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/cli.py +251 -12
  7. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/config.py +55 -2
  8. misroute-0.2.4/src/misroute/doctor.py +84 -0
  9. misroute-0.2.4/src/misroute/explain.py +45 -0
  10. misroute-0.2.4/src/misroute/export.py +62 -0
  11. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/wrapper.py +56 -4
  12. misroute-0.2.4/src/misroute/mcpconfig.py +67 -0
  13. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/openai_compat.py +8 -1
  14. misroute-0.2.4/src/misroute/models/presets.py +122 -0
  15. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/project.py +16 -3
  16. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/html.py +24 -3
  17. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/templates/report.html.j2 +9 -0
  18. misroute-0.2.4/src/misroute/schemas/check.json +75 -0
  19. misroute-0.2.4/src/misroute/schemas/discover.json +163 -0
  20. misroute-0.2.4/src/misroute/schemas/doctor.json +36 -0
  21. misroute-0.2.4/src/misroute/schemas/error.json +31 -0
  22. misroute-0.2.4/src/misroute/schemas/estimate.json +38 -0
  23. misroute-0.2.4/src/misroute/schemas/explain.json +36 -0
  24. misroute-0.2.4/src/misroute/schemas/fix.json +65 -0
  25. misroute-0.2.4/src/misroute/schemas/lint.json +53 -0
  26. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/connect.py +1 -1
  27. misroute-0.2.4/src/misroute/server/static.py +64 -0
  28. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/store/cache.py +18 -0
  29. misroute-0.2.4/src/misroute/testing/plugin.py +14 -0
  30. misroute-0.2.4/src/misroute/testing/routes.py +28 -0
  31. misroute-0.2.4/src/misroute/ui/__init__.py +0 -0
  32. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/ui/app.py +33 -2
  33. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/ui/index.html +118 -1
  34. misroute-0.2.4/src/misroute/ui/setup.py +123 -0
  35. misroute-0.2.2/PKG-INFO +0 -291
  36. misroute-0.2.2/README.md +0 -256
  37. misroute-0.2.2/src/misroute/agent_server.py +0 -98
  38. {misroute-0.2.2 → misroute-0.2.4}/LICENSE +0 -0
  39. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/__init__.py +0 -0
  40. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/check.py +0 -0
  41. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/__init__.py +0 -0
  42. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/codemode.py +0 -0
  43. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/confusion.py +0 -0
  44. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/hedge.py +0 -0
  45. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/nearmiss.py +0 -0
  46. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/rank.py +0 -0
  47. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/removal.py +0 -0
  48. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/__init__.py +0 -0
  49. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/clause.py +0 -0
  50. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/stats.py +0 -0
  51. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/__init__.py +0 -0
  52. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/proxy.py +0 -0
  53. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/rules.py +0 -0
  54. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/lint/__init__.py +0 -0
  55. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/lint/descriptions.py +0 -0
  56. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/__init__.py +0 -0
  57. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/anthropic.py +0 -0
  58. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/base.py +0 -0
  59. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/mock.py +0 -0
  60. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/__init__.py +0 -0
  61. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/code_mode.py +0 -0
  62. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/json_mode.py +0 -0
  63. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/stubs.py +0 -0
  64. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/py.typed +0 -0
  65. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/__init__.py +0 -0
  66. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/__init__.py +0 -0
  67. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/gate.py +0 -0
  68. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/generate.py +0 -0
  69. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/validate.py +0 -0
  70. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/__init__.py +0 -0
  71. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/fake_tools.py +0 -0
  72. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/monty_runner.py +0 -0
  73. {misroute-0.2.2/src/misroute/server → misroute-0.2.4/src/misroute/schemas}/__init__.py +0 -0
  74. {misroute-0.2.2/src/misroute/site → misroute-0.2.4/src/misroute/server}/__init__.py +0 -0
  75. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/catalog.py +0 -0
  76. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/openapi.py +0 -0
  77. {misroute-0.2.2/src/misroute/store → misroute-0.2.4/src/misroute/site}/__init__.py +0 -0
  78. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/site/build.py +0 -0
  79. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/site/templates/index.html.j2 +0 -0
  80. {misroute-0.2.2/src/misroute/testing → misroute-0.2.4/src/misroute/store}/__init__.py +0 -0
  81. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/store/runs.py +0 -0
  82. {misroute-0.2.2/src/misroute/ui → misroute-0.2.4/src/misroute/testing}/__init__.py +0 -0
  83. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/testing/fake_server.py +0 -0
  84. {misroute-0.2.2 → misroute-0.2.4}/src/misroute/trial.py +0 -0
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: misroute
3
+ Version: 0.2.4
4
+ Summary: Find the tools your agent confuses and test whether a fix helps, against a neutral edit
5
+ Keywords: mcp,agents,llm,tool-use,evaluation
6
+ Author: philippyt
7
+ Author-email: philippyt <philipaha@proton.me>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Dist: anthropic>=2.3.0
18
+ Requires-Dist: duckdb>=2.0.0
19
+ Requires-Dist: jinja2>=3.1.6
20
+ Requires-Dist: mcp>=2.3.0
21
+ Requires-Dist: numpy>=2.4.6
22
+ Requires-Dist: openai>=3.41.0
23
+ Requires-Dist: pydantic>=2.14.2
24
+ Requires-Dist: pydantic-monty>=1.4.0
25
+ Requires-Dist: pyyaml>=6.0.3
26
+ Requires-Dist: rich>=15.0.0
27
+ Requires-Dist: scipy>=1.17.1
28
+ Requires-Dist: truststore>=0.10.5
29
+ Requires-Dist: typer>=0.27.3
30
+ Requires-Dist: azure-identity>=1.27.0 ; extra == 'azure'
31
+ Requires-Dist: opentelemetry-sdk>=1.45.2 ; extra == 'otel'
32
+ Requires-Python: >=3.11
33
+ Provides-Extra: azure
34
+ Provides-Extra: otel
35
+ Description-Content-Type: text/markdown
36
+
37
+ # misroute
38
+
39
+ Find tool-routing failures before your agents reach production.
40
+
41
+ misroute tests how language models pick tools from an MCP server. It runs requests for every tool against the full catalog, finds the tools your models mix up, reproduces each failure in JSON mode and code mode, and tests description fixes against a controlled baseline.
42
+
43
+ ## What it does
44
+
45
+ - **Discover:** runs requests for every tool and ranks the pairs your models confuse, with the direction of each leak
46
+ - **Reproduce:** checks JSON tool calls, and runs code-mode programs against recorded fake tools so nothing touches your server
47
+ - **Evaluate:** compares a boundary clause with the original description and a neutral edit of the same length, with exact statistics
48
+ - **Inspect:** shows the misrouted requests, what the model called, and where errors moved after a fix
49
+ - **Integrate:** runs from the command line, in CI, in pytest, from any agent through MCP, and from a local web UI, with a cost cap on every paid run
50
+ - **Guard:** logs real tool calls and holds a second write to a sibling tool until a person approves it
51
+
52
+ ## Install
53
+
54
+ ```console
55
+ $ uv tool install misroute
56
+ ```
57
+
58
+ or `pip install misroute`. Behind a corporate package index, add `--index-url https://pypi.org/simple/`.
59
+
60
+ ## Quick start
61
+
62
+ ```console
63
+ $ export ANTHROPIC_API_KEY=...
64
+ $ misroute quick https://your-server/mcp
65
+ ```
66
+
67
+ `quick` picks a model from your environment, writes `misroute.yaml`, runs lint and a discover capped at $1, and opens the report. It takes an MCP URL, a local command, an OpenAPI file, a tool list in MCP, OpenAI or Anthropic format, or nothing at all, in which case it uses the first server in your Claude, Cursor or VS Code config.
68
+
69
+ Something not connecting? Run `misroute doctor`. It checks the server, each key, the price and one call per model, and prints the fix for every problem.
70
+
71
+ The full loop on the bundled test server, with Claude Opus 5.5:
72
+
73
+ ```console
74
+ $ misroute init --demo
75
+ $ misroute lint --draft
76
+ 9 tools, 2 with weak descriptions
77
+ get_attachment missing
78
+ draft: Returns the attachment identified by attachment_id
79
+ get_document path only
80
+ draft: Returns the full details of a single document identified by its ID
81
+
82
+ $ misroute discover
83
+ up to 495 model calls, up to $3.43
84
+
85
+ tool sibling score signals seen in
86
+ get_attachment get_document 1.00 wrong 0.33 hedge 0.42 removal 1.00 opus code, opus json
87
+ get_case_comments get_case_events 0.50 hedge 0.25 removal 1.00 opus code, opus json
88
+ get_case_events update_case_meta 0.25 wrong 0.25 opus code
89
+
90
+ $ misroute explain --pair get_attachment,get_document
91
+ $ misroute fix --pair get_case_comments,get_case_events --modes json
92
+ $ misroute report
93
+ ```
94
+
95
+ ## Connect your tools
96
+
97
+ | Source | In misroute.yaml |
98
+ |---|---|
99
+ | MCP server over HTTP | `server: {url: https://your-server/mcp, auth: bearer_env:MCP_TOKEN}` |
100
+ | Local MCP server | `server: {command: [npx, your-mcp-server]}` |
101
+ | OpenAPI file | `server: {openapi: api.json}` |
102
+ | Tool list in MCP, OpenAI or Anthropic format | `server: {tools: tools.json}` |
103
+ | Your Claude, Cursor or VS Code config | `misroute init --from claude` |
104
+ | Tools in Python | `Project.from_tools([...], "anthropic:claude-sonnet-5-5")` |
105
+
106
+ Add `include_tools: ["*issue*"]` to test a slice of a large server.
107
+
108
+ ## Models
109
+
110
+ Write models as `provider:model`:
111
+
112
+ | Shorthand | Uses |
113
+ |---|---|
114
+ | `anthropic:claude-opus-5-5` | `ANTHROPIC_API_KEY` |
115
+ | `foundry:claude-opus-5-5` | `ANTHROPIC_FOUNDRY_API_KEY` and `ANTHROPIC_FOUNDRY_BASE_URL` |
116
+ | `bedrock:anthropic.claude-opus-5-5` | your AWS credentials |
117
+ | `azure:your-deployment` | `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` |
118
+ | `openai:gpt-5.6` | `OPENAI_API_KEY` |
119
+ | `openrouter:anthropic/claude-opus-5.5` | `OPENROUTER_API_KEY` |
120
+ | `ollama:qwen3.5:9b`, `vllm:Qwen/Qwen3-14B`, `lmstudio:model` | a local server |
121
+
122
+ Prices come from the public OpenRouter catalog, cached for a week, and local models cost nothing. Set `price_in` and `price_out` to override. Every paid run shows an estimate first and stops at `budget_usd`. Behind a company TLS proxy, set `system_certs: true`.
123
+
124
+ ```yaml
125
+ server:
126
+ url: https://your-server/mcp
127
+ models:
128
+ - foundry:claude-opus-5-5
129
+ generator: openrouter:deepseek/deepseek-v4.1-flash
130
+ validator: openrouter:google/gemini-3.8-flash
131
+ modes: [json, code]
132
+ budget_usd: 5
133
+ ```
134
+
135
+ The generator writes test requests and the validator keeps only those it routes to the right tool. A model from another family than the one under test works best for both.
136
+
137
+ ## Commands
138
+
139
+ | Command | Does |
140
+ |---|---|
141
+ | `misroute quick` | lint and a capped discover in one go, then opens the report |
142
+ | `misroute init` | writes misroute.yaml from your MCP client config and environment |
143
+ | `misroute doctor` | checks the setup and prints a fix for each problem |
144
+ | `misroute lint` | flags missing, name only and path only descriptions and drafts replacements |
145
+ | `misroute discover` | ranks confused tool pairs from model behaviour |
146
+ | `misroute probe` | tests risky pairs with requests that borrow words from the sibling |
147
+ | `misroute explain` | shows the misrouted requests for a pair and what the model called |
148
+ | `misroute fix` | tests a boundary clause against a neutral edit, or against a drafted description for undescribed tools |
149
+ | `misroute check` | tests one proposed description on held out requests |
150
+ | `misroute report` | writes a self-contained HTML report |
151
+ | `misroute ui` | local web interface to set up, run, read results and approve held writes |
152
+ | `misroute ci` | fails on exceeded limits, or with `--changed` on descriptions that got worse |
153
+ | `misroute catalog` | prints tool hashes and descriptions for change-aware CI |
154
+ | `misroute serve-mcp` | runs misroute as an MCP server for agents |
155
+ | `misroute guard serve` | runs an MCP proxy that logs and guards real tool calls |
156
+ | `misroute site` | builds a static results site |
157
+ | `misroute schema` | prints the JSON Schema of a command's output |
158
+
159
+ ## How discover ranks pairs
160
+
161
+ Each tool gets requests written from the user's goal, without the tool's own words. Requests run against the full tool list in rotated orders. Each pair gets a score from three signals: the share of one tool's requests that went to the other, the share where the model called both, and where requests go when one tool is removed. The score orders pairs for attention, and the rates behind it are the numbers to quote.
162
+
163
+ The ranking marks what it cannot vouch for: pairs whose request sets differ in surface words, tools with few validated requests, and tools the validator left without any, marked UNTESTED. Tools that models answer without calling anything are listed separately.
164
+
165
+ Code mode runs each program in Monty against recorded fakes and reports faked tools, pasted stubs, programs that never call anything, invented or reused cursors and unread pages.
166
+
167
+ ## Fix and check
168
+
169
+ `fix` writes one boundary sentence per tool and runs the original, a baseline and the clause on identical requests. The baseline is a neutral rewrite of the same length, or lint's drafted description when a tool has none. Verdicts use an exact McNemar test with Holm correction per pair and a sign flip test across pairs, and every result shows errors in both directions and leakage to other tools.
170
+
171
+ `check` tests one proposed description. It splits requests into a tuning set and a held out set, gives its verdict on the held out set only, and requires a fresh held out set after three verdicts on the same tool, so a description cannot be tuned to the test requests.
172
+
173
+ ## For agents
174
+
175
+ Every command takes `--json` and prints one object with a `schema_version`. Exit codes are `0` passed, `1` limits failed or worse, `2` not enough data, `3` budget or config problem. Paid commands take `--max-cost` and `--estimate-only`, and never prompt without a terminal. `--progress ndjson` streams progress to stderr. `misroute schema check` prints the output schema.
176
+
177
+ Add misroute as an MCP server:
178
+
179
+ ```console
180
+ $ claude mcp add misroute -- uvx misroute serve-mcp
181
+ ```
182
+
183
+ ```json
184
+ {"mcpServers": {"misroute": {"command": "uvx", "args": ["misroute", "serve-mcp"]}}}
185
+ ```
186
+
187
+ The second form goes in `.cursor/mcp.json`. VS Code takes the same block under `"servers"` in `.vscode/mcp.json`.
188
+
189
+ The server exposes `lint_descriptions`, `draft_description`, `check_description`, `explain_pair`, `suggest_fix`, `get_report` and background jobs with `start_job`, `job_status` and `cancel_job` for runs longer than a tool call. Paid tools refuse to run above `max_cost`. misroute never edits your tools.
190
+
191
+ Claude Code users can install the plugin, which adds the server and a skill:
192
+
193
+ ```console
194
+ /plugin marketplace add philippyt/tool-misroute
195
+ /plugin install misroute@misroute
196
+ ```
197
+
198
+ See [AGENTS.md](AGENTS.md) and [llms.txt](llms.txt) for the agent workflow.
199
+
200
+ ## CI
201
+
202
+ Check the descriptions a pull request changed, and comment the result:
203
+
204
+ ```yaml
205
+ - uses: actions/checkout@v5
206
+ with: {fetch-depth: 0}
207
+ - uses: philippyt/tool-misroute@v0.2.4
208
+ with:
209
+ config: misroute.yaml
210
+ max-cost: "2"
211
+ ```
212
+
213
+ Or wire it yourself with `misroute catalog` on the base branch and `misroute ci --changed --baseline tools.json --markdown comment.md --junit results.xml`. `misroute lint --sarif lint.sarif` feeds GitHub code scanning, and `--strict` fails on weak descriptions.
214
+
215
+ As a pre-commit hook:
216
+
217
+ ```yaml
218
+ - repo: https://github.com/philippyt/tool-misroute
219
+ rev: v0.2.4
220
+ hooks:
221
+ - id: misroute-lint
222
+ ```
223
+
224
+ ## In tests
225
+
226
+ ```python
227
+ def test_routing(misroute_routes):
228
+ misroute_routes(my_tools, "anthropic:claude-sonnet-5-5", min_accuracy=0.95)
229
+ ```
230
+
231
+ The `misroute_routes` fixture comes with the package. It runs a small capped discover, fails with the confused pairs when accuracy is below the threshold, and caches responses so reruns are free. Outside pytest, use `from misroute.testing.routes import assert_routes`.
232
+
233
+ ## Guard
234
+
235
+ As a proxy in front of any MCP server:
236
+
237
+ ```console
238
+ $ misroute guard serve -c misroute.yaml
239
+ $ misroute guard confirm 8ba64a3fc925
240
+ ```
241
+
242
+ Inside an MCP server built with the Python SDK:
243
+
244
+ ```python
245
+ server = MCPServer("cases", middleware=[guard.middleware()])
246
+ ```
247
+
248
+ Around tool functions in any agent framework, keeping their signatures:
249
+
250
+ ```python
251
+ @guard.tool
252
+ def update_case(case_id: str, status: str) -> dict: ...
253
+ ```
254
+
255
+ The guard logs every call, flags hedges and quick switches between siblings, and holds a second write to a sibling tool until it is approved in `misroute ui` or with `misroute guard confirm`. `misroute report` adds a Production section from the guard log.
256
+
257
+ ## Results
258
+
259
+ Everything lands in `.misroute/`: a DuckDB database, JSONL transcripts of every run and a response cache.
260
+
261
+ ```console
262
+ $ duckdb .misroute/misroute.duckdb "select target, chosen, count(*) from trials where wrong group by all order by 3 desc"
263
+ ```
264
+
265
+ ## Python API
266
+
267
+ ```python
268
+ from misroute import Project
269
+
270
+ proj = Project.from_yaml("misroute.yaml")
271
+ found = await proj.discover()
272
+ result = await proj.fix(top=3, trials=96)
273
+ ```
@@ -0,0 +1,237 @@
1
+ # misroute
2
+
3
+ Find tool-routing failures before your agents reach production.
4
+
5
+ misroute tests how language models pick tools from an MCP server. It runs requests for every tool against the full catalog, finds the tools your models mix up, reproduces each failure in JSON mode and code mode, and tests description fixes against a controlled baseline.
6
+
7
+ ## What it does
8
+
9
+ - **Discover:** runs requests for every tool and ranks the pairs your models confuse, with the direction of each leak
10
+ - **Reproduce:** checks JSON tool calls, and runs code-mode programs against recorded fake tools so nothing touches your server
11
+ - **Evaluate:** compares a boundary clause with the original description and a neutral edit of the same length, with exact statistics
12
+ - **Inspect:** shows the misrouted requests, what the model called, and where errors moved after a fix
13
+ - **Integrate:** runs from the command line, in CI, in pytest, from any agent through MCP, and from a local web UI, with a cost cap on every paid run
14
+ - **Guard:** logs real tool calls and holds a second write to a sibling tool until a person approves it
15
+
16
+ ## Install
17
+
18
+ ```console
19
+ $ uv tool install misroute
20
+ ```
21
+
22
+ or `pip install misroute`. Behind a corporate package index, add `--index-url https://pypi.org/simple/`.
23
+
24
+ ## Quick start
25
+
26
+ ```console
27
+ $ export ANTHROPIC_API_KEY=...
28
+ $ misroute quick https://your-server/mcp
29
+ ```
30
+
31
+ `quick` picks a model from your environment, writes `misroute.yaml`, runs lint and a discover capped at $1, and opens the report. It takes an MCP URL, a local command, an OpenAPI file, a tool list in MCP, OpenAI or Anthropic format, or nothing at all, in which case it uses the first server in your Claude, Cursor or VS Code config.
32
+
33
+ Something not connecting? Run `misroute doctor`. It checks the server, each key, the price and one call per model, and prints the fix for every problem.
34
+
35
+ The full loop on the bundled test server, with Claude Opus 5.5:
36
+
37
+ ```console
38
+ $ misroute init --demo
39
+ $ misroute lint --draft
40
+ 9 tools, 2 with weak descriptions
41
+ get_attachment missing
42
+ draft: Returns the attachment identified by attachment_id
43
+ get_document path only
44
+ draft: Returns the full details of a single document identified by its ID
45
+
46
+ $ misroute discover
47
+ up to 495 model calls, up to $3.43
48
+
49
+ tool sibling score signals seen in
50
+ get_attachment get_document 1.00 wrong 0.33 hedge 0.42 removal 1.00 opus code, opus json
51
+ get_case_comments get_case_events 0.50 hedge 0.25 removal 1.00 opus code, opus json
52
+ get_case_events update_case_meta 0.25 wrong 0.25 opus code
53
+
54
+ $ misroute explain --pair get_attachment,get_document
55
+ $ misroute fix --pair get_case_comments,get_case_events --modes json
56
+ $ misroute report
57
+ ```
58
+
59
+ ## Connect your tools
60
+
61
+ | Source | In misroute.yaml |
62
+ |---|---|
63
+ | MCP server over HTTP | `server: {url: https://your-server/mcp, auth: bearer_env:MCP_TOKEN}` |
64
+ | Local MCP server | `server: {command: [npx, your-mcp-server]}` |
65
+ | OpenAPI file | `server: {openapi: api.json}` |
66
+ | Tool list in MCP, OpenAI or Anthropic format | `server: {tools: tools.json}` |
67
+ | Your Claude, Cursor or VS Code config | `misroute init --from claude` |
68
+ | Tools in Python | `Project.from_tools([...], "anthropic:claude-sonnet-5-5")` |
69
+
70
+ Add `include_tools: ["*issue*"]` to test a slice of a large server.
71
+
72
+ ## Models
73
+
74
+ Write models as `provider:model`:
75
+
76
+ | Shorthand | Uses |
77
+ |---|---|
78
+ | `anthropic:claude-opus-5-5` | `ANTHROPIC_API_KEY` |
79
+ | `foundry:claude-opus-5-5` | `ANTHROPIC_FOUNDRY_API_KEY` and `ANTHROPIC_FOUNDRY_BASE_URL` |
80
+ | `bedrock:anthropic.claude-opus-5-5` | your AWS credentials |
81
+ | `azure:your-deployment` | `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` |
82
+ | `openai:gpt-5.6` | `OPENAI_API_KEY` |
83
+ | `openrouter:anthropic/claude-opus-5.5` | `OPENROUTER_API_KEY` |
84
+ | `ollama:qwen3.5:9b`, `vllm:Qwen/Qwen3-14B`, `lmstudio:model` | a local server |
85
+
86
+ Prices come from the public OpenRouter catalog, cached for a week, and local models cost nothing. Set `price_in` and `price_out` to override. Every paid run shows an estimate first and stops at `budget_usd`. Behind a company TLS proxy, set `system_certs: true`.
87
+
88
+ ```yaml
89
+ server:
90
+ url: https://your-server/mcp
91
+ models:
92
+ - foundry:claude-opus-5-5
93
+ generator: openrouter:deepseek/deepseek-v4.1-flash
94
+ validator: openrouter:google/gemini-3.8-flash
95
+ modes: [json, code]
96
+ budget_usd: 5
97
+ ```
98
+
99
+ The generator writes test requests and the validator keeps only those it routes to the right tool. A model from another family than the one under test works best for both.
100
+
101
+ ## Commands
102
+
103
+ | Command | Does |
104
+ |---|---|
105
+ | `misroute quick` | lint and a capped discover in one go, then opens the report |
106
+ | `misroute init` | writes misroute.yaml from your MCP client config and environment |
107
+ | `misroute doctor` | checks the setup and prints a fix for each problem |
108
+ | `misroute lint` | flags missing, name only and path only descriptions and drafts replacements |
109
+ | `misroute discover` | ranks confused tool pairs from model behaviour |
110
+ | `misroute probe` | tests risky pairs with requests that borrow words from the sibling |
111
+ | `misroute explain` | shows the misrouted requests for a pair and what the model called |
112
+ | `misroute fix` | tests a boundary clause against a neutral edit, or against a drafted description for undescribed tools |
113
+ | `misroute check` | tests one proposed description on held out requests |
114
+ | `misroute report` | writes a self-contained HTML report |
115
+ | `misroute ui` | local web interface to set up, run, read results and approve held writes |
116
+ | `misroute ci` | fails on exceeded limits, or with `--changed` on descriptions that got worse |
117
+ | `misroute catalog` | prints tool hashes and descriptions for change-aware CI |
118
+ | `misroute serve-mcp` | runs misroute as an MCP server for agents |
119
+ | `misroute guard serve` | runs an MCP proxy that logs and guards real tool calls |
120
+ | `misroute site` | builds a static results site |
121
+ | `misroute schema` | prints the JSON Schema of a command's output |
122
+
123
+ ## How discover ranks pairs
124
+
125
+ Each tool gets requests written from the user's goal, without the tool's own words. Requests run against the full tool list in rotated orders. Each pair gets a score from three signals: the share of one tool's requests that went to the other, the share where the model called both, and where requests go when one tool is removed. The score orders pairs for attention, and the rates behind it are the numbers to quote.
126
+
127
+ The ranking marks what it cannot vouch for: pairs whose request sets differ in surface words, tools with few validated requests, and tools the validator left without any, marked UNTESTED. Tools that models answer without calling anything are listed separately.
128
+
129
+ Code mode runs each program in Monty against recorded fakes and reports faked tools, pasted stubs, programs that never call anything, invented or reused cursors and unread pages.
130
+
131
+ ## Fix and check
132
+
133
+ `fix` writes one boundary sentence per tool and runs the original, a baseline and the clause on identical requests. The baseline is a neutral rewrite of the same length, or lint's drafted description when a tool has none. Verdicts use an exact McNemar test with Holm correction per pair and a sign flip test across pairs, and every result shows errors in both directions and leakage to other tools.
134
+
135
+ `check` tests one proposed description. It splits requests into a tuning set and a held out set, gives its verdict on the held out set only, and requires a fresh held out set after three verdicts on the same tool, so a description cannot be tuned to the test requests.
136
+
137
+ ## For agents
138
+
139
+ Every command takes `--json` and prints one object with a `schema_version`. Exit codes are `0` passed, `1` limits failed or worse, `2` not enough data, `3` budget or config problem. Paid commands take `--max-cost` and `--estimate-only`, and never prompt without a terminal. `--progress ndjson` streams progress to stderr. `misroute schema check` prints the output schema.
140
+
141
+ Add misroute as an MCP server:
142
+
143
+ ```console
144
+ $ claude mcp add misroute -- uvx misroute serve-mcp
145
+ ```
146
+
147
+ ```json
148
+ {"mcpServers": {"misroute": {"command": "uvx", "args": ["misroute", "serve-mcp"]}}}
149
+ ```
150
+
151
+ The second form goes in `.cursor/mcp.json`. VS Code takes the same block under `"servers"` in `.vscode/mcp.json`.
152
+
153
+ The server exposes `lint_descriptions`, `draft_description`, `check_description`, `explain_pair`, `suggest_fix`, `get_report` and background jobs with `start_job`, `job_status` and `cancel_job` for runs longer than a tool call. Paid tools refuse to run above `max_cost`. misroute never edits your tools.
154
+
155
+ Claude Code users can install the plugin, which adds the server and a skill:
156
+
157
+ ```console
158
+ /plugin marketplace add philippyt/tool-misroute
159
+ /plugin install misroute@misroute
160
+ ```
161
+
162
+ See [AGENTS.md](AGENTS.md) and [llms.txt](llms.txt) for the agent workflow.
163
+
164
+ ## CI
165
+
166
+ Check the descriptions a pull request changed, and comment the result:
167
+
168
+ ```yaml
169
+ - uses: actions/checkout@v5
170
+ with: {fetch-depth: 0}
171
+ - uses: philippyt/tool-misroute@v0.2.4
172
+ with:
173
+ config: misroute.yaml
174
+ max-cost: "2"
175
+ ```
176
+
177
+ Or wire it yourself with `misroute catalog` on the base branch and `misroute ci --changed --baseline tools.json --markdown comment.md --junit results.xml`. `misroute lint --sarif lint.sarif` feeds GitHub code scanning, and `--strict` fails on weak descriptions.
178
+
179
+ As a pre-commit hook:
180
+
181
+ ```yaml
182
+ - repo: https://github.com/philippyt/tool-misroute
183
+ rev: v0.2.4
184
+ hooks:
185
+ - id: misroute-lint
186
+ ```
187
+
188
+ ## In tests
189
+
190
+ ```python
191
+ def test_routing(misroute_routes):
192
+ misroute_routes(my_tools, "anthropic:claude-sonnet-5-5", min_accuracy=0.95)
193
+ ```
194
+
195
+ The `misroute_routes` fixture comes with the package. It runs a small capped discover, fails with the confused pairs when accuracy is below the threshold, and caches responses so reruns are free. Outside pytest, use `from misroute.testing.routes import assert_routes`.
196
+
197
+ ## Guard
198
+
199
+ As a proxy in front of any MCP server:
200
+
201
+ ```console
202
+ $ misroute guard serve -c misroute.yaml
203
+ $ misroute guard confirm 8ba64a3fc925
204
+ ```
205
+
206
+ Inside an MCP server built with the Python SDK:
207
+
208
+ ```python
209
+ server = MCPServer("cases", middleware=[guard.middleware()])
210
+ ```
211
+
212
+ Around tool functions in any agent framework, keeping their signatures:
213
+
214
+ ```python
215
+ @guard.tool
216
+ def update_case(case_id: str, status: str) -> dict: ...
217
+ ```
218
+
219
+ The guard logs every call, flags hedges and quick switches between siblings, and holds a second write to a sibling tool until it is approved in `misroute ui` or with `misroute guard confirm`. `misroute report` adds a Production section from the guard log.
220
+
221
+ ## Results
222
+
223
+ Everything lands in `.misroute/`: a DuckDB database, JSONL transcripts of every run and a response cache.
224
+
225
+ ```console
226
+ $ duckdb .misroute/misroute.duckdb "select target, chosen, count(*) from trials where wrong group by all order by 3 desc"
227
+ ```
228
+
229
+ ## Python API
230
+
231
+ ```python
232
+ from misroute import Project
233
+
234
+ proj = Project.from_yaml("misroute.yaml")
235
+ found = await proj.discover()
236
+ result = await proj.fix(top=3, trials=96)
237
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "misroute"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Find the tools your agent confuses and test whether a fix helps, against a neutral edit"
5
5
  requires-python = ">=3.11"
6
6
  readme = "README.md"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "pyyaml>=6.0.3",
35
35
  "rich>=15.0.0",
36
36
  "scipy>=1.17.1",
37
+ "truststore>=0.10.5",
37
38
  "typer>=0.27.3",
38
39
  ]
39
40
 
@@ -48,12 +49,16 @@ azure = ["azure-identity>=1.27.0"]
48
49
  [project.scripts]
49
50
  misroute = "misroute.cli:app"
50
51
 
52
+ [project.entry-points.pytest11]
53
+ misroute = "misroute.testing.plugin"
54
+
51
55
  [build-system]
52
56
  requires = ["uv_build>=0.10.3,<0.11.0"]
53
57
  build-backend = "uv_build"
54
58
 
55
59
  [dependency-groups]
56
60
  dev = [
61
+ "jsonschema>=4.26.0",
57
62
  "pytest>=9.2.0",
58
63
  "pytest-asyncio>=1.4.1",
59
64
  "ruff>=0.17.3",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "misroute"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Find the tools your agent confuses and test whether a fix helps, against a neutral edit"
5
5
  authors = [
6
6
  { name = "philippyt", email = "philipaha@proton.me" }
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "pyyaml>=6.0.3",
32
32
  "rich>=15.0.0",
33
33
  "scipy>=1.17.1",
34
+ "truststore>=0.10.5",
34
35
  "typer>=0.27.3",
35
36
  ]
36
37
 
@@ -45,12 +46,16 @@ azure = [
45
46
  [project.scripts]
46
47
  misroute = "misroute.cli:app"
47
48
 
49
+ [project.entry-points.pytest11]
50
+ misroute = "misroute.testing.plugin"
51
+
48
52
  [build-system]
49
53
  requires = ["uv_build>=0.10.3,<0.11.0"]
50
54
  build-backend = "uv_build"
51
55
 
52
56
  [dependency-groups]
53
57
  dev = [
58
+ "jsonschema>=4.26.0",
54
59
  "pytest>=9.2.0",
55
60
  "pytest-asyncio>=1.4.1",
56
61
  "ruff>=0.17.3",