autorouter-mcp 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/dist/cli.js +22886 -0
- package/package.json +68 -0
- package/server.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Webb Ventures
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# autorouter
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Webb-Ventures/autorouter/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/autorouter-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
One tool instead of every tool.
|
|
8
|
+
|
|
9
|
+
Every MCP server you register injects its full tool list — names, descriptions,
|
|
10
|
+
JSON schemas — into the system prompt on every single turn. Five servers and a
|
|
11
|
+
handful of skills is routinely 20–30k tokens of permanent overhead, paid whether
|
|
12
|
+
or not the task touches any of them. It also makes tool selection _worse_: more
|
|
13
|
+
candidates means more mis-picks.
|
|
14
|
+
|
|
15
|
+
autorouter is an MCP server that exposes ~4 stable tools and hides everything
|
|
16
|
+
else behind a search. The model asks for what it needs in natural language, gets
|
|
17
|
+
back a few ranked candidates, reads one schema, and calls it through the router.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
find_capabilities({ query: "why did my deploy fail" })
|
|
21
|
+
→ mcp:railway/get_deployment_logs (tool)
|
|
22
|
+
→ mcp:datadog/search_datadog_logs (tool)
|
|
23
|
+
→ skill:incident-triage (skill)
|
|
24
|
+
|
|
25
|
+
describe_capability({ id: "mcp:railway/get_deployment_logs" })
|
|
26
|
+
call_capability({ id: "mcp:railway/get_deployment_logs", arguments: { lines: 50 } })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install -g @webb-ventures/autorouter-mcp # or: npx autorouter-mcp <command>
|
|
33
|
+
autorouter init --target claude # claude | codex | cursor | vscode
|
|
34
|
+
autorouter adopt --target claude # ← the step that actually saves context
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`adopt` is not optional.** Registering the router alongside your existing
|
|
38
|
+
servers is a net _increase_: their schemas are still loaded and the router adds
|
|
39
|
+
four more tools. `adopt` moves the downstream entries out of the harness config
|
|
40
|
+
and into the router's own, so the harness loads one server and the router still
|
|
41
|
+
reaches all of them.
|
|
42
|
+
|
|
43
|
+
MCP servers are not the whole bill. Skills and plugins load through entirely
|
|
44
|
+
separate mechanisms — every `SKILL.md` contributes its name and description to
|
|
45
|
+
the system prompt, and an enabled plugin contributes its own skills, commands
|
|
46
|
+
_and_ servers just by being installed. On Claude Code, `adopt` handles those too,
|
|
47
|
+
using the only two levers the harness exposes:
|
|
48
|
+
|
|
49
|
+
| what | how | effect |
|
|
50
|
+
| ------- | ---------------------------------------------- | ------------------------------------------------------- |
|
|
51
|
+
| skills | `skillOverrides[name] = "user-invocable-only"` | out of the model's context; `/name` still works for you |
|
|
52
|
+
| plugins | `enabledPlugins[id] = false` | plugin's skills, commands and servers all stop loading |
|
|
53
|
+
|
|
54
|
+
The router still finds all of them — it reads `~/.claude/skills` and
|
|
55
|
+
`installed_plugins.json` directly, which record _installation_, not what the
|
|
56
|
+
harness has enabled. That is what makes disabling a plugin a move rather than a
|
|
57
|
+
deletion.
|
|
58
|
+
|
|
59
|
+
Two things are refused rather than done quietly. A plugin whose MCP servers the
|
|
60
|
+
router cannot reach (usually OAuth, where the harness holds the token and the
|
|
61
|
+
router does not) stays enabled — disabling it would take away a server that
|
|
62
|
+
currently works. So does a plugin whose servers the router never learned about,
|
|
63
|
+
which happens if you drop `"plugins"` from `import`. Both are reported as `skip`
|
|
64
|
+
lines with the reason.
|
|
65
|
+
|
|
66
|
+
Use `--servers-only` to keep the old behaviour, `--skill-mode off` to remove the
|
|
67
|
+
slash command as well, and `--keep-skill` / `--keep-plugin` to exempt individual
|
|
68
|
+
ones. Codex, Cursor and VS Code have no skill or plugin concept, so there is
|
|
69
|
+
nothing extra to do there.
|
|
70
|
+
|
|
71
|
+
Run `autorouter doctor` to see the difference:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
## Context cost
|
|
75
|
+
exposing everything: ~23,172 tokens
|
|
76
|
+
router surface: ~700 tokens
|
|
77
|
+
still loaded direct: ~20,115 tokens (18,657 servers + 1,458 skills)
|
|
78
|
+
actually saved: ~2,357 tokens per request (10%)
|
|
79
|
+
|
|
80
|
+
## Not yet adopted
|
|
81
|
+
claude
|
|
82
|
+
servers: datadog:mcp, semble, railway, pencil, gemini
|
|
83
|
+
plugins: frontend-design
|
|
84
|
+
skills: lavish, react-doctor, find-skills, use-railway
|
|
85
|
+
→ autorouter adopt --target claude
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That "still loaded direct" line is what adoption removes. Every removal is backed
|
|
89
|
+
up verbatim to `~/.autorouter/adopted/` before anything is written;
|
|
90
|
+
`autorouter restore --target claude` puts it back byte for byte.
|
|
91
|
+
|
|
92
|
+
## How it finds things
|
|
93
|
+
|
|
94
|
+
Retrieve, then rerank.
|
|
95
|
+
|
|
96
|
+
1. **BM25** over every capability, always on, no dependencies. The tokenizer
|
|
97
|
+
splits camelCase and snake_case (`execute_sql` → `execute`, `sql`, `executesql`)
|
|
98
|
+
and boosts fields: name ×3, keywords ×2, description ×1, schema ×0.5.
|
|
99
|
+
2. **Embeddings**, optional. Voyage or OpenAI; scores are min-max normalized and
|
|
100
|
+
fused `0.5 × bm25 + 0.5 × cosine`. An absent or unreachable provider is not an
|
|
101
|
+
error — it degrades to pure lexical.
|
|
102
|
+
3. **A selector model** reranks the shortlist. It should be the cheapest model in
|
|
103
|
+
whichever harness you are using, so `init` asks which one when it cannot infer
|
|
104
|
+
it, and stores the answer in that harness's own server entry.
|
|
105
|
+
|
|
106
|
+
### The selector backend
|
|
107
|
+
|
|
108
|
+
Three ways to reach a model, tried in this order under `"mode": "auto"`:
|
|
109
|
+
|
|
110
|
+
| backend | how | when it fires |
|
|
111
|
+
| ---------- | ------------------------------------------- | --------------------------------------------------------------- |
|
|
112
|
+
| `sampling` | `sampling/createMessage` back to the host | the host declares the capability — few do; Claude Code does not |
|
|
113
|
+
| `api` | direct HTTPS to Anthropic / OpenAI / Ollama | `selector.apiKeyEnv` names a variable that is actually set |
|
|
114
|
+
| `cli` | `claude -p` or `codex exec`, headless | a harness CLI is on `PATH` |
|
|
115
|
+
|
|
116
|
+
The CLI backend is the one that usually fires, and it exists because the other two
|
|
117
|
+
usually cannot. Sampling is the protocol's own answer and almost nothing
|
|
118
|
+
implements it. The API backend then asks for an `ANTHROPIC_API_KEY` that a Claude
|
|
119
|
+
Code subscriber has no reason to own — they logged in, they did not buy a key — so
|
|
120
|
+
the router would degrade to raw index order while telling them to go purchase
|
|
121
|
+
access to a model they are already paying for. Meanwhile `claude` is sitting on
|
|
122
|
+
`PATH`, already authenticated. Shelling out to it reuses that login with nothing
|
|
123
|
+
to configure and no second bill.
|
|
124
|
+
|
|
125
|
+
What it costs is process startup: 3-5s for a 30-candidate list against
|
|
126
|
+
`claude -p --model haiku`, against roughly 1s for a direct HTTPS request. That is
|
|
127
|
+
why a configured API key still outranks it. Selections are memoized per query and
|
|
128
|
+
candidate set for the life of the process, so the price is paid once per distinct
|
|
129
|
+
search, not once per turn.
|
|
130
|
+
|
|
131
|
+
Getting there took stripping the harness back to a reranker. Out of the box the
|
|
132
|
+
same call took 22s, because a headless agent boots hooks, LSP, plugin sync and
|
|
133
|
+
`CLAUDE.md` discovery it has no use for, and then spends 6,223 output tokens
|
|
134
|
+
thinking about a list it could rank in 168 — and returns a _worse_ answer for the
|
|
135
|
+
deliberation. So the subprocess runs with `--bare --tools "" --setting-sources ""`
|
|
136
|
+
and `MAX_THINKING_TOKENS=0` (Codex: `--ephemeral -s read-only`). The empty tool
|
|
137
|
+
list is a correctness property before it is a saving: a selector that could edit
|
|
138
|
+
files would be a different program.
|
|
139
|
+
|
|
140
|
+
```jsonc
|
|
141
|
+
"selector": {
|
|
142
|
+
"mode": "auto", // auto | sampling | cli | api | off
|
|
143
|
+
"provider": "cli",
|
|
144
|
+
"cliCommand": "claude", // only needed if the binary is not under its usual name
|
|
145
|
+
"model": "haiku",
|
|
146
|
+
"candidates": 30,
|
|
147
|
+
"maxResults": 8,
|
|
148
|
+
"timeoutMs": 20000 // raised automatically to the backend's own floor
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It is also started with an empty MCP config (`--strict-mcp-config`,
|
|
153
|
+
`mcp_servers={}`) — inheriting the router's own wiring would load the exact
|
|
154
|
+
catalog the router exists to keep out of context, and on a bad day recurse into
|
|
155
|
+
the router itself.
|
|
156
|
+
|
|
157
|
+
`model` is passed through only when you set it. Codex rejects model names an
|
|
158
|
+
account's plan does not carry, so the account default is what it gets otherwise.
|
|
159
|
+
|
|
160
|
+
`mode: "off"` skips reranking entirely and returns raw index order, which is
|
|
161
|
+
fast, free, and noticeably worse.
|
|
162
|
+
|
|
163
|
+
Downstream servers are **not** spawned at startup. The catalog is built once and
|
|
164
|
+
persisted to `~/.cache/autorouter/catalog.json`; a server is cold-started only
|
|
165
|
+
when one of its capabilities is first called.
|
|
166
|
+
|
|
167
|
+
## What it indexes
|
|
168
|
+
|
|
169
|
+
| kind | source |
|
|
170
|
+
| ---------------------------- | --------------------------------------------------------------- |
|
|
171
|
+
| `tool`, `prompt`, `resource` | every configured MCP server (following `nextCursor` pagination) |
|
|
172
|
+
| `skill` | `**/SKILL.md` under your skill paths and plugin `skills/` dirs |
|
|
173
|
+
| `command`, `agent` | plugin `commands/*.md` and `agents/*.md` |
|
|
174
|
+
|
|
175
|
+
Skills, commands and agents are also republished as [slash commands](#slash-commands).
|
|
176
|
+
|
|
177
|
+
Harness configs are imported rather than duplicated: `~/.claude.json` (global and
|
|
178
|
+
per-project) and `.mcp.json`, `~/.codex/config.toml`, `~/.cursor/mcp.json`,
|
|
179
|
+
`~/.vscode/mcp.json`, and installed Claude Code plugins.
|
|
180
|
+
|
|
181
|
+
## Configuration
|
|
182
|
+
|
|
183
|
+
`autorouter.json` — looked up at `$AUTOROUTER_CONFIG`, `./.autorouter.json`,
|
|
184
|
+
`./autorouter.json`, `~/.config/autorouter/config.json`, `~/.autorouter.json`.
|
|
185
|
+
|
|
186
|
+
```jsonc
|
|
187
|
+
{
|
|
188
|
+
"import": ["claude", "codex", "cursor", "plugins"],
|
|
189
|
+
"servers": { "custom": { "command": "…", "args": [] } },
|
|
190
|
+
"skillPaths": ["~/.claude/skills", ".claude/skills"],
|
|
191
|
+
"exclude": ["gemini.generate_video"], // never surfaced at all
|
|
192
|
+
"alwaysExpose": ["semble.search"], // stays first-class, never adopted
|
|
193
|
+
"confirm": ["supabase.execute_sql"], // first call is rejected, must re-issue
|
|
194
|
+
"selector": { "mode": "auto", "model": "claude-haiku-4-5-20251001" },
|
|
195
|
+
"embeddings": {
|
|
196
|
+
"provider": "voyage",
|
|
197
|
+
"model": "voyage-3-lite",
|
|
198
|
+
"apiKeyEnv": "VOYAGE_API_KEY",
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Env overrides (`AUTOROUTER_IMPORT`, `AUTOROUTER_SELECTOR_MODEL`,
|
|
204
|
+
`AUTOROUTER_SELECTOR_MODE`, `AUTOROUTER_EMBEDDINGS_PROVIDER`, …) let each harness
|
|
205
|
+
pin its own behaviour without a shared global file. `AUTOROUTER_HOME` redirects
|
|
206
|
+
every home-relative path, for tests and containers.
|
|
207
|
+
|
|
208
|
+
## Slash commands
|
|
209
|
+
|
|
210
|
+
Adopting a plugin into the router would otherwise cost you its slash commands:
|
|
211
|
+
Claude Code reads `commands/*.md` off disk, and nothing over MCP can add to that
|
|
212
|
+
list. What it _does_ surface as slash commands are MCP prompts, so every skill,
|
|
213
|
+
plugin command and subagent in the catalog is republished as one:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
/mcp__autorouter__find <what you want to do>
|
|
217
|
+
/mcp__autorouter__datadog_ddviz
|
|
218
|
+
/mcp__autorouter__supabase_supabase
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Bodies are substituted the way the native loader substitutes them — `$ARGUMENTS`
|
|
222
|
+
for everything you typed, `$1`..`$9` positionally — so a command file written for
|
|
223
|
+
Claude Code behaves identically through the router. `argument-hint` frontmatter
|
|
224
|
+
is passed through as the argument description. Names are prefixed with the owning
|
|
225
|
+
plugin, so two plugins shipping a `setup` command both stay reachable rather than
|
|
226
|
+
one silently shadowing the other.
|
|
227
|
+
|
|
228
|
+
Tools are deliberately not published here. A prompt returns text for the model to
|
|
229
|
+
act on and cannot return a tool result, so a slash command for `execute_sql` would
|
|
230
|
+
look callable and do nothing. Tools reach the model through search instead.
|
|
231
|
+
|
|
232
|
+
## Harness support
|
|
233
|
+
|
|
234
|
+
Searching returns a tool with its **full description and input schema**, not a
|
|
235
|
+
summary — the same information a native tool listing carries, because a truncated
|
|
236
|
+
description leaves the model guessing argument names, and a guessed argument is
|
|
237
|
+
the whole difference between roughly reliable and reliable.
|
|
238
|
+
|
|
239
|
+
Better still, where the client honours `notifications/tools/list_changed`,
|
|
240
|
+
`find_capabilities` **promotes** what it matched to real first-class tools. The
|
|
241
|
+
model then makes an ordinary tool call: the host validates arguments against the
|
|
242
|
+
real schema, the permission prompt names the real tool rather than
|
|
243
|
+
`call_capability`, and the router is not in the execution path at all. It becomes
|
|
244
|
+
a loader that decides what is in your tool list, not a middleman on every call.
|
|
245
|
+
|
|
246
|
+
| supports `listChanged` | proxy only |
|
|
247
|
+
| ----------------------------------------------- | ------------------------------------------------ |
|
|
248
|
+
| Claude Code ≥ 2.1.232, GitHub Copilot, opencode | Codex, Gemini CLI, Claude Desktop, Vercel AI SDK |
|
|
249
|
+
|
|
250
|
+
Where it is unsupported the schema travels inline in the search result instead,
|
|
251
|
+
and `activate_capabilities` is never advertised, so the model is never told about
|
|
252
|
+
a tool it cannot use. Override the detection with `AUTOROUTER_DYNAMIC=on|off|auto`.
|
|
253
|
+
A tool registered mid-turn may not be callable until the next turn on Claude Code,
|
|
254
|
+
so `call_capability` always remains as the same-turn fallback and returns an
|
|
255
|
+
identical result.
|
|
256
|
+
|
|
257
|
+
## Token budgets
|
|
258
|
+
|
|
259
|
+
A router that leaks context is just a slower way of loading everything, so the
|
|
260
|
+
three surfaces that persist or accumulate are each bounded in **tokens**, not in
|
|
261
|
+
item count. Counting items looks like a bound and is not one: tool definitions in
|
|
262
|
+
a real catalog range from 199 to 1,748 tokens, so "15 tools" is anywhere between
|
|
263
|
+
3k and 26k depending only on what the model happened to search for.
|
|
264
|
+
|
|
265
|
+
| surface | budget | lifetime |
|
|
266
|
+
| ------------------------- | --------------------------------- | ---------- |
|
|
267
|
+
| promoted tool list | 3,000 tok, LRU eviction | session |
|
|
268
|
+
| inline schemas per search | 700 tok, spent top-down | one result |
|
|
269
|
+
| prompt list | descriptions clamped to 180 chars | session |
|
|
270
|
+
|
|
271
|
+
Tools promoted by the search in flight are never evicted — a tool that vanishes
|
|
272
|
+
between being offered and being called is worse than one never offered.
|
|
273
|
+
|
|
274
|
+
Schemas are **compacted** everywhere they are repeated: `$schema`, `title` and
|
|
275
|
+
`examples` are dropped, and prose is trimmed, more aggressively the deeper it
|
|
276
|
+
sits. Every structural field survives untouched — names, types, enums, `required`,
|
|
277
|
+
nesting, and `additionalProperties: false` — because dropping one of those turns a
|
|
278
|
+
valid call into a guessed one. On this machine that is a 31% cut across the
|
|
279
|
+
catalog and 3× on the worst offenders, with no effect on what is callable.
|
|
280
|
+
`describe_capability` is the exception and returns the schema verbatim: it exists
|
|
281
|
+
precisely to recover anything a budgeted result had to leave out.
|
|
282
|
+
|
|
283
|
+
## CLI
|
|
284
|
+
|
|
285
|
+
For agents that have a shell but no MCP, the same engine is directly usable:
|
|
286
|
+
|
|
287
|
+
```sh
|
|
288
|
+
autorouter search "chart a csv"
|
|
289
|
+
autorouter search "query postgres" --raw --limit 5
|
|
290
|
+
autorouter describe skill:dataviz
|
|
291
|
+
autorouter call mcp:railway/get_logs --args '{"lines":50}'
|
|
292
|
+
autorouter list --kind skill
|
|
293
|
+
autorouter doctor
|
|
294
|
+
autorouter reindex
|
|
295
|
+
|
|
296
|
+
autorouter adopt --target claude --dry-run # preview, change nothing
|
|
297
|
+
autorouter adopt --target claude --servers-only # skip skills and plugins
|
|
298
|
+
autorouter adopt --target claude --keep supabase --keep-plugin datadog
|
|
299
|
+
autorouter restore --target claude # undo the most recent adopt
|
|
300
|
+
|
|
301
|
+
autorouter login # which servers need a grant
|
|
302
|
+
autorouter login datadog:mcp # authorize one (opens a browser)
|
|
303
|
+
autorouter logout datadog:mcp # forget a stored grant
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## OAuth servers
|
|
307
|
+
|
|
308
|
+
Several of the heaviest MCP servers — Datadog, Supabase, Linear — carry no
|
|
309
|
+
credentials in their config at all. Datadog's headers expand to `""`, Supabase's
|
|
310
|
+
are only `X-Source-*`. The working token is an OAuth grant your harness obtained
|
|
311
|
+
and keeps in its own credential store, and it was issued to that harness.
|
|
312
|
+
|
|
313
|
+
The router does not read it. It runs its own authorization-code flow and holds
|
|
314
|
+
its own grant, which means it also works under Codex and Cursor, neither of which
|
|
315
|
+
has a token to borrow:
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
autorouter login datadog:mcp
|
|
319
|
+
autorouter login supabase:supabase
|
|
320
|
+
autorouter reindex
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Registration is RFC 7591 dynamic client registration, so there is no app to
|
|
324
|
+
create first. Tokens live in `~/.autorouter/oauth/<server>.json` at `0600` and
|
|
325
|
+
are refreshed automatically; `logout` deletes them. The loopback redirect uses a
|
|
326
|
+
fixed port (33418, `--port` or `$AUTOROUTER_OAUTH_PORT` to change it) because the
|
|
327
|
+
redirect URI is baked into the registration a provider stores — a grant obtained
|
|
328
|
+
on one port cannot be refreshed from another.
|
|
329
|
+
|
|
330
|
+
### Choosing permissions
|
|
331
|
+
|
|
332
|
+
A dynamically registered client defaults to every scope the provider advertises,
|
|
333
|
+
which for Supabase includes `database:write` and `edge_functions:write` — a token
|
|
334
|
+
able to drop tables, held on behalf of a search tool. Scopes are the only
|
|
335
|
+
restriction that survives a prompt injection, so pick them deliberately:
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
autorouter login supabase --list-scopes # what this server offers
|
|
339
|
+
autorouter login supabase --read-only # 8 of 13, no write scopes
|
|
340
|
+
autorouter login supabase --scopes "projects:read,database:read"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`--read-only` keeps the scopes whose names do not grant mutation. It is a
|
|
344
|
+
heuristic over naming conventions (`:write`, `admin`, `manage`, `all`) and it
|
|
345
|
+
cannot see through an opaque name like GitHub's `repo`, which grants write while
|
|
346
|
+
reading as neutral — `--scopes` is the exact lever when it guesses wrong. Where a
|
|
347
|
+
provider offers no read-only subset at all, `--read-only` fails loudly rather than
|
|
348
|
+
requesting an empty scope, which most providers read as "give the default".
|
|
349
|
+
|
|
350
|
+
Scopes are fixed when the grant is issued, so narrowing an existing one means
|
|
351
|
+
`--force` and a fresh authorization. A re-login inherits the previous narrowing
|
|
352
|
+
unless you pass a new one, and `autorouter login` with no argument prints what
|
|
353
|
+
each stored grant actually covers.
|
|
354
|
+
|
|
355
|
+
Until a server has a grant, `adopt` refuses to move it or to disable the plugin
|
|
356
|
+
that supplies it. Moving a server the router cannot reach would delete a working
|
|
357
|
+
capability, so `doctor` and `adopt` both print the exact `login` command instead.
|
|
358
|
+
|
|
359
|
+
## The trade-off, stated plainly
|
|
360
|
+
|
|
361
|
+
Routing means your host's permission prompt sees `call_capability`, not
|
|
362
|
+
`supabase.execute_sql`. That is a real loss of granularity and this tool does not
|
|
363
|
+
pretend otherwise. Three mitigations: `exclude` means a capability is never
|
|
364
|
+
surfaced; `confirm` rejects the first attempt with the resolved target spelled
|
|
365
|
+
out and requires the model to re-issue with `confirm: true`; and every result is
|
|
366
|
+
prefixed with the resolved `server/tool` so the transcript stays auditable.
|
|
367
|
+
|
|
368
|
+
Adopting skills and plugins edits `~/.claude/settings.json`, a file you also edit
|
|
369
|
+
by hand. The whole file is backed up verbatim alongside the server moves and
|
|
370
|
+
`restore` rewrites it byte for byte, but it is a shared file and worth knowing
|
|
371
|
+
about. Hidden skills default to `user-invocable-only` rather than `off` for the
|
|
372
|
+
same reason: `/name` keeps working, so the capability is relocated, not removed.
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
bun install
|
|
378
|
+
bun test
|
|
379
|
+
bun run src/cli.ts doctor
|
|
380
|
+
bun run build # bun build src/cli.ts --target=node --outfile dist/cli.js
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The build targets Node so `npx` works for people who do not have Bun.
|
|
384
|
+
|
|
385
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the invariants worth knowing before
|
|
386
|
+
changing anything — chiefly that nothing is spawned at startup, that every write
|
|
387
|
+
`adopt` makes has to restore byte for byte, and that the selector subprocess runs
|
|
388
|
+
with no tools by design.
|
|
389
|
+
|
|
390
|
+
## Licence
|
|
391
|
+
|
|
392
|
+
[MIT](LICENSE). Security reports go through [SECURITY.md](SECURITY.md), not the
|
|
393
|
+
public issue tracker.
|