autorouter-mcp 0.2.4 → 0.2.6
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/README.md +58 -0
- package/dist/cli.js +544 -15
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -364,11 +364,40 @@ autorouter adopt --target claude --servers-only # skip skills and plugins
|
|
|
364
364
|
autorouter adopt --target claude --keep project-tools --keep-plugin ui-toolkit
|
|
365
365
|
autorouter restore --target claude # undo the most recent adopt
|
|
366
366
|
|
|
367
|
+
autorouter update # upgrade in place
|
|
368
|
+
autorouter update --check # is there a newer version?
|
|
369
|
+
|
|
367
370
|
autorouter login # which servers need a grant
|
|
368
371
|
autorouter login remote-server # authorize one (opens a browser)
|
|
372
|
+
autorouter login remote-server --device # headless: enter a code elsewhere
|
|
369
373
|
autorouter logout remote-server # forget a stored grant
|
|
370
374
|
```
|
|
371
375
|
|
|
376
|
+
## Updating
|
|
377
|
+
|
|
378
|
+
`autorouter update` upgrades in place, using the package manager that installed
|
|
379
|
+
the copy you are running:
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
autorouter update # detect, then upgrade
|
|
383
|
+
autorouter update --check # report the available version, install nothing
|
|
384
|
+
autorouter update --dry-run # print the command, do not run it
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Which manager to use is decided by where the running file sits on disk, not by
|
|
388
|
+
what happens to be on `PATH` — running `npm i -g` against a pnpm-managed global
|
|
389
|
+
installs a second copy that shadows the first, and you would then be upgrading
|
|
390
|
+
one install while running the other.
|
|
391
|
+
|
|
392
|
+
Three cases do not run a package manager, and say so instead: a copy unpacked by
|
|
393
|
+
`npx`/`bunx`/`pnpm dlx` (nothing to upgrade — those refetch every run), a git
|
|
394
|
+
checkout (`git pull && bun install && bun run build`), and a layout it cannot
|
|
395
|
+
identify. All three still report whether a newer version exists.
|
|
396
|
+
|
|
397
|
+
The router is a long-lived stdio server, so a harness that already has it
|
|
398
|
+
running keeps the old build until it respawns it — restart the harness, or
|
|
399
|
+
reconnect the MCP server, after updating.
|
|
400
|
+
|
|
372
401
|
## OAuth servers
|
|
373
402
|
|
|
374
403
|
Some remote MCP servers carry no credentials in their visible configuration.
|
|
@@ -391,6 +420,35 @@ fixed port (33418, `--port` or `$AUTOROUTER_OAUTH_PORT` to change it) because th
|
|
|
391
420
|
redirect URI is baked into the registration a provider stores — a grant obtained
|
|
392
421
|
on one port cannot be refreshed from another.
|
|
393
422
|
|
|
423
|
+
### Headless machines
|
|
424
|
+
|
|
425
|
+
The browser flow cannot work over SSH: it binds a loopback listener and expects
|
|
426
|
+
a browser on the same host to redirect into it. Two flows replace it, and on a
|
|
427
|
+
box with no display autorouter picks one of them by itself rather than timing
|
|
428
|
+
out waiting for a browser that was never going to open.
|
|
429
|
+
|
|
430
|
+
```sh
|
|
431
|
+
autorouter login remote-server --device # RFC 8628: enter a code on your phone
|
|
432
|
+
autorouter login remote-server --manual # paste the redirect URL back
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`--device` is the better one where the provider offers it. Nothing has to reach
|
|
436
|
+
back into the machine: it prints a short code and a URL, you enter them in a
|
|
437
|
+
browser on any other device, and the headless side polls until you are done.
|
|
438
|
+
Detection is the `device_authorization_endpoint` in the provider's metadata.
|
|
439
|
+
|
|
440
|
+
`--manual` is the fallback for providers that do not implement RFC 8628. It
|
|
441
|
+
prints the authorization URL, you open it elsewhere, and the browser is then
|
|
442
|
+
redirected to `http://localhost:33418/callback`, which will not load — nothing
|
|
443
|
+
is listening. That is expected: copy the URL out of the address bar and paste it
|
|
444
|
+
back. Pasting the whole URL is worth preferring over just the code, because the
|
|
445
|
+
`state` in it is what proves the code came from the login you started.
|
|
446
|
+
|
|
447
|
+
Neither flow binds a port. A device login registers a client that also works for
|
|
448
|
+
a later browser login from the same machine, so nothing has to be redone if the
|
|
449
|
+
box grows a display. Set `AUTOROUTER_ASSUME_HEADLESS=1` to force the detection
|
|
450
|
+
on a machine where it guesses wrong.
|
|
451
|
+
|
|
394
452
|
### Choosing permissions
|
|
395
453
|
|
|
396
454
|
A dynamically registered client may default to every scope the provider
|
package/dist/cli.js
CHANGED
|
@@ -18556,6 +18556,7 @@ async function clearAuth(server) {
|
|
|
18556
18556
|
async function hasAuth(server) {
|
|
18557
18557
|
return Boolean((await readAuth(server)).tokens?.access_token);
|
|
18558
18558
|
}
|
|
18559
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
18559
18560
|
var CLIENT_METADATA = {
|
|
18560
18561
|
client_name: "autorouter",
|
|
18561
18562
|
client_uri: "https://github.com/rileywebb/autorouter",
|
|
@@ -18583,6 +18584,12 @@ class FileTokenStore {
|
|
|
18583
18584
|
get clientMetadata() {
|
|
18584
18585
|
return { ...CLIENT_METADATA, redirect_uris: [this.redirectUrl] };
|
|
18585
18586
|
}
|
|
18587
|
+
get deviceClientMetadata() {
|
|
18588
|
+
return {
|
|
18589
|
+
...this.clientMetadata,
|
|
18590
|
+
grant_types: [...CLIENT_METADATA.grant_types, DEVICE_GRANT_TYPE]
|
|
18591
|
+
};
|
|
18592
|
+
}
|
|
18586
18593
|
async state() {
|
|
18587
18594
|
const value = randomUUID();
|
|
18588
18595
|
await writeAuth(this.server, { state: value });
|
|
@@ -19467,6 +19474,18 @@ function run(command, args, opts = {}) {
|
|
|
19467
19474
|
child.stdin.end();
|
|
19468
19475
|
});
|
|
19469
19476
|
}
|
|
19477
|
+
function runStreaming(command, args, opts = {}) {
|
|
19478
|
+
return new Promise((resolve, reject) => {
|
|
19479
|
+
const child = spawn2(command, args, {
|
|
19480
|
+
stdio: "inherit",
|
|
19481
|
+
cwd: opts.cwd,
|
|
19482
|
+
env: opts.env,
|
|
19483
|
+
shell: process.platform === "win32"
|
|
19484
|
+
});
|
|
19485
|
+
child.on("error", reject);
|
|
19486
|
+
child.on("close", (code) => resolve(code));
|
|
19487
|
+
});
|
|
19488
|
+
}
|
|
19470
19489
|
function which(command) {
|
|
19471
19490
|
const isWindows = process.platform === "win32";
|
|
19472
19491
|
const exts = isWindows ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
@@ -22063,6 +22082,166 @@ function truncate2(s, max) {
|
|
|
22063
22082
|
import { createServer } from "node:http";
|
|
22064
22083
|
import { spawn as spawn3 } from "node:child_process";
|
|
22065
22084
|
|
|
22085
|
+
// src/cli/device.ts
|
|
22086
|
+
function deviceEndpoint(metadata) {
|
|
22087
|
+
const endpoint = metadata?.device_authorization_endpoint;
|
|
22088
|
+
return typeof endpoint === "string" && endpoint ? endpoint : undefined;
|
|
22089
|
+
}
|
|
22090
|
+
function looksHeadless(env = process.env) {
|
|
22091
|
+
if (env.AUTOROUTER_ASSUME_HEADLESS === "1")
|
|
22092
|
+
return true;
|
|
22093
|
+
if (process.platform === "darwin" || process.platform === "win32")
|
|
22094
|
+
return false;
|
|
22095
|
+
return !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
22096
|
+
}
|
|
22097
|
+
function authenticateClient(client, metadata, headers, params) {
|
|
22098
|
+
const method = selectClientAuthMethod(client, metadata?.token_endpoint_auth_methods_supported ?? []);
|
|
22099
|
+
const secret = "client_secret" in client ? client.client_secret : undefined;
|
|
22100
|
+
if (method === "client_secret_basic" && secret) {
|
|
22101
|
+
headers.set("Authorization", `Basic ${btoa(`${client.client_id}:${secret}`)}`);
|
|
22102
|
+
return;
|
|
22103
|
+
}
|
|
22104
|
+
params.set("client_id", client.client_id);
|
|
22105
|
+
if (method === "client_secret_post" && secret)
|
|
22106
|
+
params.set("client_secret", secret);
|
|
22107
|
+
}
|
|
22108
|
+
async function errorCode(res) {
|
|
22109
|
+
const text = await res.text().catch(() => "");
|
|
22110
|
+
try {
|
|
22111
|
+
const body = JSON.parse(text);
|
|
22112
|
+
if (body.error)
|
|
22113
|
+
return { code: body.error, description: body.error_description };
|
|
22114
|
+
} catch {}
|
|
22115
|
+
return { code: `http_${res.status}`, description: text.slice(0, 200) || undefined };
|
|
22116
|
+
}
|
|
22117
|
+
async function runDeviceFlow(opts) {
|
|
22118
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
22119
|
+
const doFetch = opts.fetchFn ?? fetch;
|
|
22120
|
+
const info = opts.info ?? await discoverOAuthServerInfo(opts.url, { fetchFn: doFetch });
|
|
22121
|
+
const metadata = info.authorizationServerMetadata;
|
|
22122
|
+
const endpoint = deviceEndpoint(metadata);
|
|
22123
|
+
if (!endpoint) {
|
|
22124
|
+
return {
|
|
22125
|
+
ok: false,
|
|
22126
|
+
message: `${opts.server} does not advertise a device authorization endpoint, so RFC 8628 is not ` + `available for it.
|
|
22127
|
+
` + ` Authorize from a machine with a browser, or use the paste-the-code flow:
|
|
22128
|
+
` + ` autorouter login ${opts.server} --manual`
|
|
22129
|
+
};
|
|
22130
|
+
}
|
|
22131
|
+
const store = new FileTokenStore(opts.server, opts.port ?? CALLBACK_PORT, () => {
|
|
22132
|
+
throw new Error("unreachable: the device grant does not redirect");
|
|
22133
|
+
});
|
|
22134
|
+
const resource = await selectResourceURL(opts.url, store, info.resourceMetadata);
|
|
22135
|
+
let client = (await readAuth(opts.server)).clientInformation;
|
|
22136
|
+
const registeredForDevice = (await readAuth(opts.server)).deviceClient === true;
|
|
22137
|
+
if (!client || !registeredForDevice) {
|
|
22138
|
+
if (!metadata?.registration_endpoint && !client) {
|
|
22139
|
+
return {
|
|
22140
|
+
ok: false,
|
|
22141
|
+
message: `${opts.server} supports the device grant but not dynamic client registration, so it ` + `needs an OAuth app you register yourself.
|
|
22142
|
+
` + ` Then: autorouter login ${opts.server} --device --client-id <id> [--client-secret <secret>]`
|
|
22143
|
+
};
|
|
22144
|
+
}
|
|
22145
|
+
if (metadata?.registration_endpoint) {
|
|
22146
|
+
try {
|
|
22147
|
+
client = await registerClient(info.authorizationServerUrl, {
|
|
22148
|
+
metadata,
|
|
22149
|
+
clientMetadata: store.deviceClientMetadata,
|
|
22150
|
+
scope: opts.scope,
|
|
22151
|
+
fetchFn: doFetch
|
|
22152
|
+
});
|
|
22153
|
+
await writeAuth(opts.server, { clientInformation: client, deviceClient: true });
|
|
22154
|
+
} catch (err) {
|
|
22155
|
+
if (!client) {
|
|
22156
|
+
return { ok: false, message: `${opts.server}: client registration failed — ${message(err)}` };
|
|
22157
|
+
}
|
|
22158
|
+
}
|
|
22159
|
+
}
|
|
22160
|
+
}
|
|
22161
|
+
if (!client)
|
|
22162
|
+
return { ok: false, message: `${opts.server}: no OAuth client available.` };
|
|
22163
|
+
const deviceParams = new URLSearchParams;
|
|
22164
|
+
const deviceHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
22165
|
+
authenticateClient(client, metadata, deviceHeaders, deviceParams);
|
|
22166
|
+
if (opts.scope)
|
|
22167
|
+
deviceParams.set("scope", opts.scope);
|
|
22168
|
+
if (resource)
|
|
22169
|
+
deviceParams.set("resource", resource.href);
|
|
22170
|
+
const deviceRes = await doFetch(endpoint, {
|
|
22171
|
+
method: "POST",
|
|
22172
|
+
headers: deviceHeaders,
|
|
22173
|
+
body: deviceParams
|
|
22174
|
+
});
|
|
22175
|
+
if (!deviceRes.ok) {
|
|
22176
|
+
const { code, description } = await errorCode(deviceRes);
|
|
22177
|
+
return {
|
|
22178
|
+
ok: false,
|
|
22179
|
+
message: `${opts.server}: the device authorization request was refused (${code})` + (description ? ` — ${description}` : "") + (code === "unauthorized_client" ? `
|
|
22180
|
+
The provider may not allow this grant for dynamically registered clients.
|
|
22181
|
+
` + ` Try: autorouter login ${opts.server} --manual` : "")
|
|
22182
|
+
};
|
|
22183
|
+
}
|
|
22184
|
+
const grant = await deviceRes.json();
|
|
22185
|
+
if (!grant.device_code || !grant.user_code || !grant.verification_uri) {
|
|
22186
|
+
return { ok: false, message: `${opts.server}: the device authorization response was incomplete.` };
|
|
22187
|
+
}
|
|
22188
|
+
log(`
|
|
22189
|
+
To authorize ${opts.server}, open this on any device with a browser:
|
|
22190
|
+
|
|
22191
|
+
` + ` ${grant.verification_uri_complete ?? grant.verification_uri}
|
|
22192
|
+
|
|
22193
|
+
` + ` and enter the code: ${grant.user_code}
|
|
22194
|
+
` + (grant.verification_uri_complete ? ` (that link has the code filled in; the plain URL is ${grant.verification_uri})
|
|
22195
|
+
` : "") + `
|
|
22196
|
+
Waiting for you to finish${grant.expires_in ? ` — the code expires in ${Math.round(grant.expires_in / 60)} min` : ""}…`);
|
|
22197
|
+
let intervalMs = opts.pollIntervalMs ?? (grant.interval ?? 5) * 1000;
|
|
22198
|
+
const bumpMs = opts.pollIntervalMs ? 0 : 5000;
|
|
22199
|
+
const deadline = Date.now() + (grant.expires_in ?? 900) * 1000;
|
|
22200
|
+
while (Date.now() < deadline) {
|
|
22201
|
+
await sleep(intervalMs);
|
|
22202
|
+
const params = new URLSearchParams;
|
|
22203
|
+
const headers = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
22204
|
+
authenticateClient(client, metadata, headers, params);
|
|
22205
|
+
params.set("grant_type", DEVICE_GRANT_TYPE);
|
|
22206
|
+
params.set("device_code", grant.device_code);
|
|
22207
|
+
if (resource)
|
|
22208
|
+
params.set("resource", resource.href);
|
|
22209
|
+
const res = await doFetch(metadata.token_endpoint, { method: "POST", headers, body: params });
|
|
22210
|
+
if (res.ok) {
|
|
22211
|
+
const tokens = await res.json();
|
|
22212
|
+
await writeAuth(opts.server, {
|
|
22213
|
+
tokens,
|
|
22214
|
+
...opts.scope ? { requestedScope: opts.scope } : {}
|
|
22215
|
+
});
|
|
22216
|
+
return { ok: true, message: "", grantedScope: tokens.scope ?? opts.scope };
|
|
22217
|
+
}
|
|
22218
|
+
const { code, description } = await errorCode(res);
|
|
22219
|
+
if (code === "authorization_pending")
|
|
22220
|
+
continue;
|
|
22221
|
+
if (code === "slow_down") {
|
|
22222
|
+
intervalMs += bumpMs;
|
|
22223
|
+
continue;
|
|
22224
|
+
}
|
|
22225
|
+
if (code === "access_denied") {
|
|
22226
|
+
return { ok: false, message: `${opts.server}: authorization was denied.` };
|
|
22227
|
+
}
|
|
22228
|
+
if (code === "expired_token") {
|
|
22229
|
+
return { ok: false, message: `${opts.server}: the code expired before it was entered. Run the command again.` };
|
|
22230
|
+
}
|
|
22231
|
+
return {
|
|
22232
|
+
ok: false,
|
|
22233
|
+
message: `${opts.server}: token request failed (${code})${description ? ` — ${description}` : ""}`
|
|
22234
|
+
};
|
|
22235
|
+
}
|
|
22236
|
+
return { ok: false, message: `${opts.server}: timed out waiting for the code to be entered.` };
|
|
22237
|
+
}
|
|
22238
|
+
function sleep(ms) {
|
|
22239
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
22240
|
+
}
|
|
22241
|
+
function message(err) {
|
|
22242
|
+
return err instanceof Error ? err.message : String(err);
|
|
22243
|
+
}
|
|
22244
|
+
|
|
22066
22245
|
// src/config/scopes.ts
|
|
22067
22246
|
var MUTATING = /(^|[:._\-\/])(write|admin|manage|delete|destroy|create|update|modify|readwrite|rw|full|all)([:._\-\/]|$)/i;
|
|
22068
22247
|
function isReadOnlyScope(scope) {
|
|
@@ -22110,6 +22289,9 @@ async function runLogin(opts) {
|
|
|
22110
22289
|
Scopes: ${previousScope.split(/\s+/).join(", ")}` : "")
|
|
22111
22290
|
};
|
|
22112
22291
|
}
|
|
22292
|
+
if (opts.device && opts.manual) {
|
|
22293
|
+
return { ok: false, message: "--device and --manual are different flows; pick one." };
|
|
22294
|
+
}
|
|
22113
22295
|
const port = opts.port ?? CALLBACK_PORT;
|
|
22114
22296
|
if (opts.clientId) {
|
|
22115
22297
|
await setClientInformation(entry.name, {
|
|
@@ -22117,12 +22299,67 @@ async function runLogin(opts) {
|
|
|
22117
22299
|
...opts.clientSecret ? { client_secret: opts.clientSecret } : {}
|
|
22118
22300
|
});
|
|
22119
22301
|
}
|
|
22120
|
-
|
|
22302
|
+
const want = {
|
|
22121
22303
|
scopes: opts.scopes,
|
|
22122
22304
|
readOnly: opts.readOnly,
|
|
22123
22305
|
allScopes: opts.allScopes,
|
|
22124
22306
|
previous: previousScope
|
|
22307
|
+
};
|
|
22308
|
+
const mode = await resolveMode(entry, opts);
|
|
22309
|
+
if (mode.kind === "device") {
|
|
22310
|
+
const chosen = chooseScopes(mode.advertised, want);
|
|
22311
|
+
if ("error" in chosen)
|
|
22312
|
+
return { ok: false, message: `${entry.name}: ${chosen.error}` };
|
|
22313
|
+
if (mode.announce)
|
|
22314
|
+
console.log(mode.announce);
|
|
22315
|
+
const result = await runDeviceFlow({
|
|
22316
|
+
server: entry.name,
|
|
22317
|
+
url: entry.url,
|
|
22318
|
+
scope: chosen.scope,
|
|
22319
|
+
info: mode.info,
|
|
22320
|
+
port
|
|
22321
|
+
});
|
|
22322
|
+
if (!result.ok)
|
|
22323
|
+
return result;
|
|
22324
|
+
return {
|
|
22325
|
+
ok: true,
|
|
22326
|
+
message: `${entry.name}: authorized.` + (result.grantedScope ? `
|
|
22327
|
+
${summarizeScopes(result.grantedScope)}` : "")
|
|
22328
|
+
};
|
|
22329
|
+
}
|
|
22330
|
+
if (mode.announce)
|
|
22331
|
+
console.log(mode.announce);
|
|
22332
|
+
return await authorize(entry, port, want, mode.kind);
|
|
22333
|
+
}
|
|
22334
|
+
async function resolveMode(entry, opts) {
|
|
22335
|
+
if (opts.manual)
|
|
22336
|
+
return { kind: "manual" };
|
|
22337
|
+
if (opts.device) {
|
|
22338
|
+
const info = await discoverOAuthServerInfo(entry.url).catch(() => {
|
|
22339
|
+
return;
|
|
22340
|
+
});
|
|
22341
|
+
return { kind: "device", info, advertised: advertisedFrom(info) };
|
|
22342
|
+
}
|
|
22343
|
+
if (!looksHeadless())
|
|
22344
|
+
return { kind: "browser" };
|
|
22345
|
+
const info = await discoverOAuthServerInfo(entry.url).catch(() => {
|
|
22346
|
+
return;
|
|
22125
22347
|
});
|
|
22348
|
+
if (info && deviceEndpoint(info.authorizationServerMetadata)) {
|
|
22349
|
+
return {
|
|
22350
|
+
kind: "device",
|
|
22351
|
+
info,
|
|
22352
|
+
advertised: advertisedFrom(info),
|
|
22353
|
+
announce: `No display detected, so this is using the device flow instead of opening a browser.
|
|
22354
|
+
` + ` Pass --manual to paste a redirect URL instead, or --port with X11/SSH forwarding to
|
|
22355
|
+
` + ` use the browser flow anyway.`
|
|
22356
|
+
};
|
|
22357
|
+
}
|
|
22358
|
+
return {
|
|
22359
|
+
kind: "manual",
|
|
22360
|
+
announce: `No display detected, and ${entry.name} does not offer the device flow — falling back to
|
|
22361
|
+
` + ` the paste-the-code flow. Open the URL below on any machine with a browser.`
|
|
22362
|
+
};
|
|
22126
22363
|
}
|
|
22127
22364
|
async function describeScopes(entry) {
|
|
22128
22365
|
const advertised = await advertisedScopes(entry.url);
|
|
@@ -22144,6 +22381,9 @@ ${lines.join(`
|
|
|
22144
22381
|
` : "") + ` autorouter login ${entry.name} --scopes "${(ro.length ? ro : advertised).slice(0, 2).join(",")}"`
|
|
22145
22382
|
};
|
|
22146
22383
|
}
|
|
22384
|
+
function advertisedFrom(info) {
|
|
22385
|
+
return requestableScopes(info?.resourceMetadata?.scopes_supported, info?.authorizationServerMetadata?.scopes_supported);
|
|
22386
|
+
}
|
|
22147
22387
|
async function advertisedScopes(url) {
|
|
22148
22388
|
try {
|
|
22149
22389
|
const info = await discoverOAuthServerInfo(url);
|
|
@@ -22152,7 +22392,7 @@ async function advertisedScopes(url) {
|
|
|
22152
22392
|
return [];
|
|
22153
22393
|
}
|
|
22154
22394
|
}
|
|
22155
|
-
async function authorize(entry, port, want) {
|
|
22395
|
+
async function authorize(entry, port, want, mode = "browser") {
|
|
22156
22396
|
let pendingState;
|
|
22157
22397
|
let resolveCode;
|
|
22158
22398
|
let rejectCode;
|
|
@@ -22174,12 +22414,14 @@ async function authorize(entry, port, want) {
|
|
|
22174
22414
|
else
|
|
22175
22415
|
resolveCode(code);
|
|
22176
22416
|
});
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22417
|
+
if (mode === "browser") {
|
|
22418
|
+
await new Promise((res, rej) => {
|
|
22419
|
+
server.once("error", rej);
|
|
22420
|
+
server.listen(port, "127.0.0.1", res);
|
|
22421
|
+
}).catch((err) => {
|
|
22422
|
+
throw err.code === "EADDRINUSE" ? new Error(`Port ${port} is in use. Pass --port to pick another (it must stay the same across logins).`) : err;
|
|
22423
|
+
});
|
|
22424
|
+
}
|
|
22183
22425
|
let advertised = [];
|
|
22184
22426
|
let canRegister = true;
|
|
22185
22427
|
try {
|
|
@@ -22203,6 +22445,18 @@ async function authorize(entry, port, want) {
|
|
|
22203
22445
|
};
|
|
22204
22446
|
}
|
|
22205
22447
|
const provider = new FileTokenStore(entry.name, port, (url) => {
|
|
22448
|
+
if (mode === "manual") {
|
|
22449
|
+
console.log(`
|
|
22450
|
+
Open this on any machine with a browser and authorize ${entry.name}:
|
|
22451
|
+
|
|
22452
|
+
${url}
|
|
22453
|
+
|
|
22454
|
+
` + `The browser will then be redirected to ${`http://localhost:${port}/callback`}, which
|
|
22455
|
+
` + `will not load — that is expected, nothing is listening there. Copy the full URL out of
|
|
22456
|
+
` + `the address bar and paste it below.
|
|
22457
|
+
`);
|
|
22458
|
+
return;
|
|
22459
|
+
}
|
|
22206
22460
|
console.log(`
|
|
22207
22461
|
Opening your browser to authorize ${entry.name}:
|
|
22208
22462
|
${url}
|
|
@@ -22215,7 +22469,7 @@ Opening your browser to authorize ${entry.name}:
|
|
|
22215
22469
|
return { ok: true, message: `${entry.name}: already authorized (existing grant is still valid).` };
|
|
22216
22470
|
}
|
|
22217
22471
|
pendingState = (await readAuth(entry.name)).state;
|
|
22218
|
-
const code = await withTimeout2(codePromise, 5 * 60000, "waiting for the browser callback");
|
|
22472
|
+
const code = mode === "manual" ? await readPastedCode(pendingState) : await withTimeout2(codePromise, 5 * 60000, "waiting for the browser callback");
|
|
22219
22473
|
const result = await auth(provider, { serverUrl: entry.url, authorizationCode: code, scope });
|
|
22220
22474
|
if (result !== "AUTHORIZED") {
|
|
22221
22475
|
return { ok: false, message: `${entry.name}: token exchange did not complete (${result}).` };
|
|
@@ -22246,6 +22500,40 @@ function evaluateCallback(params, expectedState) {
|
|
|
22246
22500
|
failure: description ? `${error ?? "error"}: ${description}` : error ?? "no code returned"
|
|
22247
22501
|
};
|
|
22248
22502
|
}
|
|
22503
|
+
async function readPastedCode(expectedState) {
|
|
22504
|
+
const { createInterface } = await import("node:readline/promises");
|
|
22505
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22506
|
+
try {
|
|
22507
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
22508
|
+
const answer = (await rl.question("Paste the full redirect URL (or just the code): ")).trim();
|
|
22509
|
+
if (!answer)
|
|
22510
|
+
continue;
|
|
22511
|
+
const parsed = parsePastedRedirect(answer, expectedState);
|
|
22512
|
+
if ("code" in parsed)
|
|
22513
|
+
return parsed.code;
|
|
22514
|
+
console.log(` ${parsed.error}`);
|
|
22515
|
+
}
|
|
22516
|
+
throw new Error("no usable authorization code was pasted");
|
|
22517
|
+
} finally {
|
|
22518
|
+
rl.close();
|
|
22519
|
+
}
|
|
22520
|
+
}
|
|
22521
|
+
function parsePastedRedirect(input, expectedState) {
|
|
22522
|
+
const trimmed = input.trim();
|
|
22523
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
22524
|
+
if (/[\s?&]/.test(trimmed))
|
|
22525
|
+
return { error: "that does not look like a URL or a code — try again" };
|
|
22526
|
+
return { code: trimmed };
|
|
22527
|
+
}
|
|
22528
|
+
let params;
|
|
22529
|
+
try {
|
|
22530
|
+
params = new URL(trimmed).searchParams;
|
|
22531
|
+
} catch {
|
|
22532
|
+
return { error: "that URL could not be parsed — paste the whole address bar" };
|
|
22533
|
+
}
|
|
22534
|
+
const { code, failure } = evaluateCallback(params, expectedState);
|
|
22535
|
+
return code ? { code } : { error: failure ?? "no code in that URL" };
|
|
22536
|
+
}
|
|
22249
22537
|
function openBrowser(url) {
|
|
22250
22538
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
22251
22539
|
try {
|
|
@@ -22289,7 +22577,7 @@ function chooseScopes(advertised, want) {
|
|
|
22289
22577
|
if (!ro.length) {
|
|
22290
22578
|
return {
|
|
22291
22579
|
error: `no read-only scopes are offered (available: ${advertised.join(", ")}).
|
|
22292
|
-
|
|
22580
|
+
Authorize with full access, or pick explicitly with --scopes.`
|
|
22293
22581
|
};
|
|
22294
22582
|
}
|
|
22295
22583
|
return { scope: ro.join(" ") };
|
|
@@ -22306,11 +22594,227 @@ function summarizeScopes(scope, limit = 6) {
|
|
|
22306
22594
|
if (all.length <= limit) {
|
|
22307
22595
|
return `scope: ${all.join(", ")}${writes.length ? "" : " (read-only)"}`;
|
|
22308
22596
|
}
|
|
22309
|
-
return `scope: ${all.length} granted, ${writes.length} of them write
|
|
22597
|
+
return `scope: ${all.length} granted, ${writes.length} of them write (${writes.slice(0, 3).join(", ") || "none"}${writes.length > 3 ? ", …" : ""})`;
|
|
22598
|
+
}
|
|
22599
|
+
|
|
22600
|
+
// src/cli/update.ts
|
|
22601
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
22602
|
+
import { fileURLToPath } from "node:url";
|
|
22603
|
+
import { dirname as dirname3, join as join18, resolve as resolve2, sep } from "node:path";
|
|
22604
|
+
var PACKAGE_NAME = "autorouter-mcp";
|
|
22605
|
+
function entryPath() {
|
|
22606
|
+
let raw;
|
|
22607
|
+
try {
|
|
22608
|
+
raw = fileURLToPath(import.meta.url);
|
|
22609
|
+
} catch {
|
|
22610
|
+
raw = process.argv[1] ?? "";
|
|
22611
|
+
}
|
|
22612
|
+
try {
|
|
22613
|
+
return realpathSync(raw);
|
|
22614
|
+
} catch {
|
|
22615
|
+
return raw;
|
|
22616
|
+
}
|
|
22617
|
+
}
|
|
22618
|
+
function packageRoot(from, exists) {
|
|
22619
|
+
let dir = dirname3(from);
|
|
22620
|
+
for (let i = 0;i < 12; i++) {
|
|
22621
|
+
if (exists(join18(dir, "package.json")))
|
|
22622
|
+
return dir;
|
|
22623
|
+
const parent = dirname3(dir);
|
|
22624
|
+
if (parent === dir)
|
|
22625
|
+
break;
|
|
22626
|
+
dir = parent;
|
|
22627
|
+
}
|
|
22628
|
+
return;
|
|
22629
|
+
}
|
|
22630
|
+
function managerFromLockfile(dir, exists) {
|
|
22631
|
+
if (exists(join18(dir, "bun.lock")) || exists(join18(dir, "bun.lockb")))
|
|
22632
|
+
return "bun";
|
|
22633
|
+
if (exists(join18(dir, "pnpm-lock.yaml")))
|
|
22634
|
+
return "pnpm";
|
|
22635
|
+
if (exists(join18(dir, "yarn.lock")))
|
|
22636
|
+
return "yarn";
|
|
22637
|
+
return "npm";
|
|
22638
|
+
}
|
|
22639
|
+
function detectInstall(entry, exists = existsSync) {
|
|
22640
|
+
const root = packageRoot(entry, exists);
|
|
22641
|
+
if (!root)
|
|
22642
|
+
return { kind: "unknown", dir: dirname3(entry) };
|
|
22643
|
+
const path = root.split(sep).join("/");
|
|
22644
|
+
if (exists(join18(root, ".git")))
|
|
22645
|
+
return { kind: "source", dir: root };
|
|
22646
|
+
const transient = path.includes("/_npx/") && "npx" || /\/dlx(-|\/)/.test(path) && "pnpm dlx" || path.includes("/.bun/install/cache/") && "bunx" || null;
|
|
22647
|
+
if (transient)
|
|
22648
|
+
return { kind: "transient", runner: transient, dir: root };
|
|
22649
|
+
const nodeModules = dirname3(root);
|
|
22650
|
+
const parent = dirname3(nodeModules);
|
|
22651
|
+
const globalManager = path.includes("/.bun/install/global/") ? "bun" : /\/pnpm\/global\/|\/pnpm\/[0-9]+\/node_modules\//.test(path) ? "pnpm" : /\/\.config\/yarn\/global\/|\/\.yarn\/global\//.test(path) ? "yarn" : null;
|
|
22652
|
+
if (globalManager)
|
|
22653
|
+
return { kind: "global", manager: globalManager, dir: root };
|
|
22654
|
+
if (nodeModules.split(sep).pop() === "node_modules") {
|
|
22655
|
+
if (exists(join18(parent, "package.json"))) {
|
|
22656
|
+
return {
|
|
22657
|
+
kind: "project",
|
|
22658
|
+
manager: managerFromLockfile(parent, exists),
|
|
22659
|
+
dir: root,
|
|
22660
|
+
projectDir: parent
|
|
22661
|
+
};
|
|
22662
|
+
}
|
|
22663
|
+
return { kind: "global", manager: "npm", dir: root };
|
|
22664
|
+
}
|
|
22665
|
+
return { kind: "unknown", dir: root };
|
|
22666
|
+
}
|
|
22667
|
+
function updateCommand(install, pkg, version = "latest") {
|
|
22668
|
+
const spec = `${pkg}@${version}`;
|
|
22669
|
+
if (install.kind === "global") {
|
|
22670
|
+
switch (install.manager) {
|
|
22671
|
+
case "bun":
|
|
22672
|
+
return ["bun", "add", "-g", spec];
|
|
22673
|
+
case "pnpm":
|
|
22674
|
+
return ["pnpm", "add", "-g", spec];
|
|
22675
|
+
case "yarn":
|
|
22676
|
+
return ["yarn", "global", "add", spec];
|
|
22677
|
+
default:
|
|
22678
|
+
return ["npm", "install", "-g", spec];
|
|
22679
|
+
}
|
|
22680
|
+
}
|
|
22681
|
+
if (install.kind === "project") {
|
|
22682
|
+
switch (install.manager) {
|
|
22683
|
+
case "bun":
|
|
22684
|
+
return ["bun", "add", spec];
|
|
22685
|
+
case "pnpm":
|
|
22686
|
+
return ["pnpm", "add", spec];
|
|
22687
|
+
case "yarn":
|
|
22688
|
+
return ["yarn", "add", spec];
|
|
22689
|
+
default:
|
|
22690
|
+
return ["npm", "install", spec];
|
|
22691
|
+
}
|
|
22692
|
+
}
|
|
22693
|
+
return null;
|
|
22694
|
+
}
|
|
22695
|
+
function compareVersions2(a, b) {
|
|
22696
|
+
const split = (v) => {
|
|
22697
|
+
const [core = "", pre] = v.replace(/^v/, "").split("-", 2);
|
|
22698
|
+
const parts = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
22699
|
+
return { parts, pre };
|
|
22700
|
+
};
|
|
22701
|
+
const x = split(a);
|
|
22702
|
+
const y = split(b);
|
|
22703
|
+
for (let i = 0;i < 3; i++) {
|
|
22704
|
+
const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
|
|
22705
|
+
if (d !== 0)
|
|
22706
|
+
return d < 0 ? -1 : 1;
|
|
22707
|
+
}
|
|
22708
|
+
if (x.pre && !y.pre)
|
|
22709
|
+
return -1;
|
|
22710
|
+
if (!x.pre && y.pre)
|
|
22711
|
+
return 1;
|
|
22712
|
+
if (x.pre && y.pre && x.pre !== y.pre)
|
|
22713
|
+
return x.pre < y.pre ? -1 : 1;
|
|
22714
|
+
return 0;
|
|
22715
|
+
}
|
|
22716
|
+
async function latestVersion(pkg, fetchFn = fetch) {
|
|
22717
|
+
const base = (process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
22718
|
+
try {
|
|
22719
|
+
const res = await fetchFn(`${base}/${encodeURIComponent(pkg)}/latest`, {
|
|
22720
|
+
headers: { accept: "application/json" },
|
|
22721
|
+
signal: AbortSignal.timeout(1e4)
|
|
22722
|
+
});
|
|
22723
|
+
if (!res.ok)
|
|
22724
|
+
return { error: `registry returned ${res.status}` };
|
|
22725
|
+
const body = await res.json();
|
|
22726
|
+
return body.version ? { version: body.version } : { error: "registry returned no version" };
|
|
22727
|
+
} catch (err) {
|
|
22728
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
22729
|
+
}
|
|
22730
|
+
}
|
|
22731
|
+
function cannotUpdate(install, pkg) {
|
|
22732
|
+
if (install.kind === "transient") {
|
|
22733
|
+
return `This copy was unpacked by ${install.runner}, which fetches the package fresh on every run —
|
|
22734
|
+
` + ` there is no install here to upgrade. You are already getting the latest each time.
|
|
22735
|
+
` + ` To keep a copy that does not re-download: npm install -g ${pkg}`;
|
|
22736
|
+
}
|
|
22737
|
+
if (install.kind === "source") {
|
|
22738
|
+
return `This is a checkout running from source (${install.dir}), not a package install.
|
|
22739
|
+
` + ` Update it with: git -C ${install.dir} pull && bun install && bun run build`;
|
|
22740
|
+
}
|
|
22741
|
+
return `Could not tell which package manager installed this copy (${install.dir}).
|
|
22742
|
+
` + ` Upgrade it the way you installed it, e.g. npm install -g ${pkg}@latest`;
|
|
22743
|
+
}
|
|
22744
|
+
function describe2(install) {
|
|
22745
|
+
switch (install.kind) {
|
|
22746
|
+
case "global":
|
|
22747
|
+
return `${install.manager} global install at ${install.dir}`;
|
|
22748
|
+
case "project":
|
|
22749
|
+
return `${install.manager} dependency of ${install.projectDir}`;
|
|
22750
|
+
case "transient":
|
|
22751
|
+
return `${install.runner} temporary copy at ${install.dir}`;
|
|
22752
|
+
case "source":
|
|
22753
|
+
return `source checkout at ${install.dir}`;
|
|
22754
|
+
default:
|
|
22755
|
+
return `unrecognized install at ${install.dir}`;
|
|
22756
|
+
}
|
|
22757
|
+
}
|
|
22758
|
+
async function runUpdate(opts) {
|
|
22759
|
+
const entry = opts.entry ?? entryPath();
|
|
22760
|
+
const install = detectInstall(entry, opts.exists);
|
|
22761
|
+
const manifest = install.kind === "unknown" ? null : await readJson(join18(install.dir, "package.json"));
|
|
22762
|
+
const pkg = manifest?.name ?? PACKAGE_NAME;
|
|
22763
|
+
const latest = await latestVersion(pkg, opts.fetchFn);
|
|
22764
|
+
if ("error" in latest) {
|
|
22765
|
+
return { ok: false, message: `Could not reach the registry to check for updates: ${latest.error}` };
|
|
22766
|
+
}
|
|
22767
|
+
const behind = compareVersions2(opts.current, latest.version) < 0;
|
|
22768
|
+
const status = behind ? `autorouter ${opts.current} → ${latest.version} available` : `autorouter ${opts.current} is up to date (latest is ${latest.version})`;
|
|
22769
|
+
const command = updateCommand(install, pkg, latest.version);
|
|
22770
|
+
if (!command) {
|
|
22771
|
+
return { ok: !behind, message: `${status}
|
|
22772
|
+
|
|
22773
|
+
${cannotUpdate(install, pkg)}` };
|
|
22774
|
+
}
|
|
22775
|
+
const printable = command.join(" ");
|
|
22776
|
+
if (opts.check) {
|
|
22777
|
+
return { ok: true, message: `${status}
|
|
22778
|
+
${describe2(install)}
|
|
22779
|
+
Update with: ${printable}` };
|
|
22780
|
+
}
|
|
22781
|
+
if (!behind && !opts.force) {
|
|
22782
|
+
return { ok: true, message: `${status}
|
|
22783
|
+
${describe2(install)}
|
|
22784
|
+
Re-install anyway with: --force` };
|
|
22785
|
+
}
|
|
22786
|
+
if (opts.dryRun) {
|
|
22787
|
+
return { ok: true, message: `${status}
|
|
22788
|
+
${describe2(install)}
|
|
22789
|
+
Would run: ${printable}` };
|
|
22790
|
+
}
|
|
22791
|
+
console.log(`${status}
|
|
22792
|
+
${describe2(install)}
|
|
22793
|
+
Running: ${printable}
|
|
22794
|
+
`);
|
|
22795
|
+
const code = await runStreaming(command[0], command.slice(1), {
|
|
22796
|
+
cwd: install.kind === "project" ? install.projectDir : undefined
|
|
22797
|
+
}).catch((err) => err);
|
|
22798
|
+
if (code instanceof Error) {
|
|
22799
|
+
return {
|
|
22800
|
+
ok: false,
|
|
22801
|
+
message: `Could not run ${command[0]}: ${code.message}
|
|
22802
|
+
` + ` Run it yourself: ${printable}`
|
|
22803
|
+
};
|
|
22804
|
+
}
|
|
22805
|
+
if (code !== 0) {
|
|
22806
|
+
return { ok: false, message: `${command[0]} exited with code ${code}. Nothing was changed by autorouter.` };
|
|
22807
|
+
}
|
|
22808
|
+
return {
|
|
22809
|
+
ok: true,
|
|
22810
|
+
message: `
|
|
22811
|
+
Updated to ${latest.version}. Restart any harness with the router running to pick it up` + `
|
|
22812
|
+
(it is a long-lived stdio server, so an open session keeps the old build).`
|
|
22813
|
+
};
|
|
22310
22814
|
}
|
|
22311
22815
|
|
|
22312
22816
|
// src/cli.ts
|
|
22313
|
-
var VERSION2 = "0.2.
|
|
22817
|
+
var VERSION2 = "0.2.6";
|
|
22314
22818
|
var USAGE = `autorouter — one search tool instead of every tool
|
|
22315
22819
|
|
|
22316
22820
|
autorouter serve Run as an MCP server over stdio (default)
|
|
@@ -22320,7 +22824,11 @@ var USAGE = `autorouter — one search tool instead of every tool
|
|
|
22320
22824
|
autorouter list [--kind K] List everything in the catalog
|
|
22321
22825
|
autorouter reindex Rebuild the catalog now
|
|
22322
22826
|
autorouter doctor Show what is reachable and what it saves
|
|
22323
|
-
autorouter
|
|
22827
|
+
autorouter update Upgrade via the package manager that
|
|
22828
|
+
installed this copy. --check to look
|
|
22829
|
+
without installing.
|
|
22830
|
+
autorouter login [server] Authorize an OAuth server (opens a browser,
|
|
22831
|
+
or prints a code on a headless box);
|
|
22324
22832
|
with no argument, lists what needs one
|
|
22325
22833
|
autorouter logout <server> Forget a stored grant
|
|
22326
22834
|
autorouter add <name> --url URL Register a server with the router directly
|
|
@@ -22346,6 +22854,8 @@ Options
|
|
|
22346
22854
|
--json machine-readable output
|
|
22347
22855
|
--yes init/adopt: do not prompt
|
|
22348
22856
|
--dry-run adopt: show what would move, change nothing
|
|
22857
|
+
update: print the upgrade command without running it
|
|
22858
|
+
--check update: report the available version, install nothing
|
|
22349
22859
|
--force adopt: proceed even if a server is unreachable
|
|
22350
22860
|
--keep S adopt: leave server S registered in the harness (comma-separated)
|
|
22351
22861
|
--keep-skill S adopt: leave skill S loaded (comma-separated)
|
|
@@ -22360,6 +22870,10 @@ Options
|
|
|
22360
22870
|
any narrowing the previous grant carried
|
|
22361
22871
|
--scopes S login: request exactly these scopes (comma or space separated)
|
|
22362
22872
|
--list-scopes login: show what the server offers, authorize nothing
|
|
22873
|
+
--device login: RFC 8628 — print a code to enter on another device and
|
|
22874
|
+
poll for the result. No browser or open port needed here.
|
|
22875
|
+
--manual login: print the authorization URL, then read the redirect you
|
|
22876
|
+
paste back. Works where the provider has no device endpoint.
|
|
22363
22877
|
|
|
22364
22878
|
\`add\` registers behind the router, so a new server never enters your context.
|
|
22365
22879
|
Servers added to a harness the normal way (\`claude mcp add\`) are moved behind
|
|
@@ -22397,6 +22911,17 @@ async function main(argv) {
|
|
|
22397
22911
|
case "doctor":
|
|
22398
22912
|
console.log(await runDoctor(process.cwd()));
|
|
22399
22913
|
return 0;
|
|
22914
|
+
case "update":
|
|
22915
|
+
case "upgrade": {
|
|
22916
|
+
const result = await runUpdate({
|
|
22917
|
+
current: VERSION2,
|
|
22918
|
+
check: Boolean(flags.check),
|
|
22919
|
+
dryRun: Boolean(flags["dry-run"]),
|
|
22920
|
+
force: Boolean(flags.force)
|
|
22921
|
+
});
|
|
22922
|
+
console.log(result.message);
|
|
22923
|
+
return result.ok ? 0 : 1;
|
|
22924
|
+
}
|
|
22400
22925
|
case "init":
|
|
22401
22926
|
return await cmdInit(flags);
|
|
22402
22927
|
case "login":
|
|
@@ -22481,6 +23006,8 @@ async function cmdLogin(server, flags) {
|
|
|
22481
23006
|
};
|
|
22482
23007
|
}));
|
|
22483
23008
|
console.log(`Usage: autorouter login <server> [--read-only | --all-scopes | --scopes a,b]
|
|
23009
|
+
`);
|
|
23010
|
+
console.log(` On a machine with no browser, add --device (or --manual).
|
|
22484
23011
|
`);
|
|
22485
23012
|
for (const s of states) {
|
|
22486
23013
|
const scope = s.ok && s.scope ? ` ${summarizeScopes(s.scope)}` : "";
|
|
@@ -22506,7 +23033,9 @@ ${pending.length} need a grant; each is a separate authorization:`);
|
|
|
22506
23033
|
scopes: flags.scopes,
|
|
22507
23034
|
readOnly: Boolean(flags["read-only"]),
|
|
22508
23035
|
allScopes: Boolean(flags["all-scopes"]),
|
|
22509
|
-
listScopes: Boolean(flags["list-scopes"])
|
|
23036
|
+
listScopes: Boolean(flags["list-scopes"]),
|
|
23037
|
+
device: Boolean(flags.device),
|
|
23038
|
+
manual: Boolean(flags.manual)
|
|
22510
23039
|
});
|
|
22511
23040
|
console.log(result.message);
|
|
22512
23041
|
if (result.ok && !flags["list-scopes"]) {
|
|
@@ -22797,7 +23326,7 @@ function parseArgs(argv) {
|
|
|
22797
23326
|
command = `--${name}`;
|
|
22798
23327
|
continue;
|
|
22799
23328
|
}
|
|
22800
|
-
const boolean = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "all-scopes", "list-scopes"].includes(name) && !(name === "json" && command === "add");
|
|
23329
|
+
const boolean = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "all-scopes", "list-scopes", "device", "manual", "check"].includes(name) && !(name === "json" && command === "add");
|
|
22801
23330
|
if (boolean) {
|
|
22802
23331
|
flags[name] = true;
|
|
22803
23332
|
} else if (inline !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autorouter-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "One search tool instead of every tool: an MCP capability router for Claude Code, Codex, Cursor and anything else that speaks MCP.",
|
|
5
5
|
"mcpName": "io.github.Webb-Ventures/autorouter",
|
|
6
6
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.Webb-Ventures/autorouter",
|
|
4
4
|
"title": "autorouter",
|
|
5
5
|
"description": "An MCP capability router: one search tool instead of every server's tool schema.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.6",
|
|
7
7
|
"websiteUrl": "https://github.com/Webb-Ventures/autorouter",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/Webb-Ventures/autorouter",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "autorouter-mcp",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.6",
|
|
18
18
|
"transport": { "type": "stdio" },
|
|
19
19
|
"packageArguments": [{ "type": "positional", "value": "serve" }],
|
|
20
20
|
"environmentVariables": [
|