autorouter-mcp 0.2.0 → 0.2.2
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 +96 -28
- package/dist/cli.js +1123 -772
- package/package.json +2 -2
- package/server.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -7496,6 +7496,10 @@ var DEFAULT_CONFIG = {
|
|
|
7496
7496
|
exclude: [],
|
|
7497
7497
|
alwaysExpose: [],
|
|
7498
7498
|
confirm: [],
|
|
7499
|
+
promptMode: "commands",
|
|
7500
|
+
activation: "lazy",
|
|
7501
|
+
autoAdopt: true,
|
|
7502
|
+
allowAddServer: true,
|
|
7499
7503
|
selector: {
|
|
7500
7504
|
mode: "auto",
|
|
7501
7505
|
candidates: 30,
|
|
@@ -8910,6 +8914,23 @@ function substitutePluginRoot(value, root) {
|
|
|
8910
8914
|
}
|
|
8911
8915
|
|
|
8912
8916
|
// src/config/resolve.ts
|
|
8917
|
+
function harnessSources(name, cwd) {
|
|
8918
|
+
const home = homeDir();
|
|
8919
|
+
switch (name) {
|
|
8920
|
+
case "claude":
|
|
8921
|
+
return [join7(home, ".claude.json"), join7(cwd, ".mcp.json")];
|
|
8922
|
+
case "cursor":
|
|
8923
|
+
return [join7(home, ".cursor", "mcp.json"), join7(cwd, ".cursor", "mcp.json")];
|
|
8924
|
+
case "vscode":
|
|
8925
|
+
return [join7(home, ".vscode", "mcp.json"), join7(cwd, ".vscode", "mcp.json")];
|
|
8926
|
+
case "codex":
|
|
8927
|
+
return [join7(home, ".codex", "config.toml"), join7(cwd, ".codex", "config.toml")];
|
|
8928
|
+
case "plugins":
|
|
8929
|
+
return [join7(home, ".claude", "plugins", "installed_plugins.json")];
|
|
8930
|
+
default:
|
|
8931
|
+
return [];
|
|
8932
|
+
}
|
|
8933
|
+
}
|
|
8913
8934
|
function configCandidates(cwd) {
|
|
8914
8935
|
const explicit = process.env.AUTOROUTER_CONFIG;
|
|
8915
8936
|
return [
|
|
@@ -8945,6 +8966,7 @@ async function resolveConfig(cwd = process.cwd()) {
|
|
|
8945
8966
|
};
|
|
8946
8967
|
applyEnvOverrides(config);
|
|
8947
8968
|
const imported = [];
|
|
8969
|
+
const sources = [];
|
|
8948
8970
|
const loaders = {
|
|
8949
8971
|
claude: () => loadClaude(cwd),
|
|
8950
8972
|
cursor: () => loadCursor(cwd),
|
|
@@ -8956,6 +8978,7 @@ async function resolveConfig(cwd = process.cwd()) {
|
|
|
8956
8978
|
const load = loaders[name];
|
|
8957
8979
|
if (!load)
|
|
8958
8980
|
continue;
|
|
8981
|
+
sources.push(...harnessSources(name, cwd));
|
|
8959
8982
|
try {
|
|
8960
8983
|
imported.push(...await load());
|
|
8961
8984
|
} catch {}
|
|
@@ -8965,7 +8988,7 @@ async function resolveConfig(cwd = process.cwd()) {
|
|
|
8965
8988
|
if (entry)
|
|
8966
8989
|
imported.push(entry);
|
|
8967
8990
|
}
|
|
8968
|
-
return { config, servers: dedupeServers(imported), configPath, cwd };
|
|
8991
|
+
return { config, servers: dedupeServers(imported), configPath, cwd, sources };
|
|
8969
8992
|
}
|
|
8970
8993
|
function applyEnvOverrides(config) {
|
|
8971
8994
|
const env = process.env;
|
|
@@ -8982,6 +9005,16 @@ function applyEnvOverrides(config) {
|
|
|
8982
9005
|
}
|
|
8983
9006
|
if (env.AUTOROUTER_SELECTOR_BASE_URL)
|
|
8984
9007
|
config.selector.baseUrl = env.AUTOROUTER_SELECTOR_BASE_URL;
|
|
9008
|
+
if (env.AUTOROUTER_PROMPT_MODE) {
|
|
9009
|
+
config.promptMode = env.AUTOROUTER_PROMPT_MODE;
|
|
9010
|
+
}
|
|
9011
|
+
if (env.AUTOROUTER_ACTIVATION) {
|
|
9012
|
+
config.activation = env.AUTOROUTER_ACTIVATION;
|
|
9013
|
+
}
|
|
9014
|
+
if (env.AUTOROUTER_AUTO_ADOPT)
|
|
9015
|
+
config.autoAdopt = env.AUTOROUTER_AUTO_ADOPT !== "0";
|
|
9016
|
+
if (env.AUTOROUTER_ALLOW_ADD_SERVER)
|
|
9017
|
+
config.allowAddServer = env.AUTOROUTER_ALLOW_ADD_SERVER !== "0";
|
|
8985
9018
|
if (env.AUTOROUTER_EMBEDDINGS_PROVIDER) {
|
|
8986
9019
|
config.embeddings.provider = env.AUTOROUTER_EMBEDDINGS_PROVIDER;
|
|
8987
9020
|
}
|
|
@@ -19211,7 +19244,7 @@ async function buildCatalog(resolved) {
|
|
|
19211
19244
|
const { config: config2, servers, cwd, configPath } = resolved;
|
|
19212
19245
|
const capabilities = [];
|
|
19213
19246
|
const errors3 = {};
|
|
19214
|
-
const sourceFiles = configPath ? [configPath] : [];
|
|
19247
|
+
const sourceFiles = [...configPath ? [configPath] : [], ...resolved.sources];
|
|
19215
19248
|
sourceFiles.push(...servers.filter((s) => s.transport === "http").map((s) => `${AUTH}${authPath(s.name)}`));
|
|
19216
19249
|
const skills = await collectSkills(config2.skillPaths, cwd, {
|
|
19217
19250
|
includePlugins: config2.import.includes("plugins")
|
|
@@ -20201,6 +20234,10 @@ class ConnectionPool {
|
|
|
20201
20234
|
names() {
|
|
20202
20235
|
return [...this.byName.keys()];
|
|
20203
20236
|
}
|
|
20237
|
+
setEntries(entries) {
|
|
20238
|
+
this.reset();
|
|
20239
|
+
this.byName = new Map(entries.map((e) => [e.name, e]));
|
|
20240
|
+
}
|
|
20204
20241
|
async get(serverName) {
|
|
20205
20242
|
const existing = this.clients.get(serverName);
|
|
20206
20243
|
if (existing) {
|
|
@@ -20302,15 +20339,15 @@ class CapabilityError extends Error {
|
|
|
20302
20339
|
}
|
|
20303
20340
|
|
|
20304
20341
|
class Router {
|
|
20305
|
-
resolved;
|
|
20306
20342
|
pool;
|
|
20307
|
-
guard;
|
|
20308
20343
|
catalog;
|
|
20309
20344
|
index;
|
|
20345
|
+
resolved;
|
|
20346
|
+
guard;
|
|
20310
20347
|
refreshing = null;
|
|
20311
20348
|
constructor(resolved, catalog, index, pool, guard) {
|
|
20312
|
-
this.resolved = resolved;
|
|
20313
20349
|
this.pool = pool;
|
|
20350
|
+
this.resolved = resolved;
|
|
20314
20351
|
this.guard = guard;
|
|
20315
20352
|
this.catalog = catalog;
|
|
20316
20353
|
this.index = index;
|
|
@@ -20329,13 +20366,16 @@ class Router {
|
|
|
20329
20366
|
return;
|
|
20330
20367
|
this.refreshing = (async () => {
|
|
20331
20368
|
try {
|
|
20332
|
-
const
|
|
20369
|
+
const resolved = await resolveConfig(this.resolved.cwd);
|
|
20370
|
+
const catalog = await buildCatalog(resolved);
|
|
20333
20371
|
await saveCatalog(catalog);
|
|
20334
|
-
const index = new HybridIndex(catalog.capabilities,
|
|
20372
|
+
const index = new HybridIndex(catalog.capabilities, resolved.config);
|
|
20335
20373
|
await index.warmEmbeddings();
|
|
20374
|
+
this.resolved = resolved;
|
|
20375
|
+
this.guard = new Guard(resolved.config);
|
|
20336
20376
|
this.catalog = catalog;
|
|
20337
20377
|
this.index = index;
|
|
20338
|
-
this.pool.
|
|
20378
|
+
this.pool.setEntries(resolved.servers);
|
|
20339
20379
|
} catch {} finally {
|
|
20340
20380
|
this.refreshing = null;
|
|
20341
20381
|
}
|
|
@@ -21152,9 +21192,27 @@ function oneLine(text, max) {
|
|
|
21152
21192
|
|
|
21153
21193
|
// src/server/prompts.ts
|
|
21154
21194
|
var INSTRUCTION_KINDS = new Set(["skill", "command", "agent"]);
|
|
21195
|
+
var KINDS_BY_MODE = {
|
|
21196
|
+
all: INSTRUCTION_KINDS,
|
|
21197
|
+
commands: new Set(["command", "agent"]),
|
|
21198
|
+
none: new Set
|
|
21199
|
+
};
|
|
21155
21200
|
var FIND_PROMPT = "find";
|
|
21156
|
-
var DESCRIPTION_CHARS =
|
|
21157
|
-
function
|
|
21201
|
+
var DESCRIPTION_CHARS = 120;
|
|
21202
|
+
function* published(capabilities, mode) {
|
|
21203
|
+
const kinds = KINDS_BY_MODE[mode] ?? KINDS_BY_MODE.commands;
|
|
21204
|
+
if (!kinds.size)
|
|
21205
|
+
return;
|
|
21206
|
+
const seen = new Set([FIND_PROMPT]);
|
|
21207
|
+
for (const cap of capabilities) {
|
|
21208
|
+
if (!kinds.has(cap.kind))
|
|
21209
|
+
continue;
|
|
21210
|
+
const name = uniquePromptName(cap, seen);
|
|
21211
|
+
seen.add(name);
|
|
21212
|
+
yield [name, cap];
|
|
21213
|
+
}
|
|
21214
|
+
}
|
|
21215
|
+
function promptList(capabilities, mode = "commands") {
|
|
21158
21216
|
const prompts = [
|
|
21159
21217
|
{
|
|
21160
21218
|
name: FIND_PROMPT,
|
|
@@ -21162,12 +21220,7 @@ function promptList(capabilities) {
|
|
|
21162
21220
|
arguments: [{ name: "query", description: "What you are trying to accomplish.", required: true }]
|
|
21163
21221
|
}
|
|
21164
21222
|
];
|
|
21165
|
-
const
|
|
21166
|
-
for (const cap of capabilities) {
|
|
21167
|
-
if (!INSTRUCTION_KINDS.has(cap.kind))
|
|
21168
|
-
continue;
|
|
21169
|
-
const name = uniquePromptName(cap, seen);
|
|
21170
|
-
seen.add(name);
|
|
21223
|
+
for (const [name, cap] of published(capabilities, mode)) {
|
|
21171
21224
|
prompts.push({
|
|
21172
21225
|
name,
|
|
21173
21226
|
description: clamp(cap.description || `${cap.kind} ${cap.name}`, DESCRIPTION_CHARS),
|
|
@@ -21197,13 +21250,8 @@ function uniquePromptName(cap, taken) {
|
|
|
21197
21250
|
}
|
|
21198
21251
|
return base;
|
|
21199
21252
|
}
|
|
21200
|
-
function capabilityForPrompt(capabilities, name) {
|
|
21201
|
-
const
|
|
21202
|
-
for (const cap of capabilities) {
|
|
21203
|
-
if (!INSTRUCTION_KINDS.has(cap.kind))
|
|
21204
|
-
continue;
|
|
21205
|
-
const promptName2 = uniquePromptName(cap, seen);
|
|
21206
|
-
seen.add(promptName2);
|
|
21253
|
+
function capabilityForPrompt(capabilities, name, mode = "commands") {
|
|
21254
|
+
for (const [promptName2, cap] of published(capabilities, mode)) {
|
|
21207
21255
|
if (promptName2 === name)
|
|
21208
21256
|
return cap;
|
|
21209
21257
|
}
|
|
@@ -21230,187 +21278,725 @@ function mentionsArguments(body) {
|
|
|
21230
21278
|
return /\$ARGUMENTS\b|\$\d\b/.test(body);
|
|
21231
21279
|
}
|
|
21232
21280
|
|
|
21233
|
-
// src/
|
|
21234
|
-
|
|
21235
|
-
var VERSION = "0.1.0";
|
|
21236
|
-
var INSTRUCTIONS = `This server is a capability router. Instead of loading every
|
|
21237
|
-
available tool into your context, it exposes a search interface over all of them.
|
|
21281
|
+
// src/cli/adopt.ts
|
|
21282
|
+
import { join as join16 } from "node:path";
|
|
21238
21283
|
|
|
21239
|
-
|
|
21240
|
-
|
|
21241
|
-
|
|
21242
|
-
|
|
21243
|
-
|
|
21244
|
-
|
|
21245
|
-
|
|
21246
|
-
|
|
21247
|
-
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
Search before concluding that something is impossible: the catalog covers many
|
|
21252
|
-
servers, skills and plugin commands that are not visible in your tool list.`;
|
|
21253
|
-
async function serve(opts = {}) {
|
|
21254
|
-
const router = await Router.create({ cwd: opts.cwd });
|
|
21255
|
-
const server = new Server({ name: NAME, version: VERSION }, {
|
|
21256
|
-
capabilities: {
|
|
21257
|
-
tools: { listChanged: true },
|
|
21258
|
-
prompts: { listChanged: true }
|
|
21259
|
-
},
|
|
21260
|
-
instructions: INSTRUCTIONS
|
|
21261
|
-
});
|
|
21262
|
-
let profile = null;
|
|
21263
|
-
const activated = new Map;
|
|
21264
|
-
server.oninitialized = () => {
|
|
21265
|
-
profile = profileClient(server.getClientVersion(), server.getClientCapabilities());
|
|
21266
|
-
log(`client: ${profile.name} ${profile.version} — ${profile.rationale}`);
|
|
21267
|
-
};
|
|
21268
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21269
|
-
refreshAndAnnounce();
|
|
21270
|
-
const tools = routerTools(router, profile);
|
|
21271
|
-
for (const [name, cap] of activated)
|
|
21272
|
-
tools.push(promotedTool(name, cap));
|
|
21273
|
-
return { tools };
|
|
21274
|
-
});
|
|
21275
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
21276
|
-
refreshAndAnnounce();
|
|
21277
|
-
return { prompts: promptList(router.catalog.capabilities) };
|
|
21278
|
-
});
|
|
21279
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
21280
|
-
const { name, arguments: args = {} } = request.params;
|
|
21281
|
-
if (await router.isStale())
|
|
21282
|
-
await router.refresh();
|
|
21283
|
-
return await handleGetPrompt(router, name, args);
|
|
21284
|
-
});
|
|
21285
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
21286
|
-
const { name, arguments: args = {} } = request.params;
|
|
21287
|
-
try {
|
|
21288
|
-
switch (name) {
|
|
21289
|
-
case "find_capabilities":
|
|
21290
|
-
if (await router.isStale())
|
|
21291
|
-
await router.refresh();
|
|
21292
|
-
return await handleFind(router, server, profile, activated, args);
|
|
21293
|
-
case "describe_capability":
|
|
21294
|
-
return await handleDescribe(router, args);
|
|
21295
|
-
case "call_capability":
|
|
21296
|
-
return await handleCall(router, args);
|
|
21297
|
-
case "activate_capabilities":
|
|
21298
|
-
return await handleActivate(router, server, activated, args);
|
|
21299
|
-
case "deactivate_capabilities":
|
|
21300
|
-
return await handleDeactivate(server, activated, args);
|
|
21301
|
-
default: {
|
|
21302
|
-
const cap = activated.get(name);
|
|
21303
|
-
if (cap)
|
|
21304
|
-
return await invoke(router, cap, args, true);
|
|
21305
|
-
return errorResult(`Unknown tool: ${name}. Use find_capabilities to search.`);
|
|
21306
|
-
}
|
|
21307
|
-
}
|
|
21308
|
-
} catch (err) {
|
|
21309
|
-
return errorResult(err instanceof Error ? err.message : String(err));
|
|
21310
|
-
}
|
|
21311
|
-
});
|
|
21312
|
-
async function refreshAndAnnounce() {
|
|
21313
|
-
const before = router.summary();
|
|
21314
|
-
await router.refresh();
|
|
21315
|
-
if (router.summary() === before)
|
|
21316
|
-
return;
|
|
21317
|
-
log(`catalog changed — ${router.summary()}`);
|
|
21318
|
-
server.sendToolListChanged?.();
|
|
21319
|
-
server.sendPromptListChanged?.();
|
|
21284
|
+
// src/config/write.ts
|
|
21285
|
+
import { join as join14 } from "node:path";
|
|
21286
|
+
function routerConfigPath(existingPath) {
|
|
21287
|
+
return existingPath ?? join14(homeDir(), ".config", "autorouter", "config.json");
|
|
21288
|
+
}
|
|
21289
|
+
async function read(path) {
|
|
21290
|
+
if (!await exists(path))
|
|
21291
|
+
return {};
|
|
21292
|
+
try {
|
|
21293
|
+
return JSON.parse(stripJsonComments(await readText(path) ?? "{}"));
|
|
21294
|
+
} catch {
|
|
21295
|
+
throw new Error(`${path} is not valid JSON — fix it before writing to it.`);
|
|
21320
21296
|
}
|
|
21321
|
-
const transport = new StdioServerTransport;
|
|
21322
|
-
await server.connect(transport);
|
|
21323
|
-
log(`ready — ${router.summary()}`);
|
|
21324
|
-
await new Promise((resolve2) => {
|
|
21325
|
-
let done = false;
|
|
21326
|
-
const finish = () => {
|
|
21327
|
-
if (done)
|
|
21328
|
-
return;
|
|
21329
|
-
done = true;
|
|
21330
|
-
router.close().catch(() => {});
|
|
21331
|
-
resolve2();
|
|
21332
|
-
};
|
|
21333
|
-
server.onclose = finish;
|
|
21334
|
-
transport.onclose = finish;
|
|
21335
|
-
process.on("SIGINT", finish);
|
|
21336
|
-
process.on("SIGTERM", finish);
|
|
21337
|
-
process.stdin.on("end", finish);
|
|
21338
|
-
});
|
|
21339
21297
|
}
|
|
21340
|
-
function
|
|
21341
|
-
const
|
|
21342
|
-
|
|
21343
|
-
|
|
21344
|
-
|
|
21345
|
-
|
|
21346
|
-
|
|
21347
|
-
|
|
21348
|
-
|
|
21349
|
-
|
|
21350
|
-
|
|
21351
|
-
|
|
21352
|
-
|
|
21353
|
-
|
|
21354
|
-
|
|
21355
|
-
|
|
21356
|
-
|
|
21357
|
-
|
|
21358
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
|
|
21375
|
-
|
|
21376
|
-
|
|
21377
|
-
|
|
21378
|
-
|
|
21379
|
-
arguments: { type: "object", description: "Arguments matching the capability's input schema." },
|
|
21380
|
-
confirm: {
|
|
21381
|
-
type: "boolean",
|
|
21382
|
-
description: "Set true to proceed with a capability that requires explicit confirmation."
|
|
21383
|
-
}
|
|
21384
|
-
},
|
|
21385
|
-
required: ["id"]
|
|
21386
|
-
}
|
|
21298
|
+
async function upsertServers(servers, opts) {
|
|
21299
|
+
const path = routerConfigPath(opts.configPath);
|
|
21300
|
+
await ensureDir(join14(path, ".."));
|
|
21301
|
+
const config2 = await read(path);
|
|
21302
|
+
config2.servers = opts.overwrite ? { ...config2.servers, ...servers } : { ...servers, ...config2.servers };
|
|
21303
|
+
await writeText(path, `${JSON.stringify(config2, null, 2)}
|
|
21304
|
+
`);
|
|
21305
|
+
return path;
|
|
21306
|
+
}
|
|
21307
|
+
async function removeServer(name, configPath) {
|
|
21308
|
+
const path = routerConfigPath(configPath);
|
|
21309
|
+
const config2 = await read(path);
|
|
21310
|
+
if (!config2.servers || !(name in config2.servers))
|
|
21311
|
+
return { removed: false, path };
|
|
21312
|
+
delete config2.servers[name];
|
|
21313
|
+
await writeText(path, `${JSON.stringify(config2, null, 2)}
|
|
21314
|
+
`);
|
|
21315
|
+
return { removed: true, path };
|
|
21316
|
+
}
|
|
21317
|
+
|
|
21318
|
+
// src/cli/adoptExtras.ts
|
|
21319
|
+
import { join as join15 } from "node:path";
|
|
21320
|
+
function settingsPath() {
|
|
21321
|
+
return join15(homeDir(), ".claude", "settings.json");
|
|
21322
|
+
}
|
|
21323
|
+
async function planExtras(opts) {
|
|
21324
|
+
const file = settingsPath();
|
|
21325
|
+
const settings = await readSettings(file) ?? {};
|
|
21326
|
+
const plan = { file, skills: [], plugins: [], skipped: [] };
|
|
21327
|
+
const keepSkills = new Set(opts.keepSkills);
|
|
21328
|
+
const keepPlugins = new Set(opts.keepPlugins);
|
|
21329
|
+
const plugins = await listInstalledPlugins();
|
|
21330
|
+
const pluginByName = new Map(plugins.map((p) => [p.name, p]));
|
|
21331
|
+
const disabledPluginNames = new Set;
|
|
21332
|
+
const untouched = new Set;
|
|
21333
|
+
for (const plugin of plugins) {
|
|
21334
|
+
if (keepPlugins.has(plugin.name) || keepPlugins.has(plugin.key)) {
|
|
21335
|
+
untouched.add(plugin.name);
|
|
21336
|
+
continue;
|
|
21387
21337
|
}
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
|
|
21401
|
-
|
|
21402
|
-
|
|
21403
|
-
|
|
21404
|
-
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
}
|
|
21338
|
+
if (settings.enabledPlugins?.[plugin.key] === false) {
|
|
21339
|
+
plan.skipped.push(`plugin ${plugin.name} (already disabled)`);
|
|
21340
|
+
disabledPluginNames.add(plugin.name);
|
|
21341
|
+
continue;
|
|
21342
|
+
}
|
|
21343
|
+
const servers = await pluginServerNames(plugin.root);
|
|
21344
|
+
const broken = servers.map((s) => opts.unreachable?.has(`${plugin.name}:${s}`) ? `${plugin.name}:${s}` : opts.unreachable?.has(s) ? s : null).filter((s) => s !== null);
|
|
21345
|
+
if (broken.length) {
|
|
21346
|
+
plan.skipped.push(`plugin ${plugin.name} (the router cannot reach ${broken.join(", ")} — run: ${broken.map((s) => `autorouter login ${s}`).join("; ")})`);
|
|
21347
|
+
untouched.add(plugin.name);
|
|
21348
|
+
continue;
|
|
21349
|
+
}
|
|
21350
|
+
const unrouted = servers.filter((s) => !opts.routedServers.has(s) && !opts.routedServers.has(`${plugin.name}:${s}`));
|
|
21351
|
+
if (unrouted.length) {
|
|
21352
|
+
plan.skipped.push(`plugin ${plugin.name} (${unrouted.length > 1 ? `its servers ${unrouted.join(", ")} are` : `its server ${unrouted[0]} is`} not routed yet — adopt ${unrouted.length > 1 ? "them" : "it"} first)`);
|
|
21353
|
+
continue;
|
|
21354
|
+
}
|
|
21355
|
+
plan.plugins.push({
|
|
21356
|
+
id: plugin.key,
|
|
21357
|
+
name: plugin.name,
|
|
21358
|
+
reason: servers.length ? `${servers.length} server(s) already routed` : "skills only"
|
|
21409
21359
|
});
|
|
21360
|
+
disabledPluginNames.add(plugin.name);
|
|
21410
21361
|
}
|
|
21411
|
-
|
|
21362
|
+
const { capabilities } = await collectSkills(opts.skillPaths, opts.cwd, { includePlugins: true });
|
|
21363
|
+
for (const cap of capabilities) {
|
|
21364
|
+
if (cap.server && (disabledPluginNames.has(cap.server) || untouched.has(cap.server)))
|
|
21365
|
+
continue;
|
|
21366
|
+
if (keepSkills.has(cap.name))
|
|
21367
|
+
continue;
|
|
21368
|
+
const current = settings.skillOverrides?.[cap.name];
|
|
21369
|
+
if (current === "off" || current === "user-invocable-only") {
|
|
21370
|
+
plan.skipped.push(`skill ${cap.name} (already ${current})`);
|
|
21371
|
+
continue;
|
|
21372
|
+
}
|
|
21373
|
+
plan.skills.push({ name: cap.name, mode: opts.mode, from: cap.server ?? null });
|
|
21374
|
+
}
|
|
21375
|
+
return plan;
|
|
21412
21376
|
}
|
|
21413
|
-
function
|
|
21377
|
+
async function applyExtras(plan) {
|
|
21378
|
+
const settings = await readSettings(plan.file) ?? {};
|
|
21379
|
+
settings.skillOverrides = { ...settings.skillOverrides };
|
|
21380
|
+
settings.enabledPlugins = { ...settings.enabledPlugins };
|
|
21381
|
+
for (const s of plan.skills)
|
|
21382
|
+
settings.skillOverrides[s.name] = s.mode;
|
|
21383
|
+
for (const p of plan.plugins)
|
|
21384
|
+
settings.enabledPlugins[p.id] = false;
|
|
21385
|
+
await writeText(plan.file, `${JSON.stringify(settings, null, 2)}
|
|
21386
|
+
`);
|
|
21387
|
+
}
|
|
21388
|
+
async function readSettings(file) {
|
|
21389
|
+
if (!await exists(file))
|
|
21390
|
+
return null;
|
|
21391
|
+
const text = await readText(file);
|
|
21392
|
+
if (!text?.trim())
|
|
21393
|
+
return null;
|
|
21394
|
+
try {
|
|
21395
|
+
return JSON.parse(stripJsonComments(text));
|
|
21396
|
+
} catch {
|
|
21397
|
+
return null;
|
|
21398
|
+
}
|
|
21399
|
+
}
|
|
21400
|
+
async function pluginServerNames(root) {
|
|
21401
|
+
const manifestText = await readText(join15(root, ".claude-plugin", "plugin.json"));
|
|
21402
|
+
if (!manifestText)
|
|
21403
|
+
return [];
|
|
21404
|
+
let manifest;
|
|
21405
|
+
try {
|
|
21406
|
+
manifest = JSON.parse(stripJsonComments(manifestText));
|
|
21407
|
+
} catch {
|
|
21408
|
+
return [];
|
|
21409
|
+
}
|
|
21410
|
+
if (!manifest.mcpServers)
|
|
21411
|
+
return [];
|
|
21412
|
+
if (typeof manifest.mcpServers !== "string")
|
|
21413
|
+
return Object.keys(manifest.mcpServers);
|
|
21414
|
+
const rel = manifest.mcpServers.replace(/^\.\//, "");
|
|
21415
|
+
const text = await readText(join15(root, rel));
|
|
21416
|
+
if (!text)
|
|
21417
|
+
return [];
|
|
21418
|
+
try {
|
|
21419
|
+
const loaded = JSON.parse(stripJsonComments(text));
|
|
21420
|
+
return Object.keys(loaded.mcpServers ?? loaded ?? {});
|
|
21421
|
+
} catch {
|
|
21422
|
+
return [];
|
|
21423
|
+
}
|
|
21424
|
+
}
|
|
21425
|
+
|
|
21426
|
+
// src/cli/adopt.ts
|
|
21427
|
+
async function runAdopt(opts) {
|
|
21428
|
+
const resolved = await resolveConfig(opts.cwd);
|
|
21429
|
+
const keep = new Set([
|
|
21430
|
+
"autorouter",
|
|
21431
|
+
...opts.keep,
|
|
21432
|
+
...serversBehind(resolved.config.alwaysExpose)
|
|
21433
|
+
]);
|
|
21434
|
+
const plans = [];
|
|
21435
|
+
const adopted = {};
|
|
21436
|
+
const backup = {};
|
|
21437
|
+
const docs = new Map;
|
|
21438
|
+
const apply = (site, entries) => {
|
|
21439
|
+
for (const [name, spec] of Object.entries(entries)) {
|
|
21440
|
+
if (keep.has(name)) {
|
|
21441
|
+
site.kept.push(name);
|
|
21442
|
+
continue;
|
|
21443
|
+
}
|
|
21444
|
+
site.moved.push(name);
|
|
21445
|
+
adopted[name] ??= spec;
|
|
21446
|
+
delete entries[name];
|
|
21447
|
+
}
|
|
21448
|
+
};
|
|
21449
|
+
if (opts.harness === "codex") {
|
|
21450
|
+
const file = join16(homeDir(), ".codex", "config.toml");
|
|
21451
|
+
const text = await readText(file);
|
|
21452
|
+
if (text) {
|
|
21453
|
+
let config2;
|
|
21454
|
+
try {
|
|
21455
|
+
config2 = parse(text);
|
|
21456
|
+
} catch {
|
|
21457
|
+
config2 = null;
|
|
21458
|
+
}
|
|
21459
|
+
if (config2) {
|
|
21460
|
+
const plan = { harness: "codex", file, section: "mcp_servers", moved: [], kept: [] };
|
|
21461
|
+
backup[file] = text;
|
|
21462
|
+
docs.set(file, config2);
|
|
21463
|
+
apply(plan, config2.mcp_servers ?? {});
|
|
21464
|
+
plans.push(plan);
|
|
21465
|
+
}
|
|
21466
|
+
}
|
|
21467
|
+
} else {
|
|
21468
|
+
for (const site of jsonSites(opts.harness, opts.cwd)) {
|
|
21469
|
+
if (!docs.has(site.file)) {
|
|
21470
|
+
const text = await readText(site.file);
|
|
21471
|
+
if (!text)
|
|
21472
|
+
continue;
|
|
21473
|
+
try {
|
|
21474
|
+
docs.set(site.file, JSON.parse(stripJsonComments(text)));
|
|
21475
|
+
} catch {
|
|
21476
|
+
continue;
|
|
21477
|
+
}
|
|
21478
|
+
backup[site.file] = text;
|
|
21479
|
+
}
|
|
21480
|
+
const config2 = docs.get(site.file);
|
|
21481
|
+
const container = site.path.reduce((acc, k) => acc?.[k], config2);
|
|
21482
|
+
if (!container || typeof container !== "object")
|
|
21483
|
+
continue;
|
|
21484
|
+
const plan = {
|
|
21485
|
+
harness: opts.harness,
|
|
21486
|
+
file: site.file,
|
|
21487
|
+
section: site.section,
|
|
21488
|
+
moved: [],
|
|
21489
|
+
kept: []
|
|
21490
|
+
};
|
|
21491
|
+
apply(plan, container);
|
|
21492
|
+
plans.push(plan);
|
|
21493
|
+
}
|
|
21494
|
+
}
|
|
21495
|
+
const routedServers = new Set([
|
|
21496
|
+
...resolved.servers.map((s) => s.name),
|
|
21497
|
+
...resolved.servers.flatMap((s) => s.name.includes(":") ? [s.name.split(":").pop()] : []),
|
|
21498
|
+
...Object.keys(adopted)
|
|
21499
|
+
]);
|
|
21500
|
+
const extras = opts.extras && opts.harness === "claude" ? await planExtras({
|
|
21501
|
+
cwd: opts.cwd,
|
|
21502
|
+
skillPaths: resolved.config.skillPaths,
|
|
21503
|
+
keepSkills: opts.keepSkills ?? [],
|
|
21504
|
+
keepPlugins: opts.keepPlugins ?? [],
|
|
21505
|
+
mode: opts.skillMode ?? "user-invocable-only",
|
|
21506
|
+
routedServers,
|
|
21507
|
+
unreachable: opts.unreachable
|
|
21508
|
+
}) : null;
|
|
21509
|
+
const extrasCount = (extras?.skills.length ?? 0) + (extras?.plugins.length ?? 0);
|
|
21510
|
+
const movedCount = plans.reduce((n, p) => n + p.moved.length, 0);
|
|
21511
|
+
if (!movedCount && !extrasCount) {
|
|
21512
|
+
return {
|
|
21513
|
+
plans,
|
|
21514
|
+
extras,
|
|
21515
|
+
backup: null,
|
|
21516
|
+
notes: ["Nothing to adopt — no downstream servers, skills or plugins are loaded directly by this harness."]
|
|
21517
|
+
};
|
|
21518
|
+
}
|
|
21519
|
+
if (opts.dryRun)
|
|
21520
|
+
return { plans, extras, backup: null, notes: ["Dry run — no files were changed."] };
|
|
21521
|
+
if (extrasCount) {
|
|
21522
|
+
const text = await readText(settingsPath());
|
|
21523
|
+
if (text !== null)
|
|
21524
|
+
backup[settingsPath()] = text;
|
|
21525
|
+
}
|
|
21526
|
+
const backupPath = join16(backupDir(), `${opts.harness}-${stamp()}.json`);
|
|
21527
|
+
await ensureDir(join16(backupPath, ".."));
|
|
21528
|
+
await writeText(backupPath, `${JSON.stringify({ harness: opts.harness, files: backup }, null, 2)}
|
|
21529
|
+
`);
|
|
21530
|
+
const touched = new Set(plans.filter((p) => p.moved.length).map((p) => p.file));
|
|
21531
|
+
for (const file of touched) {
|
|
21532
|
+
const config2 = docs.get(file);
|
|
21533
|
+
await writeText(file, file.endsWith(".toml") ? stringify(config2) : `${JSON.stringify(config2, null, 2)}
|
|
21534
|
+
`);
|
|
21535
|
+
}
|
|
21536
|
+
if (movedCount)
|
|
21537
|
+
await upsertServers(adopted, { configPath: resolved.configPath });
|
|
21538
|
+
if (extras && extrasCount)
|
|
21539
|
+
await applyExtras(extras);
|
|
21540
|
+
const notes = [];
|
|
21541
|
+
if (movedCount)
|
|
21542
|
+
notes.push(`Moved ${movedCount} server(s) into the router's config.`);
|
|
21543
|
+
if (extras?.skills.length) {
|
|
21544
|
+
notes.push(`Hid ${extras.skills.length} skill(s) from the model` + (extras.skills[0]?.mode === "user-invocable-only" ? " — /name still works for you." : "."));
|
|
21545
|
+
}
|
|
21546
|
+
if (extras?.plugins.length)
|
|
21547
|
+
notes.push(`Disabled ${extras.plugins.length} plugin(s).`);
|
|
21548
|
+
notes.push(`Backup at ${backupPath}`);
|
|
21549
|
+
notes.push("Restart the harness — its tool list should now show only the router.");
|
|
21550
|
+
return { plans, extras, backup: backupPath, notes };
|
|
21551
|
+
}
|
|
21552
|
+
var HARNESSES = ["claude", "codex", "cursor", "vscode"];
|
|
21553
|
+
async function runAutoAdopt(cwd) {
|
|
21554
|
+
const resolved = await resolveConfig(cwd);
|
|
21555
|
+
if (resolved.config.autoAdopt === false)
|
|
21556
|
+
return [];
|
|
21557
|
+
const notes = [];
|
|
21558
|
+
for (const harness of HARNESSES) {
|
|
21559
|
+
if (!resolved.config.import.includes(harness))
|
|
21560
|
+
continue;
|
|
21561
|
+
try {
|
|
21562
|
+
const result = await runAdopt({ harness, cwd, keep: [], dryRun: false, extras: false });
|
|
21563
|
+
const moved = result.plans.flatMap((p) => p.moved);
|
|
21564
|
+
if (moved.length)
|
|
21565
|
+
notes.push(`adopted ${moved.join(", ")} from ${harness}`);
|
|
21566
|
+
} catch {}
|
|
21567
|
+
}
|
|
21568
|
+
return notes;
|
|
21569
|
+
}
|
|
21570
|
+
function backupDir() {
|
|
21571
|
+
return join16(homeDir(), ".autorouter", "adopted");
|
|
21572
|
+
}
|
|
21573
|
+
function stamp() {
|
|
21574
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
21575
|
+
}
|
|
21576
|
+
async function runRestore(harness) {
|
|
21577
|
+
const dir = backupDir();
|
|
21578
|
+
const { readdir: readdir2 } = await import("node:fs/promises");
|
|
21579
|
+
let files;
|
|
21580
|
+
try {
|
|
21581
|
+
files = (await readdir2(dir)).filter((f) => f.startsWith(`${harness}-`)).sort();
|
|
21582
|
+
} catch {
|
|
21583
|
+
return [`No adoption backups found for ${harness}.`];
|
|
21584
|
+
}
|
|
21585
|
+
const latest = files.at(-1);
|
|
21586
|
+
if (!latest)
|
|
21587
|
+
return [`No adoption backups found for ${harness}.`];
|
|
21588
|
+
const payload = JSON.parse(await readText(join16(dir, latest)));
|
|
21589
|
+
const notes = [];
|
|
21590
|
+
for (const [file, text] of Object.entries(payload.files)) {
|
|
21591
|
+
await writeText(file, text);
|
|
21592
|
+
notes.push(`Restored ${file}`);
|
|
21593
|
+
}
|
|
21594
|
+
notes.push(`From ${join16(dir, latest)}. The router's own config still lists these servers; that is harmless (duplicates are deduped) but you can remove them.`);
|
|
21595
|
+
return notes;
|
|
21596
|
+
}
|
|
21597
|
+
function jsonSites(harness, cwd) {
|
|
21598
|
+
switch (harness) {
|
|
21599
|
+
case "claude":
|
|
21600
|
+
return [
|
|
21601
|
+
{ file: join16(homeDir(), ".claude.json"), section: "mcpServers", path: ["mcpServers"] },
|
|
21602
|
+
{
|
|
21603
|
+
file: join16(homeDir(), ".claude.json"),
|
|
21604
|
+
section: `projects[${cwd}].mcpServers`,
|
|
21605
|
+
path: ["projects", cwd, "mcpServers"]
|
|
21606
|
+
},
|
|
21607
|
+
{ file: join16(cwd, ".mcp.json"), section: "mcpServers", path: ["mcpServers"] }
|
|
21608
|
+
];
|
|
21609
|
+
case "cursor":
|
|
21610
|
+
return [
|
|
21611
|
+
{ file: join16(homeDir(), ".cursor", "mcp.json"), section: "mcpServers", path: ["mcpServers"] },
|
|
21612
|
+
{ file: join16(cwd, ".cursor", "mcp.json"), section: "mcpServers", path: ["mcpServers"] }
|
|
21613
|
+
];
|
|
21614
|
+
case "vscode":
|
|
21615
|
+
return [
|
|
21616
|
+
{ file: join16(homeDir(), ".vscode", "mcp.json"), section: "servers", path: ["servers"] },
|
|
21617
|
+
{ file: join16(cwd, ".vscode", "mcp.json"), section: "servers", path: ["servers"] }
|
|
21618
|
+
];
|
|
21619
|
+
default:
|
|
21620
|
+
return [];
|
|
21621
|
+
}
|
|
21622
|
+
}
|
|
21623
|
+
function serversBehind(patterns) {
|
|
21624
|
+
const out = [];
|
|
21625
|
+
for (const p of patterns) {
|
|
21626
|
+
const m = /^(?:mcp:)?([^:/.*]+)[/.]/.exec(p);
|
|
21627
|
+
if (m?.[1])
|
|
21628
|
+
out.push(m[1]);
|
|
21629
|
+
}
|
|
21630
|
+
return out;
|
|
21631
|
+
}
|
|
21632
|
+
|
|
21633
|
+
// src/cli/add.ts
|
|
21634
|
+
function parseAddSpec(spec) {
|
|
21635
|
+
if (spec.json) {
|
|
21636
|
+
let parsed;
|
|
21637
|
+
try {
|
|
21638
|
+
parsed = JSON.parse(spec.json);
|
|
21639
|
+
} catch (err) {
|
|
21640
|
+
throw new Error(`--json is not valid JSON: ${err instanceof Error ? err.message : err}`);
|
|
21641
|
+
}
|
|
21642
|
+
const servers = parsed?.mcpServers ?? parsed?.servers;
|
|
21643
|
+
if (servers && typeof servers === "object") {
|
|
21644
|
+
const entries = Object.entries(servers);
|
|
21645
|
+
if (entries.length !== 1 && !spec.name) {
|
|
21646
|
+
throw new Error(`--json holds ${entries.length} servers; name the one you want: autorouter add <name> --json '…'`);
|
|
21647
|
+
}
|
|
21648
|
+
const picked = spec.name ? entries.find(([n]) => n === spec.name) : entries[0];
|
|
21649
|
+
if (!picked)
|
|
21650
|
+
throw new Error(`--json has no server named "${spec.name}".`);
|
|
21651
|
+
return { name: spec.name ?? picked[0], raw: picked[1] };
|
|
21652
|
+
}
|
|
21653
|
+
if (!spec.name)
|
|
21654
|
+
throw new Error("A name is required: autorouter add <name> --json '…'");
|
|
21655
|
+
return { name: spec.name, raw: parsed };
|
|
21656
|
+
}
|
|
21657
|
+
if (!spec.name)
|
|
21658
|
+
throw new Error("A name is required: autorouter add <name> --url … | --command …");
|
|
21659
|
+
if (!spec.url && !spec.command) {
|
|
21660
|
+
throw new Error(`Nothing to register: pass --url, --command, or -- <command> <args…>`);
|
|
21661
|
+
}
|
|
21662
|
+
return {
|
|
21663
|
+
name: spec.name,
|
|
21664
|
+
raw: {
|
|
21665
|
+
...spec.url ? { url: spec.url, headers: spec.headers } : {},
|
|
21666
|
+
...spec.command ? { command: spec.command, args: spec.args ?? [] } : {},
|
|
21667
|
+
...spec.env ? { env: spec.env } : {}
|
|
21668
|
+
}
|
|
21669
|
+
};
|
|
21670
|
+
}
|
|
21671
|
+
function describeSpec(raw) {
|
|
21672
|
+
return raw.url ? raw.url : `${raw.command} ${(raw.args ?? []).join(" ")}`.trim();
|
|
21673
|
+
}
|
|
21674
|
+
async function runAdd(spec, cwd = process.cwd()) {
|
|
21675
|
+
const { name, raw } = parseAddSpec(spec);
|
|
21676
|
+
const target = describeSpec(raw);
|
|
21677
|
+
const entry = normalizeServer(name, raw, "config");
|
|
21678
|
+
if (!entry) {
|
|
21679
|
+
return { ok: false, name, target, message: `Could not read a server out of that: ${target || "(empty)"}` };
|
|
21680
|
+
}
|
|
21681
|
+
if (isSelfReference(entry)) {
|
|
21682
|
+
return {
|
|
21683
|
+
ok: false,
|
|
21684
|
+
name,
|
|
21685
|
+
target,
|
|
21686
|
+
message: `"${target}" launches autorouter itself. Registering the router with the router would recurse forever.`
|
|
21687
|
+
};
|
|
21688
|
+
}
|
|
21689
|
+
const resolved = await resolveConfig(cwd);
|
|
21690
|
+
const path = await upsertServers({ [name]: raw }, {
|
|
21691
|
+
configPath: resolved.configPath,
|
|
21692
|
+
overwrite: true
|
|
21693
|
+
});
|
|
21694
|
+
let client;
|
|
21695
|
+
let capabilities;
|
|
21696
|
+
let failure;
|
|
21697
|
+
try {
|
|
21698
|
+
client = await connect(entry);
|
|
21699
|
+
capabilities = (await enumerateServer(entry, client)).length;
|
|
21700
|
+
} catch (err) {
|
|
21701
|
+
failure = authHint(name, err);
|
|
21702
|
+
} finally {
|
|
21703
|
+
try {
|
|
21704
|
+
await client?.close();
|
|
21705
|
+
} catch {}
|
|
21706
|
+
}
|
|
21707
|
+
if (failure) {
|
|
21708
|
+
return {
|
|
21709
|
+
ok: false,
|
|
21710
|
+
name,
|
|
21711
|
+
target,
|
|
21712
|
+
path,
|
|
21713
|
+
message: `Registered ${name} in ${path}, but it did not answer: ${failure}`
|
|
21714
|
+
};
|
|
21715
|
+
}
|
|
21716
|
+
const router = await Router.create({ cwd, force: true });
|
|
21717
|
+
await router.close();
|
|
21718
|
+
return {
|
|
21719
|
+
ok: true,
|
|
21720
|
+
name,
|
|
21721
|
+
target,
|
|
21722
|
+
path,
|
|
21723
|
+
capabilities,
|
|
21724
|
+
message: `Added ${name} → ${target}
|
|
21725
|
+
${capabilities} capabilities, registered in ${path}`
|
|
21726
|
+
};
|
|
21727
|
+
}
|
|
21728
|
+
async function runRemove(name, cwd = process.cwd()) {
|
|
21729
|
+
const resolved = await resolveConfig(cwd);
|
|
21730
|
+
const { removed, path } = await removeServer(name, resolved.configPath);
|
|
21731
|
+
if (!removed)
|
|
21732
|
+
return `No server named "${name}" in ${path}.`;
|
|
21733
|
+
const stillImported = (await resolveConfig(cwd)).servers.some((s) => s.name === name);
|
|
21734
|
+
return stillImported ? `Removed ${name} from ${path}, but a harness still registers it — it will keep appearing. ` + `Add "${name}/*" to "exclude" to hide it entirely.` : `Removed ${name} from ${path}.`;
|
|
21735
|
+
}
|
|
21736
|
+
|
|
21737
|
+
// src/server/index.ts
|
|
21738
|
+
var NAME = "autorouter";
|
|
21739
|
+
var VERSION = "0.1.0";
|
|
21740
|
+
var INSTRUCTIONS = `This server is a capability router. Instead of loading every
|
|
21741
|
+
available tool into your context, it exposes a search interface over all of them.
|
|
21742
|
+
|
|
21743
|
+
1. find_capabilities({ query }) — say what you are trying to do, in plain
|
|
21744
|
+
language. Top tool hits come back with their input schema attached.
|
|
21745
|
+
2. call_capability({ id, arguments }) — run any hit by its id. A tool you call
|
|
21746
|
+
more than once is promoted into your tool list automatically.
|
|
21747
|
+
3. describe_capability({ id }) — the full schema for a hit whose schema was
|
|
21748
|
+
abbreviated, or a skill's instruction text. Reading those instructions is how
|
|
21749
|
+
a skill runs.
|
|
21750
|
+
|
|
21751
|
+
Search before concluding something is impossible: the catalog is much larger
|
|
21752
|
+
than your tool list.`;
|
|
21753
|
+
async function serve(opts = {}) {
|
|
21754
|
+
const router = await Router.create({ cwd: opts.cwd });
|
|
21755
|
+
const server = new Server({ name: NAME, version: VERSION }, {
|
|
21756
|
+
capabilities: {
|
|
21757
|
+
tools: { listChanged: true },
|
|
21758
|
+
prompts: { listChanged: true }
|
|
21759
|
+
},
|
|
21760
|
+
instructions: INSTRUCTIONS
|
|
21761
|
+
});
|
|
21762
|
+
let profile = null;
|
|
21763
|
+
const activated = new Map;
|
|
21764
|
+
const { promptMode, activation, allowAddServer } = router.resolved.config;
|
|
21765
|
+
let pendingNotes = [];
|
|
21766
|
+
const canPromote = () => Boolean(profile?.supportsListChanged) && activation !== "off";
|
|
21767
|
+
const promoteOnUse = async (cap) => {
|
|
21768
|
+
if (activation !== "lazy" || !canPromote())
|
|
21769
|
+
return null;
|
|
21770
|
+
if (cap.kind !== "tool" || !cap.inputSchema)
|
|
21771
|
+
return null;
|
|
21772
|
+
const [name] = await promote(server, activated, [cap]);
|
|
21773
|
+
return name ?? null;
|
|
21774
|
+
};
|
|
21775
|
+
server.oninitialized = () => {
|
|
21776
|
+
profile = profileClient(server.getClientVersion(), server.getClientCapabilities());
|
|
21777
|
+
log(`client: ${profile.name} ${profile.version} — ${profile.rationale}`);
|
|
21778
|
+
};
|
|
21779
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21780
|
+
refreshAndAnnounce();
|
|
21781
|
+
const tools = routerTools(router, profile, activation, allowAddServer);
|
|
21782
|
+
for (const [name, cap] of activated)
|
|
21783
|
+
tools.push(promotedTool(name, cap));
|
|
21784
|
+
return { tools };
|
|
21785
|
+
});
|
|
21786
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
21787
|
+
refreshAndAnnounce();
|
|
21788
|
+
return { prompts: promptList(router.catalog.capabilities, promptMode) };
|
|
21789
|
+
});
|
|
21790
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
21791
|
+
const { name, arguments: args = {} } = request.params;
|
|
21792
|
+
if (await router.isStale())
|
|
21793
|
+
await router.refresh();
|
|
21794
|
+
return await handleGetPrompt(router, name, args, promptMode);
|
|
21795
|
+
});
|
|
21796
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
21797
|
+
const { name, arguments: args = {} } = request.params;
|
|
21798
|
+
try {
|
|
21799
|
+
switch (name) {
|
|
21800
|
+
case "find_capabilities":
|
|
21801
|
+
if (await router.isStale())
|
|
21802
|
+
await refreshAndAnnounce();
|
|
21803
|
+
return await handleFind(router, server, profile, activated, activation, args, drainNotes());
|
|
21804
|
+
case "describe_capability":
|
|
21805
|
+
return await handleDescribe(router, args);
|
|
21806
|
+
case "call_capability":
|
|
21807
|
+
return await handleCall(router, promoteOnUse, args);
|
|
21808
|
+
case "activate_capabilities":
|
|
21809
|
+
return await handleActivate(router, server, activated, args);
|
|
21810
|
+
case "deactivate_capabilities":
|
|
21811
|
+
return await handleDeactivate(server, activated, args);
|
|
21812
|
+
case "add_server":
|
|
21813
|
+
return await handleAddServer(router, server, args);
|
|
21814
|
+
default: {
|
|
21815
|
+
const cap = activated.get(name);
|
|
21816
|
+
if (cap)
|
|
21817
|
+
return await invoke(router, cap, args, true);
|
|
21818
|
+
return errorResult(`Unknown tool: ${name}. Use find_capabilities to search.`);
|
|
21819
|
+
}
|
|
21820
|
+
}
|
|
21821
|
+
} catch (err) {
|
|
21822
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
21823
|
+
}
|
|
21824
|
+
});
|
|
21825
|
+
let adopting = false;
|
|
21826
|
+
async function maybeAutoAdopt() {
|
|
21827
|
+
if (adopting)
|
|
21828
|
+
return;
|
|
21829
|
+
adopting = true;
|
|
21830
|
+
try {
|
|
21831
|
+
const adopted = await runAutoAdopt(router.resolved.cwd);
|
|
21832
|
+
for (const note of adopted) {
|
|
21833
|
+
log(note);
|
|
21834
|
+
pendingNotes.push(`${note} — restart the harness to reclaim its context.`);
|
|
21835
|
+
}
|
|
21836
|
+
if (adopted.length)
|
|
21837
|
+
await router.refresh({ force: true });
|
|
21838
|
+
} catch {} finally {
|
|
21839
|
+
adopting = false;
|
|
21840
|
+
}
|
|
21841
|
+
}
|
|
21842
|
+
async function refreshAndAnnounce() {
|
|
21843
|
+
const before = router.summary();
|
|
21844
|
+
const wasStale = await router.isStale();
|
|
21845
|
+
await router.refresh();
|
|
21846
|
+
if (wasStale)
|
|
21847
|
+
await maybeAutoAdopt();
|
|
21848
|
+
if (router.summary() === before)
|
|
21849
|
+
return;
|
|
21850
|
+
log(`catalog changed — ${router.summary()}`);
|
|
21851
|
+
server.sendToolListChanged?.();
|
|
21852
|
+
server.sendPromptListChanged?.();
|
|
21853
|
+
}
|
|
21854
|
+
function drainNotes() {
|
|
21855
|
+
const notes = pendingNotes;
|
|
21856
|
+
pendingNotes = [];
|
|
21857
|
+
return notes;
|
|
21858
|
+
}
|
|
21859
|
+
const transport = new StdioServerTransport;
|
|
21860
|
+
await server.connect(transport);
|
|
21861
|
+
log(`ready — ${router.summary()}`);
|
|
21862
|
+
await new Promise((resolve2) => {
|
|
21863
|
+
let done = false;
|
|
21864
|
+
const finish = () => {
|
|
21865
|
+
if (done)
|
|
21866
|
+
return;
|
|
21867
|
+
done = true;
|
|
21868
|
+
router.close().catch(() => {});
|
|
21869
|
+
resolve2();
|
|
21870
|
+
};
|
|
21871
|
+
server.onclose = finish;
|
|
21872
|
+
transport.onclose = finish;
|
|
21873
|
+
process.on("SIGINT", finish);
|
|
21874
|
+
process.on("SIGTERM", finish);
|
|
21875
|
+
process.stdin.on("end", finish);
|
|
21876
|
+
});
|
|
21877
|
+
}
|
|
21878
|
+
function routerTools(router, profile, activation, allowAddServer) {
|
|
21879
|
+
const tools = [
|
|
21880
|
+
{
|
|
21881
|
+
name: "find_capabilities",
|
|
21882
|
+
description: `Search every available tool, skill, prompt and command by what you are trying to do, and get back only the ones that fit. Catalog: ${router.summary()}. Use this whenever a task might need a capability you cannot already see — searching is cheap and the catalog is much larger than your tool list.`,
|
|
21883
|
+
inputSchema: {
|
|
21884
|
+
type: "object",
|
|
21885
|
+
properties: {
|
|
21886
|
+
query: {
|
|
21887
|
+
type: "string",
|
|
21888
|
+
description: "What you are trying to accomplish, in plain language. Describe the goal, not a guessed tool name."
|
|
21889
|
+
},
|
|
21890
|
+
kind: {
|
|
21891
|
+
type: "string",
|
|
21892
|
+
enum: ["tool", "skill", "prompt", "resource", "command", "agent"],
|
|
21893
|
+
description: "Optional: restrict results to one kind of capability."
|
|
21894
|
+
},
|
|
21895
|
+
server: { type: "string", description: "Optional: restrict to one provider." },
|
|
21896
|
+
limit: { type: "number", description: "Max results (default 8)." }
|
|
21897
|
+
},
|
|
21898
|
+
required: ["query"]
|
|
21899
|
+
}
|
|
21900
|
+
},
|
|
21901
|
+
{
|
|
21902
|
+
name: "describe_capability",
|
|
21903
|
+
description: "Get full detail for one capability id from find_capabilities: the complete input schema for a tool, or the full instruction text for a skill or command.",
|
|
21904
|
+
inputSchema: {
|
|
21905
|
+
type: "object",
|
|
21906
|
+
properties: { id: { type: "string", description: "Capability id, e.g. mcp:supabase/execute_sql" } },
|
|
21907
|
+
required: ["id"]
|
|
21908
|
+
}
|
|
21909
|
+
},
|
|
21910
|
+
{
|
|
21911
|
+
name: "call_capability",
|
|
21912
|
+
description: "Run a capability by id and return its result. Works for tools, prompts and resources; skills and commands are executed by following the text from describe_capability instead.",
|
|
21913
|
+
inputSchema: {
|
|
21914
|
+
type: "object",
|
|
21915
|
+
properties: {
|
|
21916
|
+
id: { type: "string", description: "Capability id to invoke." },
|
|
21917
|
+
arguments: { type: "object", description: "Arguments matching the capability's input schema." },
|
|
21918
|
+
confirm: {
|
|
21919
|
+
type: "boolean",
|
|
21920
|
+
description: "Set true to proceed with a capability that requires explicit confirmation."
|
|
21921
|
+
}
|
|
21922
|
+
},
|
|
21923
|
+
required: ["id"]
|
|
21924
|
+
}
|
|
21925
|
+
}
|
|
21926
|
+
];
|
|
21927
|
+
if (profile?.supportsListChanged && activation !== "off") {
|
|
21928
|
+
tools.push({
|
|
21929
|
+
name: "activate_capabilities",
|
|
21930
|
+
description: "Promote capabilities to real, first-class tools in your tool list so you can call them directly with full schema validation. Use after find_capabilities when you expect to call something several times.",
|
|
21931
|
+
inputSchema: {
|
|
21932
|
+
type: "object",
|
|
21933
|
+
properties: {
|
|
21934
|
+
ids: { type: "array", items: { type: "string" }, description: "Capability ids to promote." }
|
|
21935
|
+
},
|
|
21936
|
+
required: ["ids"]
|
|
21937
|
+
}
|
|
21938
|
+
}, {
|
|
21939
|
+
name: "deactivate_capabilities",
|
|
21940
|
+
description: "Remove previously activated capabilities from your tool list to reclaim context.",
|
|
21941
|
+
inputSchema: {
|
|
21942
|
+
type: "object",
|
|
21943
|
+
properties: {
|
|
21944
|
+
ids: { type: "array", items: { type: "string" }, description: "Ids to remove; omit to remove all." }
|
|
21945
|
+
}
|
|
21946
|
+
}
|
|
21947
|
+
});
|
|
21948
|
+
}
|
|
21949
|
+
if (allowAddServer) {
|
|
21950
|
+
tools.push({
|
|
21951
|
+
name: "add_server",
|
|
21952
|
+
description: 'Register a new MCP server behind the router, instead of running `claude mcp add`. Pass url or command+args, or json for a pasted {"mcpServers":{…}} snippet. Rejected without confirm: true.',
|
|
21953
|
+
inputSchema: {
|
|
21954
|
+
type: "object",
|
|
21955
|
+
properties: {
|
|
21956
|
+
name: { type: "string" },
|
|
21957
|
+
url: { type: "string" },
|
|
21958
|
+
command: { type: "string" },
|
|
21959
|
+
args: { type: "array", items: { type: "string" } },
|
|
21960
|
+
env: { type: "object" },
|
|
21961
|
+
headers: { type: "object" },
|
|
21962
|
+
json: { type: "string" },
|
|
21963
|
+
confirm: { type: "boolean" }
|
|
21964
|
+
},
|
|
21965
|
+
required: ["name"]
|
|
21966
|
+
}
|
|
21967
|
+
});
|
|
21968
|
+
}
|
|
21969
|
+
return tools;
|
|
21970
|
+
}
|
|
21971
|
+
async function handleAddServer(router, server, args) {
|
|
21972
|
+
let target;
|
|
21973
|
+
let name;
|
|
21974
|
+
try {
|
|
21975
|
+
const parsed = parseAddSpec(args);
|
|
21976
|
+
name = parsed.name;
|
|
21977
|
+
target = describeSpec(parsed.raw);
|
|
21978
|
+
} catch (err) {
|
|
21979
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
21980
|
+
}
|
|
21981
|
+
if (args.confirm !== true) {
|
|
21982
|
+
return textResult([
|
|
21983
|
+
`Confirmation required before registering "${name}".`,
|
|
21984
|
+
`This machine will ${args.url || target.startsWith("http") ? "connect to" : "execute"}: ${target}`,
|
|
21985
|
+
"",
|
|
21986
|
+
"Show the user that line. If it is what they asked for, call add_server again with confirm: true."
|
|
21987
|
+
].join(`
|
|
21988
|
+
`));
|
|
21989
|
+
}
|
|
21990
|
+
const result = await runAdd(args, router.resolved.cwd);
|
|
21991
|
+
if (!result.ok)
|
|
21992
|
+
return errorResult(result.message);
|
|
21993
|
+
await router.refresh({ force: true });
|
|
21994
|
+
server.sendToolListChanged?.();
|
|
21995
|
+
server.sendPromptListChanged?.();
|
|
21996
|
+
return textResult(`${result.message}
|
|
21997
|
+
Search for them with find_capabilities.`);
|
|
21998
|
+
}
|
|
21999
|
+
function promotedTool(name, cap) {
|
|
21414
22000
|
return {
|
|
21415
22001
|
name,
|
|
21416
22002
|
description: clamp(cap.description || `${cap.kind} ${cap.name}`, PROMOTED_DESC_CHARS),
|
|
@@ -21421,7 +22007,7 @@ var PROMOTED_DESC_CHARS = 320;
|
|
|
21421
22007
|
function promotedCost(name, cap) {
|
|
21422
22008
|
return tokensOf(promotedTool(name, cap));
|
|
21423
22009
|
}
|
|
21424
|
-
async function handleGetPrompt(router, name, args) {
|
|
22010
|
+
async function handleGetPrompt(router, name, args, mode) {
|
|
21425
22011
|
if (name === FIND_PROMPT) {
|
|
21426
22012
|
const query = (args.query ?? "").trim();
|
|
21427
22013
|
if (!query)
|
|
@@ -21440,7 +22026,7 @@ async function handleGetPrompt(router, name, args) {
|
|
|
21440
22026
|
]
|
|
21441
22027
|
};
|
|
21442
22028
|
}
|
|
21443
|
-
const cap = capabilityForPrompt(router.catalog.capabilities, name);
|
|
22029
|
+
const cap = capabilityForPrompt(router.catalog.capabilities, name, mode);
|
|
21444
22030
|
if (!cap)
|
|
21445
22031
|
throw new Error(`Unknown prompt: ${name}`);
|
|
21446
22032
|
const found = await router.describe(cap.id);
|
|
@@ -21451,7 +22037,7 @@ async function handleGetPrompt(router, name, args) {
|
|
|
21451
22037
|
}
|
|
21452
22038
|
var AUTO_ACTIVATE_LIMIT = 5;
|
|
21453
22039
|
var ACTIVE_TOKEN_BUDGET = 3000;
|
|
21454
|
-
async function handleFind(router, server, profile, activated, args) {
|
|
22040
|
+
async function handleFind(router, server, profile, activated, activation, args, notes = []) {
|
|
21455
22041
|
const query = (args.query ?? "").trim();
|
|
21456
22042
|
if (!query)
|
|
21457
22043
|
return errorResult("query is required");
|
|
@@ -21462,587 +22048,301 @@ async function handleFind(router, server, profile, activated, args) {
|
|
|
21462
22048
|
harness: profile?.name,
|
|
21463
22049
|
server_handle: server
|
|
21464
22050
|
});
|
|
21465
|
-
const promoted = profile?.supportsListChanged ? await promote(server, activated, result.hits.map((h) => h.capability)) : [];
|
|
21466
|
-
const
|
|
22051
|
+
const promoted = activation === "eager" && profile?.supportsListChanged ? await promote(server, activated, result.hits.map((h) => h.capability)) : [];
|
|
22052
|
+
const body = renderRouteResult(query, result, { inlineSchemas: promoted.length === 0 });
|
|
22053
|
+
const text = notes.length ? `${notes.map((n) => `[autorouter] ${n}`).join(`
|
|
22054
|
+
`)}
|
|
22055
|
+
|
|
22056
|
+
${body}` : body;
|
|
21467
22057
|
if (!promoted.length)
|
|
21468
22058
|
return textResult(text);
|
|
21469
|
-
return textResult(`${text}
|
|
21470
|
-
|
|
21471
|
-
Now callable directly as: ${promoted.join(", ")}.
|
|
21472
|
-
` + "If they are not yet in your tool list, use call_capability with the ids above — the result is identical.");
|
|
21473
|
-
}
|
|
21474
|
-
async function promote(server, activated, caps) {
|
|
21475
|
-
const names = [];
|
|
21476
|
-
let changed = false;
|
|
21477
|
-
for (const cap of caps) {
|
|
21478
|
-
if (cap.kind !== "tool" || !cap.inputSchema)
|
|
21479
|
-
continue;
|
|
21480
|
-
if (names.length >= AUTO_ACTIVATE_LIMIT)
|
|
21481
|
-
break;
|
|
21482
|
-
const name = exposedName(cap);
|
|
21483
|
-
names.push(name);
|
|
21484
|
-
if (activated.get(name)?.id !== cap.id)
|
|
21485
|
-
changed = true;
|
|
21486
|
-
activated.delete(name);
|
|
21487
|
-
activated.set(name, cap);
|
|
21488
|
-
}
|
|
21489
|
-
if (evictToBudget(activated, names))
|
|
21490
|
-
changed = true;
|
|
21491
|
-
if (changed)
|
|
21492
|
-
await server.sendToolListChanged();
|
|
21493
|
-
return names;
|
|
21494
|
-
}
|
|
21495
|
-
function evictToBudget(activated, keep) {
|
|
21496
|
-
let total = 0;
|
|
21497
|
-
for (const [name, cap] of activated)
|
|
21498
|
-
total += promotedCost(name, cap);
|
|
21499
|
-
if (total <= ACTIVE_TOKEN_BUDGET)
|
|
21500
|
-
return false;
|
|
21501
|
-
const protectedNames = new Set(keep);
|
|
21502
|
-
let changed = false;
|
|
21503
|
-
for (const [name, cap] of activated) {
|
|
21504
|
-
if (total <= ACTIVE_TOKEN_BUDGET)
|
|
21505
|
-
break;
|
|
21506
|
-
if (protectedNames.has(name))
|
|
21507
|
-
continue;
|
|
21508
|
-
total -= promotedCost(name, cap);
|
|
21509
|
-
activated.delete(name);
|
|
21510
|
-
changed = true;
|
|
21511
|
-
}
|
|
21512
|
-
return changed;
|
|
21513
|
-
}
|
|
21514
|
-
async function handleDescribe(router, args) {
|
|
21515
|
-
if (!args.id)
|
|
21516
|
-
return errorResult("id is required");
|
|
21517
|
-
const found = await router.describe(args.id);
|
|
21518
|
-
if (!found) {
|
|
21519
|
-
return errorResult(`No capability with id "${args.id}". Use find_capabilities to search.`);
|
|
21520
|
-
}
|
|
21521
|
-
return textResult(renderCapability(found.capability, found.body));
|
|
21522
|
-
}
|
|
21523
|
-
async function handleCall(router, args) {
|
|
21524
|
-
if (!args.id)
|
|
21525
|
-
return errorResult("id is required");
|
|
21526
|
-
const cap = router.get(args.id);
|
|
21527
|
-
if (!cap)
|
|
21528
|
-
return errorResult(`No capability with id "${args.id}".`);
|
|
21529
|
-
return invoke(router, cap, args.arguments ?? {}, args.confirm === true);
|
|
21530
|
-
}
|
|
21531
|
-
async function invoke(router, cap, args, confirmed) {
|
|
21532
|
-
try {
|
|
21533
|
-
const out = await router.call(cap.id, args, { confirmed });
|
|
21534
|
-
if (out.capability.kind === "skill" || out.capability.kind === "command" || out.capability.kind === "agent") {
|
|
21535
|
-
return { content: out.content, isError: false };
|
|
21536
|
-
}
|
|
21537
|
-
return {
|
|
21538
|
-
content: [{ type: "text", text: `[${out.source}]` }, ...out.content],
|
|
21539
|
-
isError: out.isError,
|
|
21540
|
-
...out.structuredContent ? { structuredContent: out.structuredContent } : {}
|
|
21541
|
-
};
|
|
21542
|
-
} catch (err) {
|
|
21543
|
-
return errorResult(err instanceof Error ? err.message : String(err));
|
|
21544
|
-
}
|
|
21545
|
-
}
|
|
21546
|
-
async function handleActivate(router, server, activated, args) {
|
|
21547
|
-
const ids = args.ids ?? [];
|
|
21548
|
-
if (!ids.length)
|
|
21549
|
-
return errorResult("ids is required");
|
|
21550
|
-
const added = [];
|
|
21551
|
-
const missing = [];
|
|
21552
|
-
for (const id of ids) {
|
|
21553
|
-
const cap = router.get(id);
|
|
21554
|
-
if (!cap || cap.kind !== "tool") {
|
|
21555
|
-
missing.push(id);
|
|
21556
|
-
continue;
|
|
21557
|
-
}
|
|
21558
|
-
const toolName = exposedName(cap);
|
|
21559
|
-
activated.set(toolName, cap);
|
|
21560
|
-
added.push(toolName);
|
|
21561
|
-
}
|
|
21562
|
-
if (added.length)
|
|
21563
|
-
await server.sendToolListChanged();
|
|
21564
|
-
const lines = [
|
|
21565
|
-
added.length ? `Activated as tools: ${added.join(", ")}` : "Nothing activated.",
|
|
21566
|
-
missing.length ? `Not activatable (unknown, or not a tool): ${missing.join(", ")}` : "",
|
|
21567
|
-
added.length ? "If these do not appear in your tool list yet, call them through call_capability instead — the result is identical." : ""
|
|
21568
|
-
].filter(Boolean);
|
|
21569
|
-
return textResult(lines.join(`
|
|
21570
|
-
`));
|
|
21571
|
-
}
|
|
21572
|
-
async function handleDeactivate(server, activated, args) {
|
|
21573
|
-
const before = activated.size;
|
|
21574
|
-
if (!args.ids?.length)
|
|
21575
|
-
activated.clear();
|
|
21576
|
-
else {
|
|
21577
|
-
for (const id of args.ids) {
|
|
21578
|
-
for (const [name, cap] of activated) {
|
|
21579
|
-
if (cap.id === id || name === id)
|
|
21580
|
-
activated.delete(name);
|
|
21581
|
-
}
|
|
21582
|
-
}
|
|
21583
|
-
}
|
|
21584
|
-
if (activated.size !== before)
|
|
21585
|
-
await server.sendToolListChanged();
|
|
21586
|
-
return textResult(`Active tools: ${activated.size} (was ${before}).`);
|
|
21587
|
-
}
|
|
21588
|
-
function exposedName(cap) {
|
|
21589
|
-
const parsed = parseCapabilityId(cap.id);
|
|
21590
|
-
const raw = parsed.server ? `${parsed.server}__${cap.name}` : cap.name;
|
|
21591
|
-
return raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
21592
|
-
}
|
|
21593
|
-
function textResult(text) {
|
|
21594
|
-
return { content: [{ type: "text", text }] };
|
|
21595
|
-
}
|
|
21596
|
-
function errorResult(text) {
|
|
21597
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
21598
|
-
}
|
|
21599
|
-
function log(message) {
|
|
21600
|
-
process.stderr.write(`[autorouter] ${message}
|
|
21601
|
-
`);
|
|
21602
|
-
}
|
|
21603
|
-
|
|
21604
|
-
// src/cli/init.ts
|
|
21605
|
-
import { join as join14 } from "node:path";
|
|
21606
|
-
import { createInterface } from "node:readline/promises";
|
|
21607
|
-
var HARNESS_LABEL = {
|
|
21608
|
-
claude: "Claude Code",
|
|
21609
|
-
codex: "Codex CLI",
|
|
21610
|
-
cursor: "Cursor",
|
|
21611
|
-
vscode: "VS Code / Copilot"
|
|
21612
|
-
};
|
|
21613
|
-
async function runInit(opts) {
|
|
21614
|
-
const notes = [];
|
|
21615
|
-
const env = {};
|
|
21616
|
-
const suggested = await suggestModel(opts.harness);
|
|
21617
|
-
const model = opts.yes ? suggested : await promptModel(opts.harness, suggested);
|
|
21618
|
-
if (model) {
|
|
21619
|
-
env.AUTOROUTER_SELECTOR_MODEL = model;
|
|
21620
|
-
notes.push(`Selector model: ${model}`);
|
|
21621
|
-
} else {
|
|
21622
|
-
notes.push("No selector model set — the router will use MCP sampling if the host offers it, otherwise raw index ranking.");
|
|
21623
|
-
}
|
|
21624
|
-
switch (opts.harness) {
|
|
21625
|
-
case "claude":
|
|
21626
|
-
notes.push(await writeJsonServer(join14(homeDir(), ".claude.json"), "mcpServers", opts, env));
|
|
21627
|
-
break;
|
|
21628
|
-
case "cursor":
|
|
21629
|
-
notes.push(await writeJsonServer(join14(homeDir(), ".cursor", "mcp.json"), "mcpServers", opts, env));
|
|
21630
|
-
break;
|
|
21631
|
-
case "vscode":
|
|
21632
|
-
notes.push(await writeJsonServer(join14(homeDir(), ".vscode", "mcp.json"), "servers", opts, env));
|
|
21633
|
-
break;
|
|
21634
|
-
case "codex":
|
|
21635
|
-
notes.push(await writeCodexServer(opts, env));
|
|
21636
|
-
break;
|
|
21637
|
-
}
|
|
21638
|
-
notes.push(await writePrimer(opts.harness));
|
|
21639
|
-
return notes;
|
|
21640
|
-
}
|
|
21641
|
-
async function writeJsonServer(path, key, opts, env) {
|
|
21642
|
-
await ensureDir(join14(path, ".."));
|
|
21643
|
-
const existing = await readText(path) ?? "{}";
|
|
21644
|
-
let config2;
|
|
21645
|
-
try {
|
|
21646
|
-
config2 = JSON.parse(existing);
|
|
21647
|
-
} catch {
|
|
21648
|
-
return `Could not parse ${path}; add the server manually.`;
|
|
21649
|
-
}
|
|
21650
|
-
config2[key] ??= {};
|
|
21651
|
-
config2[key].autorouter = {
|
|
21652
|
-
command: opts.command,
|
|
21653
|
-
args: opts.args,
|
|
21654
|
-
...Object.keys(env).length ? { env } : {}
|
|
21655
|
-
};
|
|
21656
|
-
await writeText(path, `${JSON.stringify(config2, null, 2)}
|
|
21657
|
-
`);
|
|
21658
|
-
return `Registered autorouter in ${path}`;
|
|
21659
|
-
}
|
|
21660
|
-
async function writeCodexServer(opts, env) {
|
|
21661
|
-
const path = join14(homeDir(), ".codex", "config.toml");
|
|
21662
|
-
await ensureDir(join14(path, ".."));
|
|
21663
|
-
const text = await readText(path) ?? "";
|
|
21664
|
-
let config2;
|
|
21665
|
-
try {
|
|
21666
|
-
config2 = text ? parse(text) : {};
|
|
21667
|
-
} catch {
|
|
21668
|
-
return `Could not parse ${path}; add [mcp_servers.autorouter] manually.`;
|
|
21669
|
-
}
|
|
21670
|
-
config2.mcp_servers ??= {};
|
|
21671
|
-
config2.mcp_servers.autorouter = {
|
|
21672
|
-
command: opts.command,
|
|
21673
|
-
args: opts.args,
|
|
21674
|
-
startup_timeout_sec: 60,
|
|
21675
|
-
...Object.keys(env).length ? { env } : {}
|
|
21676
|
-
};
|
|
21677
|
-
await writeText(path, stringify(config2));
|
|
21678
|
-
return `Registered autorouter in ${path}`;
|
|
21679
|
-
}
|
|
21680
|
-
async function writePrimer(harness) {
|
|
21681
|
-
const targets = {
|
|
21682
|
-
claude: join14(homeDir(), ".claude", "CLAUDE.md"),
|
|
21683
|
-
codex: join14(homeDir(), ".codex", "AGENTS.md"),
|
|
21684
|
-
cursor: join14(process.cwd(), ".cursor", "rules", "autorouter.mdc"),
|
|
21685
|
-
vscode: join14(process.cwd(), ".github", "copilot-instructions.md")
|
|
21686
|
-
};
|
|
21687
|
-
const path = targets[harness];
|
|
21688
|
-
await ensureDir(join14(path, ".."));
|
|
21689
|
-
const current = await readText(path) ?? "";
|
|
21690
|
-
if (current.includes("<!-- autorouter -->"))
|
|
21691
|
-
return `Primer already present in ${path}`;
|
|
21692
|
-
const section = harness === "cursor" ? `---
|
|
21693
|
-
description: Capability router
|
|
21694
|
-
alwaysApply: true
|
|
21695
|
-
---
|
|
21696
|
-
|
|
21697
|
-
${PRIMER}` : PRIMER;
|
|
21698
|
-
await writeText(path, current ? `${current.trimEnd()}
|
|
21699
|
-
|
|
21700
|
-
${section}
|
|
21701
|
-
` : `${section}
|
|
21702
|
-
`);
|
|
21703
|
-
return `Added router primer to ${path}`;
|
|
21704
|
-
}
|
|
21705
|
-
var PRIMER = `<!-- autorouter -->
|
|
21706
|
-
## Capability router
|
|
21707
|
-
|
|
21708
|
-
Most tools, skills and commands on this machine are not in your tool list. They
|
|
21709
|
-
are behind \`find_capabilities\`.
|
|
21710
|
-
|
|
21711
|
-
Before deciding a task cannot be done, or reaching for a manual workaround, call
|
|
21712
|
-
\`find_capabilities({ query: "<what you are trying to do>" })\`. It searches every
|
|
21713
|
-
configured MCP server, skill and plugin command and returns only what fits.
|
|
21714
|
-
Then \`describe_capability\` for the schema, and \`call_capability\` to run it.
|
|
21715
|
-
<!-- /autorouter -->`;
|
|
21716
|
-
async function promptModel(harness, suggested) {
|
|
21717
|
-
if (!process.stdin.isTTY)
|
|
21718
|
-
return suggested;
|
|
21719
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21720
|
-
try {
|
|
21721
|
-
console.log(`
|
|
21722
|
-
The router uses a small model to pick which capabilities fit a request.
|
|
21723
|
-
` + `It should be the cheapest model available in ${HARNESS_LABEL[harness]}.`);
|
|
21724
|
-
const answer = (await rl.question(suggested ? `Selector model [${suggested}] (enter to accept, "none" to skip): ` : "Selector model (blank to skip): ")).trim();
|
|
21725
|
-
if (answer.toLowerCase() === "none")
|
|
21726
|
-
return null;
|
|
21727
|
-
return answer || suggested;
|
|
21728
|
-
} finally {
|
|
21729
|
-
rl.close();
|
|
21730
|
-
}
|
|
21731
|
-
}
|
|
21732
|
-
|
|
21733
|
-
// src/cli/adopt.ts
|
|
21734
|
-
import { join as join16 } from "node:path";
|
|
22059
|
+
return textResult(`${text}
|
|
21735
22060
|
|
|
21736
|
-
|
|
21737
|
-
|
|
21738
|
-
function settingsPath() {
|
|
21739
|
-
return join15(homeDir(), ".claude", "settings.json");
|
|
22061
|
+
Now callable directly as: ${promoted.join(", ")}.
|
|
22062
|
+
` + "If they are not yet in your tool list, use call_capability with the ids above — the result is identical.");
|
|
21740
22063
|
}
|
|
21741
|
-
async function
|
|
21742
|
-
const
|
|
21743
|
-
|
|
21744
|
-
const
|
|
21745
|
-
|
|
21746
|
-
const keepPlugins = new Set(opts.keepPlugins);
|
|
21747
|
-
const plugins = await listInstalledPlugins();
|
|
21748
|
-
const pluginByName = new Map(plugins.map((p) => [p.name, p]));
|
|
21749
|
-
const disabledPluginNames = new Set;
|
|
21750
|
-
const untouched = new Set;
|
|
21751
|
-
for (const plugin of plugins) {
|
|
21752
|
-
if (keepPlugins.has(plugin.name) || keepPlugins.has(plugin.key)) {
|
|
21753
|
-
untouched.add(plugin.name);
|
|
21754
|
-
continue;
|
|
21755
|
-
}
|
|
21756
|
-
if (settings.enabledPlugins?.[plugin.key] === false) {
|
|
21757
|
-
plan.skipped.push(`plugin ${plugin.name} (already disabled)`);
|
|
21758
|
-
disabledPluginNames.add(plugin.name);
|
|
21759
|
-
continue;
|
|
21760
|
-
}
|
|
21761
|
-
const servers = await pluginServerNames(plugin.root);
|
|
21762
|
-
const broken = servers.map((s) => opts.unreachable?.has(`${plugin.name}:${s}`) ? `${plugin.name}:${s}` : opts.unreachable?.has(s) ? s : null).filter((s) => s !== null);
|
|
21763
|
-
if (broken.length) {
|
|
21764
|
-
plan.skipped.push(`plugin ${plugin.name} (the router cannot reach ${broken.join(", ")} — run: ${broken.map((s) => `autorouter login ${s}`).join("; ")})`);
|
|
21765
|
-
untouched.add(plugin.name);
|
|
21766
|
-
continue;
|
|
21767
|
-
}
|
|
21768
|
-
const unrouted = servers.filter((s) => !opts.routedServers.has(s) && !opts.routedServers.has(`${plugin.name}:${s}`));
|
|
21769
|
-
if (unrouted.length) {
|
|
21770
|
-
plan.skipped.push(`plugin ${plugin.name} (${unrouted.length > 1 ? `its servers ${unrouted.join(", ")} are` : `its server ${unrouted[0]} is`} not routed yet — adopt ${unrouted.length > 1 ? "them" : "it"} first)`);
|
|
21771
|
-
continue;
|
|
21772
|
-
}
|
|
21773
|
-
plan.plugins.push({
|
|
21774
|
-
id: plugin.key,
|
|
21775
|
-
name: plugin.name,
|
|
21776
|
-
reason: servers.length ? `${servers.length} server(s) already routed` : "skills only"
|
|
21777
|
-
});
|
|
21778
|
-
disabledPluginNames.add(plugin.name);
|
|
21779
|
-
}
|
|
21780
|
-
const { capabilities } = await collectSkills(opts.skillPaths, opts.cwd, { includePlugins: true });
|
|
21781
|
-
for (const cap of capabilities) {
|
|
21782
|
-
if (cap.server && (disabledPluginNames.has(cap.server) || untouched.has(cap.server)))
|
|
21783
|
-
continue;
|
|
21784
|
-
if (keepSkills.has(cap.name))
|
|
21785
|
-
continue;
|
|
21786
|
-
const current = settings.skillOverrides?.[cap.name];
|
|
21787
|
-
if (current === "off" || current === "user-invocable-only") {
|
|
21788
|
-
plan.skipped.push(`skill ${cap.name} (already ${current})`);
|
|
22064
|
+
async function promote(server, activated, caps) {
|
|
22065
|
+
const names = [];
|
|
22066
|
+
let changed = false;
|
|
22067
|
+
for (const cap of caps) {
|
|
22068
|
+
if (cap.kind !== "tool" || !cap.inputSchema)
|
|
21789
22069
|
continue;
|
|
21790
|
-
|
|
21791
|
-
|
|
22070
|
+
if (names.length >= AUTO_ACTIVATE_LIMIT)
|
|
22071
|
+
break;
|
|
22072
|
+
const name = exposedName(cap);
|
|
22073
|
+
names.push(name);
|
|
22074
|
+
if (activated.get(name)?.id !== cap.id)
|
|
22075
|
+
changed = true;
|
|
22076
|
+
activated.delete(name);
|
|
22077
|
+
activated.set(name, cap);
|
|
21792
22078
|
}
|
|
21793
|
-
|
|
21794
|
-
|
|
21795
|
-
|
|
21796
|
-
|
|
21797
|
-
|
|
21798
|
-
settings.enabledPlugins = { ...settings.enabledPlugins };
|
|
21799
|
-
for (const s of plan.skills)
|
|
21800
|
-
settings.skillOverrides[s.name] = s.mode;
|
|
21801
|
-
for (const p of plan.plugins)
|
|
21802
|
-
settings.enabledPlugins[p.id] = false;
|
|
21803
|
-
await writeText(plan.file, `${JSON.stringify(settings, null, 2)}
|
|
21804
|
-
`);
|
|
22079
|
+
if (evictToBudget(activated, names))
|
|
22080
|
+
changed = true;
|
|
22081
|
+
if (changed)
|
|
22082
|
+
await server.sendToolListChanged();
|
|
22083
|
+
return names;
|
|
21805
22084
|
}
|
|
21806
|
-
|
|
21807
|
-
|
|
21808
|
-
|
|
21809
|
-
|
|
21810
|
-
if (
|
|
21811
|
-
return
|
|
21812
|
-
|
|
21813
|
-
|
|
21814
|
-
|
|
21815
|
-
|
|
22085
|
+
function evictToBudget(activated, keep) {
|
|
22086
|
+
let total = 0;
|
|
22087
|
+
for (const [name, cap] of activated)
|
|
22088
|
+
total += promotedCost(name, cap);
|
|
22089
|
+
if (total <= ACTIVE_TOKEN_BUDGET)
|
|
22090
|
+
return false;
|
|
22091
|
+
const protectedNames = new Set(keep);
|
|
22092
|
+
let changed = false;
|
|
22093
|
+
for (const [name, cap] of activated) {
|
|
22094
|
+
if (total <= ACTIVE_TOKEN_BUDGET)
|
|
22095
|
+
break;
|
|
22096
|
+
if (protectedNames.has(name))
|
|
22097
|
+
continue;
|
|
22098
|
+
total -= promotedCost(name, cap);
|
|
22099
|
+
activated.delete(name);
|
|
22100
|
+
changed = true;
|
|
21816
22101
|
}
|
|
22102
|
+
return changed;
|
|
21817
22103
|
}
|
|
21818
|
-
async function
|
|
21819
|
-
|
|
21820
|
-
|
|
21821
|
-
|
|
21822
|
-
|
|
21823
|
-
|
|
21824
|
-
manifest = JSON.parse(stripJsonComments(manifestText));
|
|
21825
|
-
} catch {
|
|
21826
|
-
return [];
|
|
21827
|
-
}
|
|
21828
|
-
if (!manifest.mcpServers)
|
|
21829
|
-
return [];
|
|
21830
|
-
if (typeof manifest.mcpServers !== "string")
|
|
21831
|
-
return Object.keys(manifest.mcpServers);
|
|
21832
|
-
const rel = manifest.mcpServers.replace(/^\.\//, "");
|
|
21833
|
-
const text = await readText(join15(root, rel));
|
|
21834
|
-
if (!text)
|
|
21835
|
-
return [];
|
|
21836
|
-
try {
|
|
21837
|
-
const loaded = JSON.parse(stripJsonComments(text));
|
|
21838
|
-
return Object.keys(loaded.mcpServers ?? loaded ?? {});
|
|
21839
|
-
} catch {
|
|
21840
|
-
return [];
|
|
22104
|
+
async function handleDescribe(router, args) {
|
|
22105
|
+
if (!args.id)
|
|
22106
|
+
return errorResult("id is required");
|
|
22107
|
+
const found = await router.describe(args.id);
|
|
22108
|
+
if (!found) {
|
|
22109
|
+
return errorResult(`No capability with id "${args.id}". Use find_capabilities to search.`);
|
|
21841
22110
|
}
|
|
22111
|
+
return textResult(renderCapability(found.capability, found.body));
|
|
21842
22112
|
}
|
|
21843
|
-
|
|
21844
|
-
|
|
21845
|
-
|
|
21846
|
-
const
|
|
21847
|
-
|
|
21848
|
-
"
|
|
21849
|
-
|
|
21850
|
-
|
|
21851
|
-
|
|
21852
|
-
const
|
|
21853
|
-
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
delete entries[name];
|
|
21865
|
-
}
|
|
22113
|
+
async function handleCall(router, promoteOnUse, args) {
|
|
22114
|
+
if (!args.id)
|
|
22115
|
+
return errorResult("id is required");
|
|
22116
|
+
const cap = router.get(args.id);
|
|
22117
|
+
if (!cap)
|
|
22118
|
+
return errorResult(`No capability with id "${args.id}".`);
|
|
22119
|
+
const out = await invoke(router, cap, args.arguments ?? {}, args.confirm === true);
|
|
22120
|
+
if (out.isError)
|
|
22121
|
+
return out;
|
|
22122
|
+
const name = await promoteOnUse(cap);
|
|
22123
|
+
if (!name)
|
|
22124
|
+
return out;
|
|
22125
|
+
return {
|
|
22126
|
+
...out,
|
|
22127
|
+
content: [
|
|
22128
|
+
...out.content,
|
|
22129
|
+
{
|
|
22130
|
+
type: "text",
|
|
22131
|
+
text: `(now in your tool list as ${name} — call it directly next time)`
|
|
22132
|
+
}
|
|
22133
|
+
]
|
|
21866
22134
|
};
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
config2 = parse(text);
|
|
21874
|
-
} catch {
|
|
21875
|
-
config2 = null;
|
|
21876
|
-
}
|
|
21877
|
-
if (config2) {
|
|
21878
|
-
const plan = { harness: "codex", file, section: "mcp_servers", moved: [], kept: [] };
|
|
21879
|
-
backup[file] = text;
|
|
21880
|
-
docs.set(file, config2);
|
|
21881
|
-
apply(plan, config2.mcp_servers ?? {});
|
|
21882
|
-
plans.push(plan);
|
|
21883
|
-
}
|
|
21884
|
-
}
|
|
21885
|
-
} else {
|
|
21886
|
-
for (const site of jsonSites(opts.harness, opts.cwd)) {
|
|
21887
|
-
if (!docs.has(site.file)) {
|
|
21888
|
-
const text = await readText(site.file);
|
|
21889
|
-
if (!text)
|
|
21890
|
-
continue;
|
|
21891
|
-
try {
|
|
21892
|
-
docs.set(site.file, JSON.parse(stripJsonComments(text)));
|
|
21893
|
-
} catch {
|
|
21894
|
-
continue;
|
|
21895
|
-
}
|
|
21896
|
-
backup[site.file] = text;
|
|
21897
|
-
}
|
|
21898
|
-
const config2 = docs.get(site.file);
|
|
21899
|
-
const container = site.path.reduce((acc, k) => acc?.[k], config2);
|
|
21900
|
-
if (!container || typeof container !== "object")
|
|
21901
|
-
continue;
|
|
21902
|
-
const plan = {
|
|
21903
|
-
harness: opts.harness,
|
|
21904
|
-
file: site.file,
|
|
21905
|
-
section: site.section,
|
|
21906
|
-
moved: [],
|
|
21907
|
-
kept: []
|
|
21908
|
-
};
|
|
21909
|
-
apply(plan, container);
|
|
21910
|
-
plans.push(plan);
|
|
22135
|
+
}
|
|
22136
|
+
async function invoke(router, cap, args, confirmed) {
|
|
22137
|
+
try {
|
|
22138
|
+
const out = await router.call(cap.id, args, { confirmed });
|
|
22139
|
+
if (out.capability.kind === "skill" || out.capability.kind === "command" || out.capability.kind === "agent") {
|
|
22140
|
+
return { content: out.content, isError: false };
|
|
21911
22141
|
}
|
|
21912
|
-
}
|
|
21913
|
-
const routedServers = new Set([
|
|
21914
|
-
...resolved.servers.map((s) => s.name),
|
|
21915
|
-
...resolved.servers.flatMap((s) => s.name.includes(":") ? [s.name.split(":").pop()] : []),
|
|
21916
|
-
...Object.keys(adopted)
|
|
21917
|
-
]);
|
|
21918
|
-
const extras = opts.extras && opts.harness === "claude" ? await planExtras({
|
|
21919
|
-
cwd: opts.cwd,
|
|
21920
|
-
skillPaths: resolved.config.skillPaths,
|
|
21921
|
-
keepSkills: opts.keepSkills ?? [],
|
|
21922
|
-
keepPlugins: opts.keepPlugins ?? [],
|
|
21923
|
-
mode: opts.skillMode ?? "user-invocable-only",
|
|
21924
|
-
routedServers,
|
|
21925
|
-
unreachable: opts.unreachable
|
|
21926
|
-
}) : null;
|
|
21927
|
-
const extrasCount = (extras?.skills.length ?? 0) + (extras?.plugins.length ?? 0);
|
|
21928
|
-
const movedCount = plans.reduce((n, p) => n + p.moved.length, 0);
|
|
21929
|
-
if (!movedCount && !extrasCount) {
|
|
21930
22142
|
return {
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
notes: ["Nothing to adopt — no downstream servers, skills or plugins are loaded directly by this harness."]
|
|
22143
|
+
content: [{ type: "text", text: `[${out.source}]` }, ...out.content],
|
|
22144
|
+
isError: out.isError,
|
|
22145
|
+
...out.structuredContent ? { structuredContent: out.structuredContent } : {}
|
|
21935
22146
|
};
|
|
22147
|
+
} catch (err) {
|
|
22148
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
21936
22149
|
}
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
|
|
21943
|
-
|
|
21944
|
-
const
|
|
21945
|
-
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
const
|
|
21951
|
-
|
|
21952
|
-
|
|
22150
|
+
}
|
|
22151
|
+
async function handleActivate(router, server, activated, args) {
|
|
22152
|
+
const ids = args.ids ?? [];
|
|
22153
|
+
if (!ids.length)
|
|
22154
|
+
return errorResult("ids is required");
|
|
22155
|
+
const added = [];
|
|
22156
|
+
const missing = [];
|
|
22157
|
+
for (const id of ids) {
|
|
22158
|
+
const cap = router.get(id);
|
|
22159
|
+
if (!cap || cap.kind !== "tool") {
|
|
22160
|
+
missing.push(id);
|
|
22161
|
+
continue;
|
|
22162
|
+
}
|
|
22163
|
+
const toolName = exposedName(cap);
|
|
22164
|
+
activated.set(toolName, cap);
|
|
22165
|
+
added.push(toolName);
|
|
21953
22166
|
}
|
|
21954
|
-
if (
|
|
21955
|
-
await
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
22167
|
+
if (added.length)
|
|
22168
|
+
await server.sendToolListChanged();
|
|
22169
|
+
const lines = [
|
|
22170
|
+
added.length ? `Activated as tools: ${added.join(", ")}` : "Nothing activated.",
|
|
22171
|
+
missing.length ? `Not activatable (unknown, or not a tool): ${missing.join(", ")}` : "",
|
|
22172
|
+
added.length ? "If these do not appear in your tool list yet, call them through call_capability instead — the result is identical." : ""
|
|
22173
|
+
].filter(Boolean);
|
|
22174
|
+
return textResult(lines.join(`
|
|
22175
|
+
`));
|
|
22176
|
+
}
|
|
22177
|
+
async function handleDeactivate(server, activated, args) {
|
|
22178
|
+
const before = activated.size;
|
|
22179
|
+
if (!args.ids?.length)
|
|
22180
|
+
activated.clear();
|
|
22181
|
+
else {
|
|
22182
|
+
for (const id of args.ids) {
|
|
22183
|
+
for (const [name, cap] of activated) {
|
|
22184
|
+
if (cap.id === id || name === id)
|
|
22185
|
+
activated.delete(name);
|
|
22186
|
+
}
|
|
22187
|
+
}
|
|
21963
22188
|
}
|
|
21964
|
-
if (
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
notes.push("Restart the harness — its tool list should now show only the router.");
|
|
21968
|
-
return { plans, extras, backup: backupPath, notes };
|
|
22189
|
+
if (activated.size !== before)
|
|
22190
|
+
await server.sendToolListChanged();
|
|
22191
|
+
return textResult(`Active tools: ${activated.size} (was ${before}).`);
|
|
21969
22192
|
}
|
|
21970
|
-
function
|
|
21971
|
-
|
|
22193
|
+
function exposedName(cap) {
|
|
22194
|
+
const parsed = parseCapabilityId(cap.id);
|
|
22195
|
+
const raw = parsed.server ? `${parsed.server}__${cap.name}` : cap.name;
|
|
22196
|
+
return raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
21972
22197
|
}
|
|
21973
|
-
function
|
|
21974
|
-
return
|
|
22198
|
+
function textResult(text) {
|
|
22199
|
+
return { content: [{ type: "text", text }] };
|
|
21975
22200
|
}
|
|
21976
|
-
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
22201
|
+
function errorResult(text) {
|
|
22202
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
22203
|
+
}
|
|
22204
|
+
function log(message) {
|
|
22205
|
+
process.stderr.write(`[autorouter] ${message}
|
|
22206
|
+
`);
|
|
22207
|
+
}
|
|
22208
|
+
|
|
22209
|
+
// src/cli/init.ts
|
|
22210
|
+
import { join as join17 } from "node:path";
|
|
22211
|
+
import { createInterface } from "node:readline/promises";
|
|
22212
|
+
var HARNESS_LABEL = {
|
|
22213
|
+
claude: "Claude Code",
|
|
22214
|
+
codex: "Codex CLI",
|
|
22215
|
+
cursor: "Cursor",
|
|
22216
|
+
vscode: "VS Code / Copilot"
|
|
22217
|
+
};
|
|
22218
|
+
async function runInit(opts) {
|
|
21989
22219
|
const notes = [];
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
22220
|
+
const env = {};
|
|
22221
|
+
const suggested = await suggestModel(opts.harness);
|
|
22222
|
+
const model = opts.yes ? suggested : await promptModel(opts.harness, suggested);
|
|
22223
|
+
if (model) {
|
|
22224
|
+
env.AUTOROUTER_SELECTOR_MODEL = model;
|
|
22225
|
+
notes.push(`Selector model: ${model}`);
|
|
22226
|
+
} else {
|
|
22227
|
+
notes.push("No selector model set — the router will use MCP sampling if the host offers it, otherwise raw index ranking.");
|
|
21993
22228
|
}
|
|
21994
|
-
|
|
21995
|
-
return notes;
|
|
21996
|
-
}
|
|
21997
|
-
function jsonSites(harness, cwd) {
|
|
21998
|
-
switch (harness) {
|
|
22229
|
+
switch (opts.harness) {
|
|
21999
22230
|
case "claude":
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
{
|
|
22003
|
-
file: join16(homeDir(), ".claude.json"),
|
|
22004
|
-
section: `projects[${cwd}].mcpServers`,
|
|
22005
|
-
path: ["projects", cwd, "mcpServers"]
|
|
22006
|
-
},
|
|
22007
|
-
{ file: join16(cwd, ".mcp.json"), section: "mcpServers", path: ["mcpServers"] }
|
|
22008
|
-
];
|
|
22231
|
+
notes.push(await writeJsonServer(join17(homeDir(), ".claude.json"), "mcpServers", opts, env));
|
|
22232
|
+
break;
|
|
22009
22233
|
case "cursor":
|
|
22010
|
-
|
|
22011
|
-
|
|
22012
|
-
{ file: join16(cwd, ".cursor", "mcp.json"), section: "mcpServers", path: ["mcpServers"] }
|
|
22013
|
-
];
|
|
22234
|
+
notes.push(await writeJsonServer(join17(homeDir(), ".cursor", "mcp.json"), "mcpServers", opts, env));
|
|
22235
|
+
break;
|
|
22014
22236
|
case "vscode":
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
|
|
22018
|
-
|
|
22019
|
-
|
|
22020
|
-
return [];
|
|
22237
|
+
notes.push(await writeJsonServer(join17(homeDir(), ".vscode", "mcp.json"), "servers", opts, env));
|
|
22238
|
+
break;
|
|
22239
|
+
case "codex":
|
|
22240
|
+
notes.push(await writeCodexServer(opts, env));
|
|
22241
|
+
break;
|
|
22021
22242
|
}
|
|
22243
|
+
notes.push(await writePrimer(opts.harness));
|
|
22244
|
+
return notes;
|
|
22022
22245
|
}
|
|
22023
|
-
function
|
|
22024
|
-
|
|
22025
|
-
|
|
22026
|
-
|
|
22027
|
-
|
|
22028
|
-
|
|
22246
|
+
async function writeJsonServer(path, key, opts, env) {
|
|
22247
|
+
await ensureDir(join17(path, ".."));
|
|
22248
|
+
const existing = await readText(path) ?? "{}";
|
|
22249
|
+
let config2;
|
|
22250
|
+
try {
|
|
22251
|
+
config2 = JSON.parse(existing);
|
|
22252
|
+
} catch {
|
|
22253
|
+
return `Could not parse ${path}; add the server manually.`;
|
|
22029
22254
|
}
|
|
22030
|
-
|
|
22255
|
+
config2[key] ??= {};
|
|
22256
|
+
config2[key].autorouter = {
|
|
22257
|
+
command: opts.command,
|
|
22258
|
+
args: opts.args,
|
|
22259
|
+
...Object.keys(env).length ? { env } : {}
|
|
22260
|
+
};
|
|
22261
|
+
await writeText(path, `${JSON.stringify(config2, null, 2)}
|
|
22262
|
+
`);
|
|
22263
|
+
return `Registered autorouter in ${path}`;
|
|
22031
22264
|
}
|
|
22032
|
-
async function
|
|
22033
|
-
const path =
|
|
22034
|
-
await ensureDir(
|
|
22035
|
-
|
|
22036
|
-
|
|
22037
|
-
|
|
22038
|
-
|
|
22039
|
-
|
|
22040
|
-
|
|
22041
|
-
}
|
|
22265
|
+
async function writeCodexServer(opts, env) {
|
|
22266
|
+
const path = join17(homeDir(), ".codex", "config.toml");
|
|
22267
|
+
await ensureDir(join17(path, ".."));
|
|
22268
|
+
const text = await readText(path) ?? "";
|
|
22269
|
+
let config2;
|
|
22270
|
+
try {
|
|
22271
|
+
config2 = text ? parse(text) : {};
|
|
22272
|
+
} catch {
|
|
22273
|
+
return `Could not parse ${path}; add [mcp_servers.autorouter] manually.`;
|
|
22042
22274
|
}
|
|
22043
|
-
config2.
|
|
22044
|
-
|
|
22275
|
+
config2.mcp_servers ??= {};
|
|
22276
|
+
config2.mcp_servers.autorouter = {
|
|
22277
|
+
command: opts.command,
|
|
22278
|
+
args: opts.args,
|
|
22279
|
+
startup_timeout_sec: 60,
|
|
22280
|
+
...Object.keys(env).length ? { env } : {}
|
|
22281
|
+
};
|
|
22282
|
+
await writeText(path, stringify(config2));
|
|
22283
|
+
return `Registered autorouter in ${path}`;
|
|
22284
|
+
}
|
|
22285
|
+
async function writePrimer(harness) {
|
|
22286
|
+
const targets = {
|
|
22287
|
+
claude: join17(homeDir(), ".claude", "CLAUDE.md"),
|
|
22288
|
+
codex: join17(homeDir(), ".codex", "AGENTS.md"),
|
|
22289
|
+
cursor: join17(process.cwd(), ".cursor", "rules", "autorouter.mdc"),
|
|
22290
|
+
vscode: join17(process.cwd(), ".github", "copilot-instructions.md")
|
|
22291
|
+
};
|
|
22292
|
+
const path = targets[harness];
|
|
22293
|
+
await ensureDir(join17(path, ".."));
|
|
22294
|
+
const current = await readText(path) ?? "";
|
|
22295
|
+
const section = harness === "cursor" ? `---
|
|
22296
|
+
description: Capability router
|
|
22297
|
+
alwaysApply: true
|
|
22298
|
+
---
|
|
22299
|
+
|
|
22300
|
+
${PRIMER}` : PRIMER;
|
|
22301
|
+
const existing = /<!-- autorouter -->[\s\S]*?<!-- \/autorouter -->/.exec(current);
|
|
22302
|
+
if (existing) {
|
|
22303
|
+
if (existing[0] === PRIMER)
|
|
22304
|
+
return `Primer already current in ${path}`;
|
|
22305
|
+
await writeText(path, current.replace(existing[0], PRIMER));
|
|
22306
|
+
return `Updated router primer in ${path}`;
|
|
22307
|
+
}
|
|
22308
|
+
await writeText(path, current ? `${current.trimEnd()}
|
|
22309
|
+
|
|
22310
|
+
${section}
|
|
22311
|
+
` : `${section}
|
|
22045
22312
|
`);
|
|
22313
|
+
return `Added router primer to ${path}`;
|
|
22314
|
+
}
|
|
22315
|
+
var PRIMER = `<!-- autorouter -->
|
|
22316
|
+
## Capability router
|
|
22317
|
+
|
|
22318
|
+
Most tools, skills and commands on this machine are not in your tool list. They
|
|
22319
|
+
are behind \`find_capabilities\`.
|
|
22320
|
+
|
|
22321
|
+
Before deciding a task cannot be done, or reaching for a manual workaround, call
|
|
22322
|
+
\`find_capabilities({ query: "<what you are trying to do>" })\`. It searches every
|
|
22323
|
+
configured MCP server, skill and plugin command and returns only what fits.
|
|
22324
|
+
Then \`describe_capability\` for the schema, and \`call_capability\` to run it.
|
|
22325
|
+
|
|
22326
|
+
To install a new MCP server, use the router's \`add_server\` tool, or run
|
|
22327
|
+
\`autorouter add <name> --url <url>\` / \`autorouter add <name> -- <command>\`.
|
|
22328
|
+
Do not run \`claude mcp add\` — that loads the server's whole tool list into
|
|
22329
|
+
context, which is what the router is here to avoid.
|
|
22330
|
+
<!-- /autorouter -->`;
|
|
22331
|
+
async function promptModel(harness, suggested) {
|
|
22332
|
+
if (!process.stdin.isTTY)
|
|
22333
|
+
return suggested;
|
|
22334
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22335
|
+
try {
|
|
22336
|
+
console.log(`
|
|
22337
|
+
The router uses a small model to pick which capabilities fit a request.
|
|
22338
|
+
` + `It should be the cheapest model available in ${HARNESS_LABEL[harness]}.`);
|
|
22339
|
+
const answer = (await rl.question(suggested ? `Selector model [${suggested}] (enter to accept, "none" to skip): ` : "Selector model (blank to skip): ")).trim();
|
|
22340
|
+
if (answer.toLowerCase() === "none")
|
|
22341
|
+
return null;
|
|
22342
|
+
return answer || suggested;
|
|
22343
|
+
} finally {
|
|
22344
|
+
rl.close();
|
|
22345
|
+
}
|
|
22046
22346
|
}
|
|
22047
22347
|
|
|
22048
22348
|
// src/cli/doctor.ts
|
|
@@ -22099,7 +22399,7 @@ async function runDoctor(cwd) {
|
|
|
22099
22399
|
lines.push("");
|
|
22100
22400
|
const stillDirect = new Map;
|
|
22101
22401
|
const stillLoaded = new Map;
|
|
22102
|
-
for (const harness of
|
|
22402
|
+
for (const harness of HARNESSES2) {
|
|
22103
22403
|
try {
|
|
22104
22404
|
const { plans, extras } = await runAdopt({ harness, cwd, keep: [], dryRun: true, extras: true });
|
|
22105
22405
|
const names = plans.flatMap((p) => p.moved);
|
|
@@ -22113,18 +22413,29 @@ async function runDoctor(cwd) {
|
|
|
22113
22413
|
}
|
|
22114
22414
|
} catch {}
|
|
22115
22415
|
}
|
|
22116
|
-
const
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22416
|
+
const unrealized = (harness) => {
|
|
22417
|
+
const servers = new Set(stillDirect.get(harness) ?? []);
|
|
22418
|
+
const skills = new Set(stillLoaded.get(harness)?.skills ?? []);
|
|
22419
|
+
return catalog.capabilities.filter((c) => c.server && servers.has(c.server) || c.kind === "skill" && skills.has(c.name)).reduce((sum, c) => sum + c.approxTokens, 0);
|
|
22420
|
+
};
|
|
22120
22421
|
const full = catalog.capabilities.reduce((sum, c) => sum + c.approxTokens, 0);
|
|
22121
|
-
const routerSurface = 700 + tokensOf(promptList(catalog.capabilities));
|
|
22422
|
+
const routerSurface = 700 + tokensOf(promptList(catalog.capabilities, cfg.promptMode));
|
|
22122
22423
|
lines.push("## Context cost");
|
|
22123
22424
|
lines.push(` exposing everything: ~${full.toLocaleString()} tokens`);
|
|
22124
|
-
lines.push(` router surface: ~${routerSurface} tokens`);
|
|
22125
|
-
lines.push(`
|
|
22126
|
-
|
|
22127
|
-
|
|
22425
|
+
lines.push(` router surface: ~${routerSurface.toLocaleString()} tokens`);
|
|
22426
|
+
lines.push(` best case: ~${(full - routerSurface).toLocaleString()} tokens saved (${Math.round((full - routerSurface) / full * 100)}%)`);
|
|
22427
|
+
lines.push("", " per harness (a session only ever runs in one):");
|
|
22428
|
+
for (const harness of HARNESSES2) {
|
|
22429
|
+
if (!stillDirect.has(harness) && !stillLoaded.has(harness))
|
|
22430
|
+
continue;
|
|
22431
|
+
const direct = unrealized(harness);
|
|
22432
|
+
const saved = full - routerSurface - direct;
|
|
22433
|
+
lines.push(` ${harness.padEnd(7)} still loaded ~${direct.toLocaleString()}` + (saved > 0 ? ` → saves ~${saved.toLocaleString()} (${Math.round(saved / full * 100)}%)` : " → saves nothing yet"));
|
|
22434
|
+
}
|
|
22435
|
+
const adopted = HARNESSES2.filter((h) => !stillDirect.has(h) && !stillLoaded.has(h));
|
|
22436
|
+
if (adopted.length) {
|
|
22437
|
+
lines.push(` ${adopted.join(", ")}: fully adopted → saves ~${(full - routerSurface).toLocaleString()} (${Math.round((full - routerSurface) / full * 100)}%)`);
|
|
22438
|
+
}
|
|
22128
22439
|
if (stillDirect.size || stillLoaded.size) {
|
|
22129
22440
|
lines.push("", "## Not yet adopted");
|
|
22130
22441
|
lines.push(" These are loaded by a harness directly, so they are injected on every");
|
|
@@ -22148,6 +22459,7 @@ async function runDoctor(cwd) {
|
|
|
22148
22459
|
return lines.join(`
|
|
22149
22460
|
`);
|
|
22150
22461
|
}
|
|
22462
|
+
var HARNESSES2 = ["claude", "codex", "cursor", "vscode"];
|
|
22151
22463
|
function truncate2(s, max) {
|
|
22152
22464
|
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
22153
22465
|
}
|
|
@@ -22400,7 +22712,7 @@ function summarizeScopes(scope, limit = 6) {
|
|
|
22400
22712
|
}
|
|
22401
22713
|
|
|
22402
22714
|
// src/cli.ts
|
|
22403
|
-
var VERSION2 = "0.2.
|
|
22715
|
+
var VERSION2 = "0.2.2";
|
|
22404
22716
|
var USAGE = `autorouter — one search tool instead of every tool
|
|
22405
22717
|
|
|
22406
22718
|
autorouter serve Run as an MCP server over stdio (default)
|
|
@@ -22413,6 +22725,10 @@ var USAGE = `autorouter — one search tool instead of every tool
|
|
|
22413
22725
|
autorouter login [server] Authorize an OAuth server (opens a browser);
|
|
22414
22726
|
with no argument, lists what needs one
|
|
22415
22727
|
autorouter logout <server> Forget a stored grant
|
|
22728
|
+
autorouter add <name> --url URL Register a server with the router directly
|
|
22729
|
+
autorouter add <name> -- <cmd> ... Same, for a stdio server
|
|
22730
|
+
autorouter add --json '<snippet>' Same, from a pasted mcpServers block
|
|
22731
|
+
autorouter remove <name> Unregister one
|
|
22416
22732
|
autorouter init --target <harness> Register with claude|codex|cursor|vscode
|
|
22417
22733
|
autorouter adopt --target <harness> Move that harness's other MCP servers —
|
|
22418
22734
|
and, on Claude Code, its skills and
|
|
@@ -22423,6 +22739,9 @@ var USAGE = `autorouter — one search tool instead of every tool
|
|
|
22423
22739
|
|
|
22424
22740
|
Options
|
|
22425
22741
|
--raw search: skip the selector model, show index ranking
|
|
22742
|
+
--url URL add: an http server
|
|
22743
|
+
--command C add: a stdio server (or put the command after a bare --)
|
|
22744
|
+
--json SNIPPET add: a pasted {"mcpServers": {...}} block or a bare entry
|
|
22426
22745
|
--kind K restrict to tool|skill|prompt|resource|command|agent
|
|
22427
22746
|
--server S restrict to one provider
|
|
22428
22747
|
--limit N max results
|
|
@@ -22442,13 +22761,17 @@ Options
|
|
|
22442
22761
|
--scopes S login: request exactly these scopes (comma or space separated)
|
|
22443
22762
|
--list-scopes login: show what the server offers, authorize nothing
|
|
22444
22763
|
|
|
22764
|
+
\`add\` registers behind the router, so a new server never enters your context.
|
|
22765
|
+
Servers added to a harness the normal way (\`claude mcp add\`) are moved behind
|
|
22766
|
+
it automatically; set "autoAdopt": false to keep that manual.
|
|
22767
|
+
|
|
22445
22768
|
Servers like Datadog and Supabase hold no credentials in their config — the
|
|
22446
22769
|
working token is an OAuth grant the harness keeps privately. The router obtains
|
|
22447
22770
|
its own grant instead of borrowing one, so \`autorouter login <server>\` is a
|
|
22448
22771
|
one-time step before those can be adopted.
|
|
22449
22772
|
`;
|
|
22450
22773
|
async function main(argv) {
|
|
22451
|
-
const { command, positionals, flags } = parseArgs(argv);
|
|
22774
|
+
const { command, positionals, flags, rest } = parseArgs(argv);
|
|
22452
22775
|
switch (command) {
|
|
22453
22776
|
case undefined:
|
|
22454
22777
|
case "serve":
|
|
@@ -22480,6 +22803,11 @@ async function main(argv) {
|
|
|
22480
22803
|
return await cmdLogin(positionals[0], flags);
|
|
22481
22804
|
case "logout":
|
|
22482
22805
|
return await cmdLogout(positionals[0]);
|
|
22806
|
+
case "add":
|
|
22807
|
+
return await cmdAdd(positionals[0], flags, rest);
|
|
22808
|
+
case "remove":
|
|
22809
|
+
case "rm":
|
|
22810
|
+
return await cmdRemove(positionals[0]);
|
|
22483
22811
|
case "adopt":
|
|
22484
22812
|
return await cmdAdopt(flags);
|
|
22485
22813
|
case "restore": {
|
|
@@ -22509,6 +22837,33 @@ ${USAGE}`);
|
|
|
22509
22837
|
return 1;
|
|
22510
22838
|
}
|
|
22511
22839
|
}
|
|
22840
|
+
async function cmdAdd(name, flags, rest) {
|
|
22841
|
+
try {
|
|
22842
|
+
const result = await runAdd({
|
|
22843
|
+
name,
|
|
22844
|
+
url: flags.url,
|
|
22845
|
+
command: flags.command ?? rest[0],
|
|
22846
|
+
args: flags.command ? undefined : rest.slice(1),
|
|
22847
|
+
json: flags.json
|
|
22848
|
+
});
|
|
22849
|
+
console.log(result.message);
|
|
22850
|
+
if (!result.ok && result.path) {
|
|
22851
|
+
console.log("Fix it and re-run, or `autorouter remove` it.");
|
|
22852
|
+
}
|
|
22853
|
+
return result.ok ? 0 : 1;
|
|
22854
|
+
} catch (err) {
|
|
22855
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
22856
|
+
return 1;
|
|
22857
|
+
}
|
|
22858
|
+
}
|
|
22859
|
+
async function cmdRemove(name) {
|
|
22860
|
+
if (!name) {
|
|
22861
|
+
console.error("Usage: autorouter remove <name>");
|
|
22862
|
+
return 1;
|
|
22863
|
+
}
|
|
22864
|
+
console.log(await runRemove(name));
|
|
22865
|
+
return 0;
|
|
22866
|
+
}
|
|
22512
22867
|
async function cmdLogin(server, flags) {
|
|
22513
22868
|
if (!server) {
|
|
22514
22869
|
const resolved = await resolveConfig(process.cwd());
|
|
@@ -22828,24 +23183,20 @@ function parseArgs(argv) {
|
|
|
22828
23183
|
const positionals = [];
|
|
22829
23184
|
const flags = {};
|
|
22830
23185
|
let command;
|
|
23186
|
+
let rest = [];
|
|
22831
23187
|
for (let i = 0;i < argv.length; i++) {
|
|
22832
23188
|
const arg = argv[i];
|
|
23189
|
+
if (arg === "--") {
|
|
23190
|
+
rest = argv.slice(i + 1);
|
|
23191
|
+
break;
|
|
23192
|
+
}
|
|
22833
23193
|
if (arg.startsWith("--")) {
|
|
22834
23194
|
const [name, inline] = splitOnce(arg.slice(2), "=");
|
|
22835
23195
|
if (!command && (name === "help" || name === "version")) {
|
|
22836
23196
|
command = `--${name}`;
|
|
22837
23197
|
continue;
|
|
22838
23198
|
}
|
|
22839
|
-
const boolean5 = [
|
|
22840
|
-
"raw",
|
|
22841
|
-
"json",
|
|
22842
|
-
"yes",
|
|
22843
|
-
"dry-run",
|
|
22844
|
-
"force",
|
|
22845
|
-
"servers-only",
|
|
22846
|
-
"read-only",
|
|
22847
|
-
"list-scopes"
|
|
22848
|
-
].includes(name);
|
|
23199
|
+
const boolean5 = ["raw", "json", "yes", "dry-run", "force", "servers-only", "read-only", "list-scopes"].includes(name) && !(name === "json" && command === "add");
|
|
22849
23200
|
if (boolean5) {
|
|
22850
23201
|
flags[name] = true;
|
|
22851
23202
|
} else if (inline !== undefined) {
|
|
@@ -22860,7 +23211,7 @@ function parseArgs(argv) {
|
|
|
22860
23211
|
else
|
|
22861
23212
|
positionals.push(arg);
|
|
22862
23213
|
}
|
|
22863
|
-
return { command, positionals, flags };
|
|
23214
|
+
return { command, positionals, flags, rest };
|
|
22864
23215
|
}
|
|
22865
23216
|
function splitOnce(s, sep) {
|
|
22866
23217
|
const i = s.indexOf(sep);
|