autorouter-mcp 0.2.4 → 0.2.5

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 CHANGED
@@ -366,6 +366,7 @@ autorouter restore --target claude # undo the most recent adopt
366
366
 
367
367
  autorouter login # which servers need a grant
368
368
  autorouter login remote-server # authorize one (opens a browser)
369
+ autorouter login remote-server --device # headless: enter a code elsewhere
369
370
  autorouter logout remote-server # forget a stored grant
370
371
  ```
371
372
 
@@ -391,6 +392,35 @@ fixed port (33418, `--port` or `$AUTOROUTER_OAUTH_PORT` to change it) because th
391
392
  redirect URI is baked into the registration a provider stores — a grant obtained
392
393
  on one port cannot be refreshed from another.
393
394
 
395
+ ### Headless machines
396
+
397
+ The browser flow cannot work over SSH: it binds a loopback listener and expects
398
+ a browser on the same host to redirect into it. Two flows replace it, and on a
399
+ box with no display autorouter picks one of them by itself rather than timing
400
+ out waiting for a browser that was never going to open.
401
+
402
+ ```sh
403
+ autorouter login remote-server --device # RFC 8628: enter a code on your phone
404
+ autorouter login remote-server --manual # paste the redirect URL back
405
+ ```
406
+
407
+ `--device` is the better one where the provider offers it. Nothing has to reach
408
+ back into the machine: it prints a short code and a URL, you enter them in a
409
+ browser on any other device, and the headless side polls until you are done.
410
+ Detection is the `device_authorization_endpoint` in the provider's metadata.
411
+
412
+ `--manual` is the fallback for providers that do not implement RFC 8628. It
413
+ prints the authorization URL, you open it elsewhere, and the browser is then
414
+ redirected to `http://localhost:33418/callback`, which will not load — nothing
415
+ is listening. That is expected: copy the URL out of the address bar and paste it
416
+ back. Pasting the whole URL is worth preferring over just the code, because the
417
+ `state` in it is what proves the code came from the login you started.
418
+
419
+ Neither flow binds a port. A device login registers a client that also works for
420
+ a later browser login from the same machine, so nothing has to be redone if the
421
+ box grows a display. Set `AUTOROUTER_ASSUME_HEADLESS=1` to force the detection
422
+ on a machine where it guesses wrong.
423
+
394
424
  ### Choosing permissions
395
425
 
396
426
  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 });
@@ -22063,6 +22070,166 @@ function truncate2(s, max) {
22063
22070
  import { createServer } from "node:http";
22064
22071
  import { spawn as spawn3 } from "node:child_process";
22065
22072
 
22073
+ // src/cli/device.ts
22074
+ function deviceEndpoint(metadata) {
22075
+ const endpoint = metadata?.device_authorization_endpoint;
22076
+ return typeof endpoint === "string" && endpoint ? endpoint : undefined;
22077
+ }
22078
+ function looksHeadless(env = process.env) {
22079
+ if (env.AUTOROUTER_ASSUME_HEADLESS === "1")
22080
+ return true;
22081
+ if (process.platform === "darwin" || process.platform === "win32")
22082
+ return false;
22083
+ return !env.DISPLAY && !env.WAYLAND_DISPLAY;
22084
+ }
22085
+ function authenticateClient(client, metadata, headers, params) {
22086
+ const method = selectClientAuthMethod(client, metadata?.token_endpoint_auth_methods_supported ?? []);
22087
+ const secret = "client_secret" in client ? client.client_secret : undefined;
22088
+ if (method === "client_secret_basic" && secret) {
22089
+ headers.set("Authorization", `Basic ${btoa(`${client.client_id}:${secret}`)}`);
22090
+ return;
22091
+ }
22092
+ params.set("client_id", client.client_id);
22093
+ if (method === "client_secret_post" && secret)
22094
+ params.set("client_secret", secret);
22095
+ }
22096
+ async function errorCode(res) {
22097
+ const text = await res.text().catch(() => "");
22098
+ try {
22099
+ const body = JSON.parse(text);
22100
+ if (body.error)
22101
+ return { code: body.error, description: body.error_description };
22102
+ } catch {}
22103
+ return { code: `http_${res.status}`, description: text.slice(0, 200) || undefined };
22104
+ }
22105
+ async function runDeviceFlow(opts) {
22106
+ const log = opts.log ?? ((line) => console.log(line));
22107
+ const doFetch = opts.fetchFn ?? fetch;
22108
+ const info = opts.info ?? await discoverOAuthServerInfo(opts.url, { fetchFn: doFetch });
22109
+ const metadata = info.authorizationServerMetadata;
22110
+ const endpoint = deviceEndpoint(metadata);
22111
+ if (!endpoint) {
22112
+ return {
22113
+ ok: false,
22114
+ message: `${opts.server} does not advertise a device authorization endpoint, so RFC 8628 is not ` + `available for it.
22115
+ ` + ` Authorize from a machine with a browser, or use the paste-the-code flow:
22116
+ ` + ` autorouter login ${opts.server} --manual`
22117
+ };
22118
+ }
22119
+ const store = new FileTokenStore(opts.server, opts.port ?? CALLBACK_PORT, () => {
22120
+ throw new Error("unreachable: the device grant does not redirect");
22121
+ });
22122
+ const resource = await selectResourceURL(opts.url, store, info.resourceMetadata);
22123
+ let client = (await readAuth(opts.server)).clientInformation;
22124
+ const registeredForDevice = (await readAuth(opts.server)).deviceClient === true;
22125
+ if (!client || !registeredForDevice) {
22126
+ if (!metadata?.registration_endpoint && !client) {
22127
+ return {
22128
+ ok: false,
22129
+ message: `${opts.server} supports the device grant but not dynamic client registration, so it ` + `needs an OAuth app you register yourself.
22130
+ ` + ` Then: autorouter login ${opts.server} --device --client-id <id> [--client-secret <secret>]`
22131
+ };
22132
+ }
22133
+ if (metadata?.registration_endpoint) {
22134
+ try {
22135
+ client = await registerClient(info.authorizationServerUrl, {
22136
+ metadata,
22137
+ clientMetadata: store.deviceClientMetadata,
22138
+ scope: opts.scope,
22139
+ fetchFn: doFetch
22140
+ });
22141
+ await writeAuth(opts.server, { clientInformation: client, deviceClient: true });
22142
+ } catch (err) {
22143
+ if (!client) {
22144
+ return { ok: false, message: `${opts.server}: client registration failed — ${message(err)}` };
22145
+ }
22146
+ }
22147
+ }
22148
+ }
22149
+ if (!client)
22150
+ return { ok: false, message: `${opts.server}: no OAuth client available.` };
22151
+ const deviceParams = new URLSearchParams;
22152
+ const deviceHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
22153
+ authenticateClient(client, metadata, deviceHeaders, deviceParams);
22154
+ if (opts.scope)
22155
+ deviceParams.set("scope", opts.scope);
22156
+ if (resource)
22157
+ deviceParams.set("resource", resource.href);
22158
+ const deviceRes = await doFetch(endpoint, {
22159
+ method: "POST",
22160
+ headers: deviceHeaders,
22161
+ body: deviceParams
22162
+ });
22163
+ if (!deviceRes.ok) {
22164
+ const { code, description } = await errorCode(deviceRes);
22165
+ return {
22166
+ ok: false,
22167
+ message: `${opts.server}: the device authorization request was refused (${code})` + (description ? ` — ${description}` : "") + (code === "unauthorized_client" ? `
22168
+ The provider may not allow this grant for dynamically registered clients.
22169
+ ` + ` Try: autorouter login ${opts.server} --manual` : "")
22170
+ };
22171
+ }
22172
+ const grant = await deviceRes.json();
22173
+ if (!grant.device_code || !grant.user_code || !grant.verification_uri) {
22174
+ return { ok: false, message: `${opts.server}: the device authorization response was incomplete.` };
22175
+ }
22176
+ log(`
22177
+ To authorize ${opts.server}, open this on any device with a browser:
22178
+
22179
+ ` + ` ${grant.verification_uri_complete ?? grant.verification_uri}
22180
+
22181
+ ` + ` and enter the code: ${grant.user_code}
22182
+ ` + (grant.verification_uri_complete ? ` (that link has the code filled in; the plain URL is ${grant.verification_uri})
22183
+ ` : "") + `
22184
+ Waiting for you to finish${grant.expires_in ? ` — the code expires in ${Math.round(grant.expires_in / 60)} min` : ""}…`);
22185
+ let intervalMs = opts.pollIntervalMs ?? (grant.interval ?? 5) * 1000;
22186
+ const bumpMs = opts.pollIntervalMs ? 0 : 5000;
22187
+ const deadline = Date.now() + (grant.expires_in ?? 900) * 1000;
22188
+ while (Date.now() < deadline) {
22189
+ await sleep(intervalMs);
22190
+ const params = new URLSearchParams;
22191
+ const headers = new Headers({ "content-type": "application/x-www-form-urlencoded" });
22192
+ authenticateClient(client, metadata, headers, params);
22193
+ params.set("grant_type", DEVICE_GRANT_TYPE);
22194
+ params.set("device_code", grant.device_code);
22195
+ if (resource)
22196
+ params.set("resource", resource.href);
22197
+ const res = await doFetch(metadata.token_endpoint, { method: "POST", headers, body: params });
22198
+ if (res.ok) {
22199
+ const tokens = await res.json();
22200
+ await writeAuth(opts.server, {
22201
+ tokens,
22202
+ ...opts.scope ? { requestedScope: opts.scope } : {}
22203
+ });
22204
+ return { ok: true, message: "", grantedScope: tokens.scope ?? opts.scope };
22205
+ }
22206
+ const { code, description } = await errorCode(res);
22207
+ if (code === "authorization_pending")
22208
+ continue;
22209
+ if (code === "slow_down") {
22210
+ intervalMs += bumpMs;
22211
+ continue;
22212
+ }
22213
+ if (code === "access_denied") {
22214
+ return { ok: false, message: `${opts.server}: authorization was denied.` };
22215
+ }
22216
+ if (code === "expired_token") {
22217
+ return { ok: false, message: `${opts.server}: the code expired before it was entered. Run the command again.` };
22218
+ }
22219
+ return {
22220
+ ok: false,
22221
+ message: `${opts.server}: token request failed (${code})${description ? ` — ${description}` : ""}`
22222
+ };
22223
+ }
22224
+ return { ok: false, message: `${opts.server}: timed out waiting for the code to be entered.` };
22225
+ }
22226
+ function sleep(ms) {
22227
+ return new Promise((res) => setTimeout(res, ms));
22228
+ }
22229
+ function message(err) {
22230
+ return err instanceof Error ? err.message : String(err);
22231
+ }
22232
+
22066
22233
  // src/config/scopes.ts
22067
22234
  var MUTATING = /(^|[:._\-\/])(write|admin|manage|delete|destroy|create|update|modify|readwrite|rw|full|all)([:._\-\/]|$)/i;
22068
22235
  function isReadOnlyScope(scope) {
@@ -22110,6 +22277,9 @@ async function runLogin(opts) {
22110
22277
  Scopes: ${previousScope.split(/\s+/).join(", ")}` : "")
22111
22278
  };
22112
22279
  }
22280
+ if (opts.device && opts.manual) {
22281
+ return { ok: false, message: "--device and --manual are different flows; pick one." };
22282
+ }
22113
22283
  const port = opts.port ?? CALLBACK_PORT;
22114
22284
  if (opts.clientId) {
22115
22285
  await setClientInformation(entry.name, {
@@ -22117,12 +22287,67 @@ async function runLogin(opts) {
22117
22287
  ...opts.clientSecret ? { client_secret: opts.clientSecret } : {}
22118
22288
  });
22119
22289
  }
22120
- return await authorize(entry, port, {
22290
+ const want = {
22121
22291
  scopes: opts.scopes,
22122
22292
  readOnly: opts.readOnly,
22123
22293
  allScopes: opts.allScopes,
22124
22294
  previous: previousScope
22295
+ };
22296
+ const mode = await resolveMode(entry, opts);
22297
+ if (mode.kind === "device") {
22298
+ const chosen = chooseScopes(mode.advertised, want);
22299
+ if ("error" in chosen)
22300
+ return { ok: false, message: `${entry.name}: ${chosen.error}` };
22301
+ if (mode.announce)
22302
+ console.log(mode.announce);
22303
+ const result = await runDeviceFlow({
22304
+ server: entry.name,
22305
+ url: entry.url,
22306
+ scope: chosen.scope,
22307
+ info: mode.info,
22308
+ port
22309
+ });
22310
+ if (!result.ok)
22311
+ return result;
22312
+ return {
22313
+ ok: true,
22314
+ message: `${entry.name}: authorized.` + (result.grantedScope ? `
22315
+ ${summarizeScopes(result.grantedScope)}` : "")
22316
+ };
22317
+ }
22318
+ if (mode.announce)
22319
+ console.log(mode.announce);
22320
+ return await authorize(entry, port, want, mode.kind);
22321
+ }
22322
+ async function resolveMode(entry, opts) {
22323
+ if (opts.manual)
22324
+ return { kind: "manual" };
22325
+ if (opts.device) {
22326
+ const info = await discoverOAuthServerInfo(entry.url).catch(() => {
22327
+ return;
22328
+ });
22329
+ return { kind: "device", info, advertised: advertisedFrom(info) };
22330
+ }
22331
+ if (!looksHeadless())
22332
+ return { kind: "browser" };
22333
+ const info = await discoverOAuthServerInfo(entry.url).catch(() => {
22334
+ return;
22125
22335
  });
22336
+ if (info && deviceEndpoint(info.authorizationServerMetadata)) {
22337
+ return {
22338
+ kind: "device",
22339
+ info,
22340
+ advertised: advertisedFrom(info),
22341
+ announce: `No display detected, so this is using the device flow instead of opening a browser.
22342
+ ` + ` Pass --manual to paste a redirect URL instead, or --port with X11/SSH forwarding to
22343
+ ` + ` use the browser flow anyway.`
22344
+ };
22345
+ }
22346
+ return {
22347
+ kind: "manual",
22348
+ announce: `No display detected, and ${entry.name} does not offer the device flow — falling back to
22349
+ ` + ` the paste-the-code flow. Open the URL below on any machine with a browser.`
22350
+ };
22126
22351
  }
22127
22352
  async function describeScopes(entry) {
22128
22353
  const advertised = await advertisedScopes(entry.url);
@@ -22144,6 +22369,9 @@ ${lines.join(`
22144
22369
  ` : "") + ` autorouter login ${entry.name} --scopes "${(ro.length ? ro : advertised).slice(0, 2).join(",")}"`
22145
22370
  };
22146
22371
  }
22372
+ function advertisedFrom(info) {
22373
+ return requestableScopes(info?.resourceMetadata?.scopes_supported, info?.authorizationServerMetadata?.scopes_supported);
22374
+ }
22147
22375
  async function advertisedScopes(url) {
22148
22376
  try {
22149
22377
  const info = await discoverOAuthServerInfo(url);
@@ -22152,7 +22380,7 @@ async function advertisedScopes(url) {
22152
22380
  return [];
22153
22381
  }
22154
22382
  }
22155
- async function authorize(entry, port, want) {
22383
+ async function authorize(entry, port, want, mode = "browser") {
22156
22384
  let pendingState;
22157
22385
  let resolveCode;
22158
22386
  let rejectCode;
@@ -22174,12 +22402,14 @@ async function authorize(entry, port, want) {
22174
22402
  else
22175
22403
  resolveCode(code);
22176
22404
  });
22177
- await new Promise((res, rej) => {
22178
- server.once("error", rej);
22179
- server.listen(port, "127.0.0.1", res);
22180
- }).catch((err) => {
22181
- throw err.code === "EADDRINUSE" ? new Error(`Port ${port} is in use. Pass --port to pick another (it must stay the same across logins).`) : err;
22182
- });
22405
+ if (mode === "browser") {
22406
+ await new Promise((res, rej) => {
22407
+ server.once("error", rej);
22408
+ server.listen(port, "127.0.0.1", res);
22409
+ }).catch((err) => {
22410
+ throw err.code === "EADDRINUSE" ? new Error(`Port ${port} is in use. Pass --port to pick another (it must stay the same across logins).`) : err;
22411
+ });
22412
+ }
22183
22413
  let advertised = [];
22184
22414
  let canRegister = true;
22185
22415
  try {
@@ -22203,6 +22433,18 @@ async function authorize(entry, port, want) {
22203
22433
  };
22204
22434
  }
22205
22435
  const provider = new FileTokenStore(entry.name, port, (url) => {
22436
+ if (mode === "manual") {
22437
+ console.log(`
22438
+ Open this on any machine with a browser and authorize ${entry.name}:
22439
+
22440
+ ${url}
22441
+
22442
+ ` + `The browser will then be redirected to ${`http://localhost:${port}/callback`}, which
22443
+ ` + `will not load — that is expected, nothing is listening there. Copy the full URL out of
22444
+ ` + `the address bar and paste it below.
22445
+ `);
22446
+ return;
22447
+ }
22206
22448
  console.log(`
22207
22449
  Opening your browser to authorize ${entry.name}:
22208
22450
  ${url}
@@ -22215,7 +22457,7 @@ Opening your browser to authorize ${entry.name}:
22215
22457
  return { ok: true, message: `${entry.name}: already authorized (existing grant is still valid).` };
22216
22458
  }
22217
22459
  pendingState = (await readAuth(entry.name)).state;
22218
- const code = await withTimeout2(codePromise, 5 * 60000, "waiting for the browser callback");
22460
+ const code = mode === "manual" ? await readPastedCode(pendingState) : await withTimeout2(codePromise, 5 * 60000, "waiting for the browser callback");
22219
22461
  const result = await auth(provider, { serverUrl: entry.url, authorizationCode: code, scope });
22220
22462
  if (result !== "AUTHORIZED") {
22221
22463
  return { ok: false, message: `${entry.name}: token exchange did not complete (${result}).` };
@@ -22246,6 +22488,40 @@ function evaluateCallback(params, expectedState) {
22246
22488
  failure: description ? `${error ?? "error"}: ${description}` : error ?? "no code returned"
22247
22489
  };
22248
22490
  }
22491
+ async function readPastedCode(expectedState) {
22492
+ const { createInterface } = await import("node:readline/promises");
22493
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
22494
+ try {
22495
+ for (let attempt = 0;attempt < 3; attempt++) {
22496
+ const answer = (await rl.question("Paste the full redirect URL (or just the code): ")).trim();
22497
+ if (!answer)
22498
+ continue;
22499
+ const parsed = parsePastedRedirect(answer, expectedState);
22500
+ if ("code" in parsed)
22501
+ return parsed.code;
22502
+ console.log(` ${parsed.error}`);
22503
+ }
22504
+ throw new Error("no usable authorization code was pasted");
22505
+ } finally {
22506
+ rl.close();
22507
+ }
22508
+ }
22509
+ function parsePastedRedirect(input, expectedState) {
22510
+ const trimmed = input.trim();
22511
+ if (!/^https?:\/\//i.test(trimmed)) {
22512
+ if (/[\s?&]/.test(trimmed))
22513
+ return { error: "that does not look like a URL or a code — try again" };
22514
+ return { code: trimmed };
22515
+ }
22516
+ let params;
22517
+ try {
22518
+ params = new URL(trimmed).searchParams;
22519
+ } catch {
22520
+ return { error: "that URL could not be parsed — paste the whole address bar" };
22521
+ }
22522
+ const { code, failure } = evaluateCallback(params, expectedState);
22523
+ return code ? { code } : { error: failure ?? "no code in that URL" };
22524
+ }
22249
22525
  function openBrowser(url) {
22250
22526
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
22251
22527
  try {
@@ -22289,7 +22565,7 @@ function chooseScopes(advertised, want) {
22289
22565
  if (!ro.length) {
22290
22566
  return {
22291
22567
  error: `no read-only scopes are offered (available: ${advertised.join(", ")}).
22292
- ` + ` Authorize with full access, or pick explicitly with --scopes.`
22568
+ Authorize with full access, or pick explicitly with --scopes.`
22293
22569
  };
22294
22570
  }
22295
22571
  return { scope: ro.join(" ") };
@@ -22306,11 +22582,11 @@ function summarizeScopes(scope, limit = 6) {
22306
22582
  if (all.length <= limit) {
22307
22583
  return `scope: ${all.join(", ")}${writes.length ? "" : " (read-only)"}`;
22308
22584
  }
22309
- return `scope: ${all.length} granted, ${writes.length} of them write ` + `(${writes.slice(0, 3).join(", ") || "none"}${writes.length > 3 ? ", …" : ""})`;
22585
+ return `scope: ${all.length} granted, ${writes.length} of them write (${writes.slice(0, 3).join(", ") || "none"}${writes.length > 3 ? ", …" : ""})`;
22310
22586
  }
22311
22587
 
22312
22588
  // src/cli.ts
22313
- var VERSION2 = "0.2.4";
22589
+ var VERSION2 = "0.2.5";
22314
22590
  var USAGE = `autorouter — one search tool instead of every tool
22315
22591
 
22316
22592
  autorouter serve Run as an MCP server over stdio (default)
@@ -22320,7 +22596,8 @@ var USAGE = `autorouter — one search tool instead of every tool
22320
22596
  autorouter list [--kind K] List everything in the catalog
22321
22597
  autorouter reindex Rebuild the catalog now
22322
22598
  autorouter doctor Show what is reachable and what it saves
22323
- autorouter login [server] Authorize an OAuth server (opens a browser);
22599
+ autorouter login [server] Authorize an OAuth server (opens a browser,
22600
+ or prints a code on a headless box);
22324
22601
  with no argument, lists what needs one
22325
22602
  autorouter logout <server> Forget a stored grant
22326
22603
  autorouter add <name> --url URL Register a server with the router directly
@@ -22360,6 +22637,10 @@ Options
22360
22637
  any narrowing the previous grant carried
22361
22638
  --scopes S login: request exactly these scopes (comma or space separated)
22362
22639
  --list-scopes login: show what the server offers, authorize nothing
22640
+ --device login: RFC 8628 — print a code to enter on another device and
22641
+ poll for the result. No browser or open port needed here.
22642
+ --manual login: print the authorization URL, then read the redirect you
22643
+ paste back. Works where the provider has no device endpoint.
22363
22644
 
22364
22645
  \`add\` registers behind the router, so a new server never enters your context.
22365
22646
  Servers added to a harness the normal way (\`claude mcp add\`) are moved behind
@@ -22481,6 +22762,8 @@ async function cmdLogin(server, flags) {
22481
22762
  };
22482
22763
  }));
22483
22764
  console.log(`Usage: autorouter login <server> [--read-only | --all-scopes | --scopes a,b]
22765
+ `);
22766
+ console.log(` On a machine with no browser, add --device (or --manual).
22484
22767
  `);
22485
22768
  for (const s of states) {
22486
22769
  const scope = s.ok && s.scope ? ` ${summarizeScopes(s.scope)}` : "";
@@ -22506,7 +22789,9 @@ ${pending.length} need a grant; each is a separate authorization:`);
22506
22789
  scopes: flags.scopes,
22507
22790
  readOnly: Boolean(flags["read-only"]),
22508
22791
  allScopes: Boolean(flags["all-scopes"]),
22509
- listScopes: Boolean(flags["list-scopes"])
22792
+ listScopes: Boolean(flags["list-scopes"]),
22793
+ device: Boolean(flags.device),
22794
+ manual: Boolean(flags.manual)
22510
22795
  });
22511
22796
  console.log(result.message);
22512
22797
  if (result.ok && !flags["list-scopes"]) {
@@ -22797,7 +23082,7 @@ function parseArgs(argv) {
22797
23082
  command = `--${name}`;
22798
23083
  continue;
22799
23084
  }
22800
- const boolean = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "all-scopes", "list-scopes"].includes(name) && !(name === "json" && command === "add");
23085
+ const boolean = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "all-scopes", "list-scopes", "device", "manual"].includes(name) && !(name === "json" && command === "add");
22801
23086
  if (boolean) {
22802
23087
  flags[name] = true;
22803
23088
  } else if (inline !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autorouter-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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.4",
6
+ "version": "0.2.5",
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.4",
17
+ "version": "0.2.5",
18
18
  "transport": { "type": "stdio" },
19
19
  "packageArguments": [{ "type": "positional", "value": "serve" }],
20
20
  "environmentVariables": [