autorouter-mcp 0.2.5 → 0.2.7

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
@@ -364,12 +364,41 @@ 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)
369
372
  autorouter login remote-server --device # headless: enter a code elsewhere
373
+ autorouter login remote-server --no-reindex # authorize, index later
370
374
  autorouter logout remote-server # forget a stored grant
371
375
  ```
372
376
 
377
+ ## Updating
378
+
379
+ `autorouter update` upgrades in place, using the package manager that installed
380
+ the copy you are running:
381
+
382
+ ```sh
383
+ autorouter update # detect, then upgrade
384
+ autorouter update --check # report the available version, install nothing
385
+ autorouter update --dry-run # print the command, do not run it
386
+ ```
387
+
388
+ Which manager to use is decided by where the running file sits on disk, not by
389
+ what happens to be on `PATH` — running `npm i -g` against a pnpm-managed global
390
+ installs a second copy that shadows the first, and you would then be upgrading
391
+ one install while running the other.
392
+
393
+ Three cases do not run a package manager, and say so instead: a copy unpacked by
394
+ `npx`/`bunx`/`pnpm dlx` (nothing to upgrade — those refetch every run), a git
395
+ checkout (`git pull && bun install && bun run build`), and a layout it cannot
396
+ identify. All three still report whether a newer version exists.
397
+
398
+ The router is a long-lived stdio server, so a harness that already has it
399
+ running keeps the old build until it respawns it — restart the harness, or
400
+ reconnect the MCP server, after updating.
401
+
373
402
  ## OAuth servers
374
403
 
375
404
  Some remote MCP servers carry no credentials in their visible configuration.
@@ -382,9 +411,17 @@ has a token to borrow:
382
411
 
383
412
  ```sh
384
413
  autorouter login remote-server
385
- autorouter reindex
386
414
  ```
387
415
 
416
+ A new grant is followed by a reindex automatically, because the capabilities
417
+ behind it are not searchable until the catalog has seen them — a grant on its
418
+ own changes nothing the router can reach. `--no-reindex` skips it when you are
419
+ authorizing several servers in a row and would rather index once at the end.
420
+
421
+ Only a login that actually stores a new grant triggers it. `--list-scopes` and a
422
+ server that already had a grant both change nothing, so neither pays for an
423
+ index rebuild.
424
+
388
425
  Registration is RFC 7591 dynamic client registration, so there is no app to
389
426
  create first. Tokens live in `~/.autorouter/oauth/<server>.json` at `0600` and
390
427
  are refreshed automatically; `logout` deletes them. The loopback redirect uses a
package/dist/cli.js CHANGED
@@ -19474,6 +19474,18 @@ function run(command, args, opts = {}) {
19474
19474
  child.stdin.end();
19475
19475
  });
19476
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
+ }
19477
19489
  function which(command) {
19478
19490
  const isWindows = process.platform === "win32";
19479
19491
  const exts = isWindows ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
@@ -22311,6 +22323,7 @@ async function runLogin(opts) {
22311
22323
  return result;
22312
22324
  return {
22313
22325
  ok: true,
22326
+ authorized: true,
22314
22327
  message: `${entry.name}: authorized.` + (result.grantedScope ? `
22315
22328
  ${summarizeScopes(result.grantedScope)}` : "")
22316
22329
  };
@@ -22454,7 +22467,11 @@ Opening your browser to authorize ${entry.name}:
22454
22467
  try {
22455
22468
  const first = await auth(provider, { serverUrl: entry.url, scope });
22456
22469
  if (first === "AUTHORIZED") {
22457
- return { ok: true, message: `${entry.name}: already authorized (existing grant is still valid).` };
22470
+ return {
22471
+ ok: true,
22472
+ authorized: true,
22473
+ message: `${entry.name}: already authorized (existing grant is still valid).`
22474
+ };
22458
22475
  }
22459
22476
  pendingState = (await readAuth(entry.name)).state;
22460
22477
  const code = mode === "manual" ? await readPastedCode(pendingState) : await withTimeout2(codePromise, 5 * 60000, "waiting for the browser callback");
@@ -22466,8 +22483,12 @@ Opening your browser to authorize ${entry.name}:
22466
22483
  await writeAuth(entry.name, { requestedScope: scope });
22467
22484
  const stored = await readAuth(entry.name);
22468
22485
  const granted = stored.tokens?.scope ?? scope;
22469
- return { ok: true, message: `${entry.name}: authorized.${granted ? `
22470
- ${summarizeScopes(granted)}` : ""}` };
22486
+ return {
22487
+ ok: true,
22488
+ authorized: true,
22489
+ message: `${entry.name}: authorized.${granted ? `
22490
+ ${summarizeScopes(granted)}` : ""}`
22491
+ };
22471
22492
  } catch (err) {
22472
22493
  return { ok: false, message: `${entry.name}: ${err instanceof Error ? err.message : String(err)}` };
22473
22494
  } finally {
@@ -22585,8 +22606,224 @@ function summarizeScopes(scope, limit = 6) {
22585
22606
  return `scope: ${all.length} granted, ${writes.length} of them write (${writes.slice(0, 3).join(", ") || "none"}${writes.length > 3 ? ", …" : ""})`;
22586
22607
  }
22587
22608
 
22609
+ // src/cli/update.ts
22610
+ import { existsSync, realpathSync } from "node:fs";
22611
+ import { fileURLToPath } from "node:url";
22612
+ import { dirname as dirname3, join as join18, resolve as resolve2, sep } from "node:path";
22613
+ var PACKAGE_NAME = "autorouter-mcp";
22614
+ function entryPath() {
22615
+ let raw;
22616
+ try {
22617
+ raw = fileURLToPath(import.meta.url);
22618
+ } catch {
22619
+ raw = process.argv[1] ?? "";
22620
+ }
22621
+ try {
22622
+ return realpathSync(raw);
22623
+ } catch {
22624
+ return raw;
22625
+ }
22626
+ }
22627
+ function packageRoot(from, exists) {
22628
+ let dir = dirname3(from);
22629
+ for (let i = 0;i < 12; i++) {
22630
+ if (exists(join18(dir, "package.json")))
22631
+ return dir;
22632
+ const parent = dirname3(dir);
22633
+ if (parent === dir)
22634
+ break;
22635
+ dir = parent;
22636
+ }
22637
+ return;
22638
+ }
22639
+ function managerFromLockfile(dir, exists) {
22640
+ if (exists(join18(dir, "bun.lock")) || exists(join18(dir, "bun.lockb")))
22641
+ return "bun";
22642
+ if (exists(join18(dir, "pnpm-lock.yaml")))
22643
+ return "pnpm";
22644
+ if (exists(join18(dir, "yarn.lock")))
22645
+ return "yarn";
22646
+ return "npm";
22647
+ }
22648
+ function detectInstall(entry, exists = existsSync) {
22649
+ const root = packageRoot(entry, exists);
22650
+ if (!root)
22651
+ return { kind: "unknown", dir: dirname3(entry) };
22652
+ const path = root.split(sep).join("/");
22653
+ if (exists(join18(root, ".git")))
22654
+ return { kind: "source", dir: root };
22655
+ const transient = path.includes("/_npx/") && "npx" || /\/dlx(-|\/)/.test(path) && "pnpm dlx" || path.includes("/.bun/install/cache/") && "bunx" || null;
22656
+ if (transient)
22657
+ return { kind: "transient", runner: transient, dir: root };
22658
+ const nodeModules = dirname3(root);
22659
+ const parent = dirname3(nodeModules);
22660
+ 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;
22661
+ if (globalManager)
22662
+ return { kind: "global", manager: globalManager, dir: root };
22663
+ if (nodeModules.split(sep).pop() === "node_modules") {
22664
+ if (exists(join18(parent, "package.json"))) {
22665
+ return {
22666
+ kind: "project",
22667
+ manager: managerFromLockfile(parent, exists),
22668
+ dir: root,
22669
+ projectDir: parent
22670
+ };
22671
+ }
22672
+ return { kind: "global", manager: "npm", dir: root };
22673
+ }
22674
+ return { kind: "unknown", dir: root };
22675
+ }
22676
+ function updateCommand(install, pkg, version = "latest") {
22677
+ const spec = `${pkg}@${version}`;
22678
+ if (install.kind === "global") {
22679
+ switch (install.manager) {
22680
+ case "bun":
22681
+ return ["bun", "add", "-g", spec];
22682
+ case "pnpm":
22683
+ return ["pnpm", "add", "-g", spec];
22684
+ case "yarn":
22685
+ return ["yarn", "global", "add", spec];
22686
+ default:
22687
+ return ["npm", "install", "-g", spec];
22688
+ }
22689
+ }
22690
+ if (install.kind === "project") {
22691
+ switch (install.manager) {
22692
+ case "bun":
22693
+ return ["bun", "add", spec];
22694
+ case "pnpm":
22695
+ return ["pnpm", "add", spec];
22696
+ case "yarn":
22697
+ return ["yarn", "add", spec];
22698
+ default:
22699
+ return ["npm", "install", spec];
22700
+ }
22701
+ }
22702
+ return null;
22703
+ }
22704
+ function compareVersions2(a, b) {
22705
+ const split = (v) => {
22706
+ const [core = "", pre] = v.replace(/^v/, "").split("-", 2);
22707
+ const parts = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
22708
+ return { parts, pre };
22709
+ };
22710
+ const x = split(a);
22711
+ const y = split(b);
22712
+ for (let i = 0;i < 3; i++) {
22713
+ const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
22714
+ if (d !== 0)
22715
+ return d < 0 ? -1 : 1;
22716
+ }
22717
+ if (x.pre && !y.pre)
22718
+ return -1;
22719
+ if (!x.pre && y.pre)
22720
+ return 1;
22721
+ if (x.pre && y.pre && x.pre !== y.pre)
22722
+ return x.pre < y.pre ? -1 : 1;
22723
+ return 0;
22724
+ }
22725
+ async function latestVersion(pkg, fetchFn = fetch) {
22726
+ const base = (process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? "https://registry.npmjs.org").replace(/\/+$/, "");
22727
+ try {
22728
+ const res = await fetchFn(`${base}/${encodeURIComponent(pkg)}/latest`, {
22729
+ headers: { accept: "application/json" },
22730
+ signal: AbortSignal.timeout(1e4)
22731
+ });
22732
+ if (!res.ok)
22733
+ return { error: `registry returned ${res.status}` };
22734
+ const body = await res.json();
22735
+ return body.version ? { version: body.version } : { error: "registry returned no version" };
22736
+ } catch (err) {
22737
+ return { error: err instanceof Error ? err.message : String(err) };
22738
+ }
22739
+ }
22740
+ function cannotUpdate(install, pkg) {
22741
+ if (install.kind === "transient") {
22742
+ return `This copy was unpacked by ${install.runner}, which fetches the package fresh on every run —
22743
+ ` + ` there is no install here to upgrade. You are already getting the latest each time.
22744
+ ` + ` To keep a copy that does not re-download: npm install -g ${pkg}`;
22745
+ }
22746
+ if (install.kind === "source") {
22747
+ return `This is a checkout running from source (${install.dir}), not a package install.
22748
+ ` + ` Update it with: git -C ${install.dir} pull && bun install && bun run build`;
22749
+ }
22750
+ return `Could not tell which package manager installed this copy (${install.dir}).
22751
+ ` + ` Upgrade it the way you installed it, e.g. npm install -g ${pkg}@latest`;
22752
+ }
22753
+ function describe2(install) {
22754
+ switch (install.kind) {
22755
+ case "global":
22756
+ return `${install.manager} global install at ${install.dir}`;
22757
+ case "project":
22758
+ return `${install.manager} dependency of ${install.projectDir}`;
22759
+ case "transient":
22760
+ return `${install.runner} temporary copy at ${install.dir}`;
22761
+ case "source":
22762
+ return `source checkout at ${install.dir}`;
22763
+ default:
22764
+ return `unrecognized install at ${install.dir}`;
22765
+ }
22766
+ }
22767
+ async function runUpdate(opts) {
22768
+ const entry = opts.entry ?? entryPath();
22769
+ const install = detectInstall(entry, opts.exists);
22770
+ const manifest = install.kind === "unknown" ? null : await readJson(join18(install.dir, "package.json"));
22771
+ const pkg = manifest?.name ?? PACKAGE_NAME;
22772
+ const latest = await latestVersion(pkg, opts.fetchFn);
22773
+ if ("error" in latest) {
22774
+ return { ok: false, message: `Could not reach the registry to check for updates: ${latest.error}` };
22775
+ }
22776
+ const behind = compareVersions2(opts.current, latest.version) < 0;
22777
+ const status = behind ? `autorouter ${opts.current} → ${latest.version} available` : `autorouter ${opts.current} is up to date (latest is ${latest.version})`;
22778
+ const command = updateCommand(install, pkg, latest.version);
22779
+ if (!command) {
22780
+ return { ok: !behind, message: `${status}
22781
+
22782
+ ${cannotUpdate(install, pkg)}` };
22783
+ }
22784
+ const printable = command.join(" ");
22785
+ if (opts.check) {
22786
+ return { ok: true, message: `${status}
22787
+ ${describe2(install)}
22788
+ Update with: ${printable}` };
22789
+ }
22790
+ if (!behind && !opts.force) {
22791
+ return { ok: true, message: `${status}
22792
+ ${describe2(install)}
22793
+ Re-install anyway with: --force` };
22794
+ }
22795
+ if (opts.dryRun) {
22796
+ return { ok: true, message: `${status}
22797
+ ${describe2(install)}
22798
+ Would run: ${printable}` };
22799
+ }
22800
+ console.log(`${status}
22801
+ ${describe2(install)}
22802
+ Running: ${printable}
22803
+ `);
22804
+ const code = await runStreaming(command[0], command.slice(1), {
22805
+ cwd: install.kind === "project" ? install.projectDir : undefined
22806
+ }).catch((err) => err);
22807
+ if (code instanceof Error) {
22808
+ return {
22809
+ ok: false,
22810
+ message: `Could not run ${command[0]}: ${code.message}
22811
+ ` + ` Run it yourself: ${printable}`
22812
+ };
22813
+ }
22814
+ if (code !== 0) {
22815
+ return { ok: false, message: `${command[0]} exited with code ${code}. Nothing was changed by autorouter.` };
22816
+ }
22817
+ return {
22818
+ ok: true,
22819
+ message: `
22820
+ Updated to ${latest.version}. Restart any harness with the router running to pick it up` + `
22821
+ (it is a long-lived stdio server, so an open session keeps the old build).`
22822
+ };
22823
+ }
22824
+
22588
22825
  // src/cli.ts
22589
- var VERSION2 = "0.2.5";
22826
+ var VERSION2 = "0.2.7";
22590
22827
  var USAGE = `autorouter — one search tool instead of every tool
22591
22828
 
22592
22829
  autorouter serve Run as an MCP server over stdio (default)
@@ -22596,6 +22833,9 @@ var USAGE = `autorouter — one search tool instead of every tool
22596
22833
  autorouter list [--kind K] List everything in the catalog
22597
22834
  autorouter reindex Rebuild the catalog now
22598
22835
  autorouter doctor Show what is reachable and what it saves
22836
+ autorouter update Upgrade via the package manager that
22837
+ installed this copy. --check to look
22838
+ without installing.
22599
22839
  autorouter login [server] Authorize an OAuth server (opens a browser,
22600
22840
  or prints a code on a headless box);
22601
22841
  with no argument, lists what needs one
@@ -22623,6 +22863,8 @@ Options
22623
22863
  --json machine-readable output
22624
22864
  --yes init/adopt: do not prompt
22625
22865
  --dry-run adopt: show what would move, change nothing
22866
+ update: print the upgrade command without running it
22867
+ --check update: report the available version, install nothing
22626
22868
  --force adopt: proceed even if a server is unreachable
22627
22869
  --keep S adopt: leave server S registered in the harness (comma-separated)
22628
22870
  --keep-skill S adopt: leave skill S loaded (comma-separated)
@@ -22637,6 +22879,7 @@ Options
22637
22879
  any narrowing the previous grant carried
22638
22880
  --scopes S login: request exactly these scopes (comma or space separated)
22639
22881
  --list-scopes login: show what the server offers, authorize nothing
22882
+ --no-reindex login: skip the reindex that otherwise follows a new grant
22640
22883
  --device login: RFC 8628 — print a code to enter on another device and
22641
22884
  poll for the result. No browser or open port needed here.
22642
22885
  --manual login: print the authorization URL, then read the redirect you
@@ -22666,18 +22909,23 @@ async function main(argv) {
22666
22909
  return await cmdCall(positionals[0], flags);
22667
22910
  case "list":
22668
22911
  return await cmdList(flags);
22669
- case "reindex": {
22670
- const router = await Router.create({ force: true });
22671
- console.log(`Reindexed: ${router.summary()}`);
22672
- const failures = Object.entries(router.catalog.errors);
22673
- for (const [name, err] of failures)
22674
- console.error(` unreachable: ${name}: ${oneLine(err, 100)}`);
22675
- await router.close();
22912
+ case "reindex":
22913
+ await cmdReindex();
22676
22914
  return 0;
22677
- }
22678
22915
  case "doctor":
22679
22916
  console.log(await runDoctor(process.cwd()));
22680
22917
  return 0;
22918
+ case "update":
22919
+ case "upgrade": {
22920
+ const result = await runUpdate({
22921
+ current: VERSION2,
22922
+ check: Boolean(flags.check),
22923
+ dryRun: Boolean(flags["dry-run"]),
22924
+ force: Boolean(flags.force)
22925
+ });
22926
+ console.log(result.message);
22927
+ return result.ok ? 0 : 1;
22928
+ }
22681
22929
  case "init":
22682
22930
  return await cmdInit(flags);
22683
22931
  case "login":
@@ -22745,6 +22993,14 @@ async function cmdRemove(name) {
22745
22993
  console.log(await runRemove(name));
22746
22994
  return 0;
22747
22995
  }
22996
+ async function cmdReindex() {
22997
+ const router = await Router.create({ force: true });
22998
+ console.log(`Reindexed: ${router.summary()}`);
22999
+ for (const [name, err] of Object.entries(router.catalog.errors)) {
23000
+ console.error(` unreachable: ${name}: ${oneLine(err, 100)}`);
23001
+ }
23002
+ await router.close();
23003
+ }
22748
23004
  async function cmdLogin(server, flags) {
22749
23005
  if (!server) {
22750
23006
  const resolved = await resolveConfig(process.cwd());
@@ -22794,10 +23050,20 @@ ${pending.length} need a grant; each is a separate authorization:`);
22794
23050
  manual: Boolean(flags.manual)
22795
23051
  });
22796
23052
  console.log(result.message);
22797
- if (result.ok && !flags["list-scopes"]) {
22798
- console.log("Re-run `autorouter reindex` to pick up its capabilities.");
23053
+ if (!result.authorized)
23054
+ return result.ok ? 0 : 1;
23055
+ if (flags["no-reindex"]) {
23056
+ console.log("Skipped the reindex; run `autorouter reindex` to pick up its capabilities.");
23057
+ return 0;
22799
23058
  }
22800
- return result.ok ? 0 : 1;
23059
+ try {
23060
+ await cmdReindex();
23061
+ } catch (err) {
23062
+ console.error(`
23063
+ The grant was saved, but the reindex failed: ${oneLine(err instanceof Error ? err.message : String(err), 200)}
23064
+ ` + ` Run \`autorouter reindex\` once that is resolved.`);
23065
+ }
23066
+ return 0;
22801
23067
  }
22802
23068
  async function cmdLogout(server) {
22803
23069
  if (!server) {
@@ -23082,7 +23348,7 @@ function parseArgs(argv) {
23082
23348
  command = `--${name}`;
23083
23349
  continue;
23084
23350
  }
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");
23351
+ const boolean = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "all-scopes", "list-scopes", "device", "manual", "check", "no-reindex"].includes(name) && !(name === "json" && command === "add");
23086
23352
  if (boolean) {
23087
23353
  flags[name] = true;
23088
23354
  } else if (inline !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autorouter-mcp",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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.5",
6
+ "version": "0.2.7",
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.5",
17
+ "version": "0.2.7",
18
18
  "transport": { "type": "stdio" },
19
19
  "packageArguments": [{ "type": "positional", "value": "serve" }],
20
20
  "environmentVariables": [