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.
- misroute-0.2.4/PKG-INFO +273 -0
- misroute-0.2.4/README.md +237 -0
- {misroute-0.2.2 → misroute-0.2.4}/pyproject.toml +6 -1
- {misroute-0.2.2 → misroute-0.2.4}/pyproject.toml.orig +6 -1
- misroute-0.2.4/src/misroute/agent_server.py +210 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/cli.py +251 -12
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/config.py +55 -2
- misroute-0.2.4/src/misroute/doctor.py +84 -0
- misroute-0.2.4/src/misroute/explain.py +45 -0
- misroute-0.2.4/src/misroute/export.py +62 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/wrapper.py +56 -4
- misroute-0.2.4/src/misroute/mcpconfig.py +67 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/openai_compat.py +8 -1
- misroute-0.2.4/src/misroute/models/presets.py +122 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/project.py +16 -3
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/html.py +24 -3
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/templates/report.html.j2 +9 -0
- misroute-0.2.4/src/misroute/schemas/check.json +75 -0
- misroute-0.2.4/src/misroute/schemas/discover.json +163 -0
- misroute-0.2.4/src/misroute/schemas/doctor.json +36 -0
- misroute-0.2.4/src/misroute/schemas/error.json +31 -0
- misroute-0.2.4/src/misroute/schemas/estimate.json +38 -0
- misroute-0.2.4/src/misroute/schemas/explain.json +36 -0
- misroute-0.2.4/src/misroute/schemas/fix.json +65 -0
- misroute-0.2.4/src/misroute/schemas/lint.json +53 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/connect.py +1 -1
- misroute-0.2.4/src/misroute/server/static.py +64 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/store/cache.py +18 -0
- misroute-0.2.4/src/misroute/testing/plugin.py +14 -0
- misroute-0.2.4/src/misroute/testing/routes.py +28 -0
- misroute-0.2.4/src/misroute/ui/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/ui/app.py +33 -2
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/ui/index.html +118 -1
- misroute-0.2.4/src/misroute/ui/setup.py +123 -0
- misroute-0.2.2/PKG-INFO +0 -291
- misroute-0.2.2/README.md +0 -256
- misroute-0.2.2/src/misroute/agent_server.py +0 -98
- {misroute-0.2.2 → misroute-0.2.4}/LICENSE +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/check.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/codemode.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/confusion.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/hedge.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/nearmiss.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/rank.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/detectors/removal.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/clause.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/fix/stats.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/proxy.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/guard/rules.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/lint/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/lint/descriptions.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/anthropic.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/base.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/models/mock.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/code_mode.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/json_mode.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/modes/stubs.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/py.typed +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/report/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/gate.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/generate.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/requests/validate.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/fake_tools.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/sandbox/monty_runner.py +0 -0
- {misroute-0.2.2/src/misroute/server → misroute-0.2.4/src/misroute/schemas}/__init__.py +0 -0
- {misroute-0.2.2/src/misroute/site → misroute-0.2.4/src/misroute/server}/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/catalog.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/server/openapi.py +0 -0
- {misroute-0.2.2/src/misroute/store → misroute-0.2.4/src/misroute/site}/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/site/build.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/site/templates/index.html.j2 +0 -0
- {misroute-0.2.2/src/misroute/testing → misroute-0.2.4/src/misroute/store}/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/store/runs.py +0 -0
- {misroute-0.2.2/src/misroute/ui → misroute-0.2.4/src/misroute/testing}/__init__.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/testing/fake_server.py +0 -0
- {misroute-0.2.2 → misroute-0.2.4}/src/misroute/trial.py +0 -0
misroute-0.2.4/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
misroute-0.2.4/README.md
ADDED
|
@@ -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.
|
|
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.
|
|
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",
|