@hasna/mcps 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -8215,7 +8215,7 @@ var require_formats = __commonJS((exports) => {
8215
8215
  }
8216
8216
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
8217
8217
  function getTime(strictTimeZone) {
8218
- return function time(str) {
8218
+ return function time3(str) {
8219
8219
  const matches = TIME.exec(str);
8220
8220
  if (!matches)
8221
8221
  return false;
@@ -9183,7 +9183,7 @@ var init_sources = __esm(() => {
9183
9183
  var require_package = __commonJS((exports, module) => {
9184
9184
  module.exports = {
9185
9185
  name: "@hasna/mcps",
9186
- version: "0.0.3",
9186
+ version: "0.0.5",
9187
9187
  description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
9188
9188
  type: "module",
9189
9189
  main: "dist/index.js",
@@ -9206,7 +9206,7 @@ var require_package = __commonJS((exports, module) => {
9206
9206
  "README.md"
9207
9207
  ],
9208
9208
  scripts: {
9209
- build: "bun run build:dashboard && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
9209
+ build: `bun run build:dashboard && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && node -e "const fs=require('fs');const f='bin/mcp.js';const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!/'))fs.writeFileSync(f,'#!/usr/bin/env bun\\n'+c);fs.chmodSync(f,0o755);" && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist`,
9210
9210
  "build:dashboard": "cd dashboard && bun install && bun run build",
9211
9211
  dev: "bun run src/cli/index.tsx",
9212
9212
  "dev:mcp": "bun run src/mcp/index.ts",
@@ -11095,6 +11095,21 @@ function getToolCounts() {
11095
11095
  const rows = db2.prepare("SELECT server_id, COUNT(*) as count FROM tool_cache GROUP BY server_id").all();
11096
11096
  return new Map(rows.map((row) => [row.server_id, Number(row.count)]));
11097
11097
  }
11098
+ function cloneServer(id, newName) {
11099
+ const server = getServer(id);
11100
+ if (!server)
11101
+ throw new Error(`Server "${id}" not found`);
11102
+ return addServer({
11103
+ name: newName,
11104
+ description: server.description ?? undefined,
11105
+ command: server.command,
11106
+ args: server.args,
11107
+ env: server.env,
11108
+ transport: server.transport,
11109
+ url: server.url ?? undefined,
11110
+ source: server.source
11111
+ });
11112
+ }
11098
11113
  function getCachedTools(serverId) {
11099
11114
  const db2 = getDb();
11100
11115
  const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
@@ -25521,10 +25536,21 @@ async function diagnoseServer(server) {
25521
25536
  const checks4 = [];
25522
25537
  if (server.transport === "stdio") {
25523
25538
  try {
25524
- execFileSync("which", [server.command], { stdio: "pipe" });
25525
- checks4.push({ name: "command on PATH", pass: true, message: `${server.command} found` });
25539
+ const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
25540
+ let version2 = "";
25541
+ try {
25542
+ version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
25543
+ `)[0];
25544
+ } catch {}
25545
+ checks4.push({ name: "command on PATH", pass: true, message: `${path}${version2 ? ` (${version2})` : ""}` });
25526
25546
  } catch {
25527
- checks4.push({ name: "command on PATH", pass: false, message: `${server.command} not found on PATH` });
25547
+ checks4.push({
25548
+ name: "command on PATH",
25549
+ pass: false,
25550
+ message: `${server.command} not found on PATH`,
25551
+ fixable: true,
25552
+ fixHint: server.args[0] || server.command
25553
+ });
25528
25554
  }
25529
25555
  }
25530
25556
  const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
@@ -27047,6 +27073,59 @@ server.tool("disable_server", "Disable a registered MCP server", { id: exports_e
27047
27073
  content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
27048
27074
  };
27049
27075
  });
27076
+ server.tool("update_server", "Update fields of a registered MCP server", {
27077
+ id: exports_external.string().describe("Server ID to update"),
27078
+ name: exports_external.string().optional().describe("New display name"),
27079
+ description: exports_external.string().optional().describe("New description"),
27080
+ command: exports_external.string().optional().describe("New command"),
27081
+ args: exports_external.array(exports_external.string()).optional().describe("New args list"),
27082
+ transport: exports_external.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
27083
+ url: exports_external.string().optional().describe("New URL for remote transports")
27084
+ }, async ({ id, name, description, command, args, transport, url: url2 }) => {
27085
+ const existing = getServer(id);
27086
+ if (!existing) {
27087
+ return {
27088
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27089
+ isError: true
27090
+ };
27091
+ }
27092
+ const fields = {};
27093
+ if (name !== undefined)
27094
+ fields.name = name;
27095
+ if (description !== undefined)
27096
+ fields.description = description;
27097
+ if (command !== undefined)
27098
+ fields.command = command;
27099
+ if (args !== undefined)
27100
+ fields.args = args;
27101
+ if (transport !== undefined)
27102
+ fields.transport = transport;
27103
+ if (url2 !== undefined)
27104
+ fields.url = url2;
27105
+ const updated = updateServer(id, fields);
27106
+ return {
27107
+ content: [{ type: "text", text: JSON.stringify(redactServerEnv(updated), null, 2) }]
27108
+ };
27109
+ });
27110
+ server.tool("list_tools", "List all cached tools across registered servers without connecting. Optionally filter by server_id.", { server_id: exports_external.string().optional().describe("Server ID to filter by (optional)") }, async ({ server_id }) => {
27111
+ if (server_id) {
27112
+ const tools = getCachedTools(server_id);
27113
+ return {
27114
+ content: [{ type: "text", text: JSON.stringify(tools.map((t) => ({ ...t, server_id })), null, 2) }]
27115
+ };
27116
+ }
27117
+ const servers = listServers();
27118
+ const allTools = [];
27119
+ for (const s of servers) {
27120
+ const tools = getCachedTools(s.id);
27121
+ for (const t of tools) {
27122
+ allTools.push({ server_id: s.id, ...t });
27123
+ }
27124
+ }
27125
+ return {
27126
+ content: [{ type: "text", text: JSON.stringify(allTools, null, 2) }]
27127
+ };
27128
+ });
27050
27129
  server.tool("get_server_info", "Get detailed information about a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
27051
27130
  const entry = getServer(id);
27052
27131
  if (!entry) {
@@ -27221,6 +27300,7 @@ if (isDirectRun) {
27221
27300
  import { existsSync as existsSync3 } from "fs";
27222
27301
  import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
27223
27302
  import { fileURLToPath as fileURLToPath2 } from "url";
27303
+ init_sources();
27224
27304
  init_db();
27225
27305
  function redactServer(server2) {
27226
27306
  return { ...server2, env: {} };
@@ -27486,6 +27566,218 @@ Dashboard not found at: ${dashboardDir}`);
27486
27566
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27487
27567
  }
27488
27568
  }
27569
+ if (singleMatch && method === "PATCH") {
27570
+ const id = singleMatch[1];
27571
+ if (!isValidId(id))
27572
+ return json({ error: "Invalid server ID" }, 400, port);
27573
+ const existing = getServer(id);
27574
+ if (!existing)
27575
+ return json({ error: `Server '${id}' not found` }, 404, port);
27576
+ try {
27577
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
27578
+ if (contentLength > MAX_BODY_SIZE)
27579
+ return json({ error: "Request body too large" }, 413, port);
27580
+ let body;
27581
+ try {
27582
+ body = await req.json();
27583
+ } catch {
27584
+ return json({ error: "Invalid JSON body" }, 400, port);
27585
+ }
27586
+ const fields = {};
27587
+ if (body.name !== undefined)
27588
+ fields.name = body.name;
27589
+ if (body.description !== undefined)
27590
+ fields.description = body.description;
27591
+ if (body.command !== undefined)
27592
+ fields.command = body.command;
27593
+ if (body.transport !== undefined)
27594
+ fields.transport = body.transport;
27595
+ if (body.url !== undefined)
27596
+ fields.url = body.url;
27597
+ if (body.args !== undefined) {
27598
+ if (!Array.isArray(body.args) || body.args.some((a) => typeof a !== "string")) {
27599
+ return json({ error: "Invalid 'args' format" }, 400, port);
27600
+ }
27601
+ fields.args = body.args;
27602
+ }
27603
+ const updated = updateServer(id, fields);
27604
+ return json(redactServer(updated), 200, port);
27605
+ } catch (e) {
27606
+ return json({ error: e instanceof Error ? e.message : "Failed to update server" }, 500, port);
27607
+ }
27608
+ }
27609
+ const serverEnvMatch = path.match(/^\/api\/servers\/([^/]+)\/env$/);
27610
+ if (serverEnvMatch && method === "POST") {
27611
+ const id = serverEnvMatch[1];
27612
+ if (!isValidId(id))
27613
+ return json({ error: "Invalid server ID" }, 400, port);
27614
+ try {
27615
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
27616
+ if (contentLength > MAX_BODY_SIZE)
27617
+ return json({ error: "Request body too large" }, 413, port);
27618
+ let body;
27619
+ try {
27620
+ body = await req.json();
27621
+ } catch {
27622
+ return json({ error: "Invalid JSON body" }, 400, port);
27623
+ }
27624
+ if (!body.key || typeof body.key !== "string")
27625
+ return json({ error: "Missing 'key'" }, 400, port);
27626
+ if (typeof body.value !== "string")
27627
+ return json({ error: "Missing 'value'" }, 400, port);
27628
+ setServerEnv(id, body.key, body.value);
27629
+ return json({ ok: true }, 200, port);
27630
+ } catch (e) {
27631
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27632
+ }
27633
+ }
27634
+ const serverEnvKeyMatch = path.match(/^\/api\/servers\/([^/]+)\/env\/([^/]+)$/);
27635
+ if (serverEnvKeyMatch && method === "DELETE") {
27636
+ const id = serverEnvKeyMatch[1];
27637
+ const key = decodeURIComponent(serverEnvKeyMatch[2]);
27638
+ if (!isValidId(id))
27639
+ return json({ error: "Invalid server ID" }, 400, port);
27640
+ try {
27641
+ unsetServerEnv(id, key);
27642
+ return json({ ok: true }, 200, port);
27643
+ } catch (e) {
27644
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27645
+ }
27646
+ }
27647
+ const serverToolsMatch = path.match(/^\/api\/servers\/([^/]+)\/tools$/);
27648
+ if (serverToolsMatch && method === "GET") {
27649
+ const id = serverToolsMatch[1];
27650
+ if (!isValidId(id))
27651
+ return json({ error: "Invalid server ID" }, 400, port);
27652
+ const entry = getServer(id);
27653
+ if (!entry)
27654
+ return json({ error: `Server '${id}' not found` }, 404, port);
27655
+ const tools = getCachedTools(id);
27656
+ return json(tools, 200, port);
27657
+ }
27658
+ const serverCallMatch = path.match(/^\/api\/servers\/([^/]+)\/call$/);
27659
+ if (serverCallMatch && method === "POST") {
27660
+ const id = serverCallMatch[1];
27661
+ if (!isValidId(id))
27662
+ return json({ error: "Invalid server ID" }, 400, port);
27663
+ const entry = getServer(id);
27664
+ if (!entry)
27665
+ return json({ error: `Server '${id}' not found` }, 404, port);
27666
+ try {
27667
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
27668
+ if (contentLength > MAX_BODY_SIZE)
27669
+ return json({ error: "Request body too large" }, 413, port);
27670
+ let body;
27671
+ try {
27672
+ body = await req.json();
27673
+ } catch {
27674
+ return json({ error: "Invalid JSON body" }, 400, port);
27675
+ }
27676
+ if (!body.tool || typeof body.tool !== "string")
27677
+ return json({ error: "Missing 'tool'" }, 400, port);
27678
+ await connectToServer(entry);
27679
+ const toolName = `${id}__${body.tool}`;
27680
+ const result = await callTool(toolName, body.args || {});
27681
+ await disconnectServer(id).catch(() => {
27682
+ return;
27683
+ });
27684
+ return json({ content: result.content }, 200, port);
27685
+ } catch (e) {
27686
+ await disconnectServer(id).catch(() => {
27687
+ return;
27688
+ });
27689
+ return json({ error: e instanceof Error ? e.message : "Failed to call tool" }, 500, port);
27690
+ }
27691
+ }
27692
+ const serverDoctorMatch = path.match(/^\/api\/servers\/([^/]+)\/doctor$/);
27693
+ if (serverDoctorMatch && method === "GET") {
27694
+ const id = serverDoctorMatch[1];
27695
+ if (!isValidId(id))
27696
+ return json({ error: "Invalid server ID" }, 400, port);
27697
+ const entry = getServer(id);
27698
+ if (!entry)
27699
+ return json({ error: `Server '${id}' not found` }, 404, port);
27700
+ try {
27701
+ const report = await diagnoseServer(entry);
27702
+ return json(report, 200, port);
27703
+ } catch (e) {
27704
+ return json({ error: e instanceof Error ? e.message : "Failed to diagnose server" }, 500, port);
27705
+ }
27706
+ }
27707
+ if (path === "/api/sources" && method === "GET") {
27708
+ return json(listSources(), 200, port);
27709
+ }
27710
+ if (path === "/api/sources" && method === "POST") {
27711
+ try {
27712
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
27713
+ if (contentLength > MAX_BODY_SIZE)
27714
+ return json({ error: "Request body too large" }, 413, port);
27715
+ let body;
27716
+ try {
27717
+ body = await req.json();
27718
+ } catch {
27719
+ return json({ error: "Invalid JSON body" }, 400, port);
27720
+ }
27721
+ if (!body.name)
27722
+ return json({ error: "Missing 'name'" }, 400, port);
27723
+ if (!body.type)
27724
+ return json({ error: "Missing 'type'" }, 400, port);
27725
+ if (!body.url)
27726
+ return json({ error: "Missing 'url'" }, 400, port);
27727
+ const source = addSource({
27728
+ name: body.name,
27729
+ type: body.type,
27730
+ url: body.url,
27731
+ description: body.description
27732
+ });
27733
+ return json(source, 200, port);
27734
+ } catch (e) {
27735
+ return json({ error: e instanceof Error ? e.message : "Failed to add source" }, 500, port);
27736
+ }
27737
+ }
27738
+ const singleSourceMatch = path.match(/^\/api\/sources\/([^/]+)$/);
27739
+ if (singleSourceMatch && method === "DELETE") {
27740
+ const id = singleSourceMatch[1];
27741
+ try {
27742
+ removeSource(id);
27743
+ return json({ ok: true }, 200, port);
27744
+ } catch (e) {
27745
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27746
+ }
27747
+ }
27748
+ const sourceEnableMatch = path.match(/^\/api\/sources\/([^/]+)\/enable$/);
27749
+ if (sourceEnableMatch && method === "POST") {
27750
+ const id = sourceEnableMatch[1];
27751
+ try {
27752
+ enableSource(id);
27753
+ return json({ ok: true }, 200, port);
27754
+ } catch (e) {
27755
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27756
+ }
27757
+ }
27758
+ const sourceDisableMatch = path.match(/^\/api\/sources\/([^/]+)\/disable$/);
27759
+ if (sourceDisableMatch && method === "POST") {
27760
+ const id = sourceDisableMatch[1];
27761
+ try {
27762
+ disableSource(id);
27763
+ return json({ ok: true }, 200, port);
27764
+ } catch (e) {
27765
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
27766
+ }
27767
+ }
27768
+ if (path === "/api/find" && method === "GET") {
27769
+ try {
27770
+ const q = url2.searchParams.get("q") || "";
27771
+ const sourcesParam = url2.searchParams.get("sources");
27772
+ const limitParam = url2.searchParams.get("limit");
27773
+ const sources = sourcesParam ? sourcesParam.split(",").filter(Boolean) : undefined;
27774
+ const limit = limitParam ? parseInt(limitParam, 10) : undefined;
27775
+ const results = await findServers(q, { sources, limit });
27776
+ return json(results, 200, port);
27777
+ } catch (e) {
27778
+ return json({ error: e instanceof Error ? e.message : "Find failed" }, 500, port);
27779
+ }
27780
+ }
27489
27781
  if (path === "/api/update" && method === "POST") {
27490
27782
  if (!isLoopbackHost(host)) {
27491
27783
  return json({ error: "Update only allowed on loopback host" }, 403, port);
@@ -27521,7 +27813,7 @@ Dashboard not found at: ${dashboardDir}`);
27521
27813
  return new Response(null, {
27522
27814
  headers: {
27523
27815
  "Access-Control-Allow-Origin": `http://localhost:${port}`,
27524
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
27816
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
27525
27817
  "Access-Control-Allow-Headers": "Content-Type"
27526
27818
  }
27527
27819
  });
@@ -28643,7 +28935,7 @@ var VERSION2 = (() => {
28643
28935
  })();
28644
28936
  var program2 = new Command;
28645
28937
  program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
28646
- program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").action((opts) => {
28938
+ program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").option("--verbose", "Show detailed info including health, command, and transport").action((opts) => {
28647
28939
  const servers = listServers();
28648
28940
  if (opts.json) {
28649
28941
  const toolCounts2 = getToolCounts();
@@ -28665,7 +28957,29 @@ program2.command("list").description("List registered MCP servers").option("--js
28665
28957
  console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
28666
28958
  if (s.description)
28667
28959
  console.log(` ${chalk2.dim(s.description)}`);
28668
- console.log(` ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
28960
+ if (opts.verbose) {
28961
+ console.log(` Command: ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
28962
+ console.log(` Transport: ${chalk2.dim(s.transport)}`);
28963
+ const now = Date.now();
28964
+ if (s.last_connected_at) {
28965
+ const connectedAt = new Date(s.last_connected_at).getTime();
28966
+ const daysDiff = Math.floor((now - connectedAt) / (1000 * 60 * 60 * 24));
28967
+ const connectedLabel = daysDiff === 0 ? "today" : daysDiff === 1 ? "1 day ago" : `${daysDiff} days ago`;
28968
+ const connectedColor = !s.last_error && daysDiff < 7 ? chalk2.green : chalk2.yellow;
28969
+ console.log(` Connected: ${connectedColor(connectedLabel)}`);
28970
+ } else {
28971
+ console.log(` Connected: ${chalk2.dim("never")}`);
28972
+ }
28973
+ if (s.last_error) {
28974
+ console.log(` Error: ${chalk2.red(s.last_error)}`);
28975
+ }
28976
+ const hasError = !!s.last_error;
28977
+ const daysSinceConnect = s.last_connected_at ? Math.floor((Date.now() - new Date(s.last_connected_at).getTime()) / (1000 * 60 * 60 * 24)) : Infinity;
28978
+ const healthIcon = hasError ? chalk2.red("\u2717 unhealthy") : daysSinceConnect < 7 ? chalk2.green("\u2713 healthy") : chalk2.yellow("\u26A0 stale");
28979
+ console.log(` Health: ${healthIcon}`);
28980
+ } else {
28981
+ console.log(` ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
28982
+ }
28669
28983
  }
28670
28984
  closeDb();
28671
28985
  });
@@ -28694,7 +29008,21 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
28694
29008
  closeDb();
28695
29009
  }
28696
29010
  });
28697
- program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").description("Add a local MCP server").action(async (command, args, opts) => {
29011
+ async function promptReadline(rl, question) {
29012
+ return new Promise((resolve2) => rl.question(question, resolve2));
29013
+ }
29014
+ function detectSourceType(url2) {
29015
+ if (url2.includes("raw.githubusercontent.com") || url2.endsWith(".md"))
29016
+ return "awesome-list";
29017
+ if (url2.includes("registry.npmjs.org"))
29018
+ return "npm-search";
29019
+ if (url2.includes("api.github.com/search"))
29020
+ return "github-topic";
29021
+ if (url2.includes("/v0/servers") || url2.includes("/servers"))
29022
+ return "mcp-registry";
29023
+ return null;
29024
+ }
29025
+ program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").option("--wizard", "Interactive setup wizard").option("--force", "Register even if duplicate command exists").description("Add a local MCP server").action(async (command, args, opts) => {
28698
29026
  try {
28699
29027
  if (opts.fromRegistry) {
28700
29028
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
@@ -28704,11 +29032,78 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
28704
29032
  closeDb();
28705
29033
  return;
28706
29034
  }
29035
+ if (opts.wizard) {
29036
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
29037
+ const transport = await promptReadline(rl, "Transport [stdio/sse/http] (default: stdio): ") || "stdio";
29038
+ const wizardCommand = await promptReadline(rl, "Command (e.g. npx, node, bunx): ");
29039
+ if (!wizardCommand) {
29040
+ console.error(chalk2.red("Command is required"));
29041
+ rl.close();
29042
+ closeDb();
29043
+ process.exit(1);
29044
+ }
29045
+ const argsStr = await promptReadline(rl, "Arguments (space-separated, e.g. -y @pkg/name): ");
29046
+ const wizardArgs = argsStr.trim() ? argsStr.trim().split(/\s+/) : [];
29047
+ const wizardName = await promptReadline(rl, "Display name (optional, press enter to skip): ");
29048
+ const wizardDescription = await promptReadline(rl, "Description (optional): ");
29049
+ const env = {};
29050
+ console.log(chalk2.dim("Add env vars (KEY=VALUE). Press enter with empty key to skip."));
29051
+ while (true) {
29052
+ const pair = await promptReadline(rl, " Env var (KEY=VALUE or empty to done): ");
29053
+ if (!pair.trim())
29054
+ break;
29055
+ const eqIdx = pair.indexOf("=");
29056
+ if (eqIdx > 0)
29057
+ env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
29058
+ }
29059
+ rl.close();
29060
+ console.log(chalk2.bold(`
29061
+ Server to add:`));
29062
+ console.log(` Command: ${wizardCommand} ${wizardArgs.join(" ")}`);
29063
+ console.log(` Transport: ${transport}`);
29064
+ if (wizardName)
29065
+ console.log(` Name: ${wizardName}`);
29066
+ if (Object.keys(env).length)
29067
+ console.log(` Env: ${Object.keys(env).join(", ")}`);
29068
+ const confirm = await new Promise((resolve2) => {
29069
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
29070
+ rl2.question(chalk2.bold("Add this server? [Y/n]: "), (ans) => {
29071
+ rl2.close();
29072
+ resolve2(ans);
29073
+ });
29074
+ });
29075
+ if (confirm.toLowerCase() === "n") {
29076
+ console.log("Aborted.");
29077
+ closeDb();
29078
+ return;
29079
+ }
29080
+ const server3 = addServer({
29081
+ command: wizardCommand,
29082
+ args: wizardArgs,
29083
+ name: wizardName || undefined,
29084
+ description: wizardDescription || undefined,
29085
+ transport,
29086
+ env
29087
+ });
29088
+ console.log(chalk2.green(`Added: ${server3.name} [${server3.id}]`));
29089
+ closeDb();
29090
+ return;
29091
+ }
28707
29092
  if (!command) {
28708
- console.error(chalk2.red("Error: command is required (or use --from-registry)"));
29093
+ console.error(chalk2.red("Error: command is required (or use --from-registry or --wizard)"));
28709
29094
  closeDb();
28710
29095
  process.exit(1);
28711
29096
  }
29097
+ const existing = listServers();
29098
+ const duplicate = existing.find((s) => s.command === command && JSON.stringify(s.args) === JSON.stringify(args));
29099
+ if (duplicate) {
29100
+ console.log(chalk2.yellow(`Warning: server "${duplicate.name}" [${duplicate.id}] already uses this command.`));
29101
+ if (!opts.force) {
29102
+ console.log(chalk2.dim("Use --force to register anyway."));
29103
+ closeDb();
29104
+ return;
29105
+ }
29106
+ }
28712
29107
  const envMap = {};
28713
29108
  if (opts.env) {
28714
29109
  for (const pair of opts.env) {
@@ -28740,6 +29135,42 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
28740
29135
  }
28741
29136
  closeDb();
28742
29137
  });
29138
+ program2.command("update-server").argument("<id>", "Server ID to update").description("Update fields of a registered server").option("--name <name>", "New display name").option("--description <desc>", "New description").option("--command <cmd>", "New command").option("--args <args...>", "New args list").option("--transport <type>", "New transport type").option("--url <url>", "New URL").action((id, opts) => {
29139
+ const server2 = getServer(id);
29140
+ if (!server2) {
29141
+ console.error(chalk2.red(`Server "${id}" not found.`));
29142
+ closeDb();
29143
+ process.exit(1);
29144
+ }
29145
+ const fields = {};
29146
+ if (opts.name !== undefined)
29147
+ fields.name = opts.name;
29148
+ if (opts.description !== undefined)
29149
+ fields.description = opts.description;
29150
+ if (opts.command !== undefined)
29151
+ fields.command = opts.command;
29152
+ if (opts.args !== undefined)
29153
+ fields.args = opts.args;
29154
+ if (opts.transport !== undefined)
29155
+ fields.transport = opts.transport;
29156
+ if (opts.url !== undefined)
29157
+ fields.url = opts.url;
29158
+ const updated = updateServer(id, fields);
29159
+ console.log(chalk2.green(`Updated server: ${updated.name} [${updated.id}]`));
29160
+ closeDb();
29161
+ });
29162
+ program2.command("clone").argument("<id>", "Server ID to clone").argument("<new-name>", "Name for the cloned server").description("Clone a server with a new name").action((id, newName) => {
29163
+ try {
29164
+ const cloned = cloneServer(id, newName);
29165
+ console.log(chalk2.green(`Cloned server: ${cloned.name} [${cloned.id}]`));
29166
+ console.log(chalk2.dim(` ${cloned.command} ${cloned.args.join(" ")}`));
29167
+ } catch (err) {
29168
+ console.error(chalk2.red(err.message));
29169
+ closeDb();
29170
+ process.exit(1);
29171
+ }
29172
+ closeDb();
29173
+ });
28743
29174
  program2.command("remove").argument("<id>", "Server ID to remove").description("Remove a registered server").action((id) => {
28744
29175
  const server2 = getServer(id);
28745
29176
  if (!server2) {
@@ -28917,7 +29348,8 @@ program2.command("status").description("Show registry stats").option("--json", "
28917
29348
  console.log(` Tools: ${totalTools} (cached)`);
28918
29349
  closeDb();
28919
29350
  });
28920
- program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").action(async (id) => {
29351
+ program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").option("--fix", "Attempt to fix issues automatically").action(async (id, opts) => {
29352
+ const { execFileSync: execFileSync22 } = await import("child_process");
28921
29353
  const servers = id ? [getServer(id)].filter(Boolean) : listServers();
28922
29354
  if (servers.length === 0) {
28923
29355
  console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
@@ -28932,6 +29364,15 @@ ${server2.name} [${server2.id}]`));
28932
29364
  for (const check2 of report.checks) {
28933
29365
  const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
28934
29366
  console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
29367
+ if (!check2.pass && opts.fix && check2.fixable && check2.fixHint) {
29368
+ console.log(chalk2.dim(` Attempting fix: ${check2.fixHint}`));
29369
+ try {
29370
+ execFileSync22("npm", ["install", "-g", check2.fixHint.replace("npm install -g ", "")], { stdio: "inherit" });
29371
+ console.log(chalk2.green(` Fixed!`));
29372
+ } catch {
29373
+ console.log(chalk2.red(` Fix failed`));
29374
+ }
29375
+ }
28935
29376
  }
28936
29377
  if (!report.healthy)
28937
29378
  allHealthy = false;
@@ -28944,6 +29385,40 @@ ${server2.name} [${server2.id}]`));
28944
29385
  }
28945
29386
  closeDb();
28946
29387
  });
29388
+ program2.command("completion").argument("<shell>", "Shell type: bash, zsh, fish").description("Generate shell completion script").action((shell) => {
29389
+ const commands = ["list", "search", "find", "add", "remove", "enable", "disable", "info", "status", "tools", "call", "doctor", "install", "export", "import", "env", "sources", "clone", "update-server", "serve", "update", "mcp", "completion"];
29390
+ if (shell === "bash") {
29391
+ console.log(`# Add to ~/.bashrc: eval "$(mcps completion bash)"
29392
+ _mcps_complete() {
29393
+ local cur prev words
29394
+ COMPREPLY=()
29395
+ cur="\${COMP_WORDS[COMP_CWORD]}"
29396
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
29397
+ local cmds="${commands.join(" ")}"
29398
+ if [ $COMP_CWORD -eq 1 ]; then
29399
+ COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
29400
+ fi
29401
+ }
29402
+ complete -F _mcps_complete mcps`);
29403
+ } else if (shell === "zsh") {
29404
+ console.log(`# Add to ~/.zshrc: eval "$(mcps completion zsh)"
29405
+ _mcps() {
29406
+ local -a cmds
29407
+ cmds=(${commands.map((c) => `'${c}'`).join(" ")})
29408
+ _describe 'commands' cmds
29409
+ }
29410
+ compdef _mcps mcps`);
29411
+ } else if (shell === "fish") {
29412
+ const lines = commands.map((c) => `complete -c mcps -f -a '${c}'`).join(`
29413
+ `);
29414
+ console.log(`# Add to ~/.config/fish/completions/mcps.fish
29415
+ ${lines}`);
29416
+ } else {
29417
+ console.error(chalk2.red(`Unknown shell: ${shell}. Use bash, zsh, or fish.`));
29418
+ process.exit(1);
29419
+ }
29420
+ closeDb();
29421
+ });
28947
29422
  program2.command("serve").description("Start the web dashboard").option("--port <port>", "Port to listen on", "19427").option("--host <host>", "Host to bind (default: 127.0.0.1)", "127.0.0.1").option("--no-open", "Don't open browser automatically").action(async (opts) => {
28948
29423
  await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
28949
29424
  });
@@ -28981,8 +29456,8 @@ program2.command("find").argument("[query]", "Search query (omit to list all fro
28981
29456
  closeDb();
28982
29457
  return;
28983
29458
  }
28984
- const allSources2 = listSources();
28985
- const sourceNameMap2 = new Map(allSources2.map((s) => [s.id, s.name]));
29459
+ const allSources = listSources();
29460
+ const sourceNameMap2 = new Map(allSources.map((s) => [s.id, s.name]));
28986
29461
  for (const r of results2) {
28987
29462
  const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
28988
29463
  console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
@@ -29005,7 +29480,9 @@ ${results2.length} servers in awesome list.`));
29005
29480
  } else {
29006
29481
  console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
29007
29482
  }
29483
+ const t0 = Date.now();
29008
29484
  const results = await findServers(q, { sources, limit, noCache });
29485
+ const elapsed = Date.now() - t0;
29009
29486
  if (opts.json) {
29010
29487
  console.log(JSON.stringify(results, null, 2));
29011
29488
  closeDb();
@@ -29016,14 +29493,21 @@ ${results2.length} servers in awesome list.`));
29016
29493
  closeDb();
29017
29494
  return;
29018
29495
  }
29496
+ const countBySource = new Map;
29497
+ for (const r of results) {
29498
+ const key = r.sourceId ?? r.source;
29499
+ countBySource.set(key, (countBySource.get(key) ?? 0) + 1);
29500
+ }
29501
+ const allSourcesList = listSources();
29502
+ const sourceNameMap = new Map(allSourcesList.map((s) => [s.id, s.name]));
29503
+ const sourcesUsed = countBySource.size;
29504
+ const breakdownParts = Array.from(countBySource.entries()).map(([k, n]) => `${sourceNameMap.get(k) ?? k}: ${n}`).join(", ");
29019
29505
  const sourceColors = {
29020
29506
  registry: chalk2.blue,
29021
29507
  npm: chalk2.red,
29022
29508
  awesome: chalk2.yellow,
29023
29509
  github: chalk2.magenta
29024
29510
  };
29025
- const allSources = listSources();
29026
- const sourceNameMap = new Map(allSources.map((s) => [s.id, s.name]));
29027
29511
  for (let i = 0;i < results.length; i++) {
29028
29512
  const r = results[i];
29029
29513
  const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
@@ -29039,7 +29523,10 @@ ${results2.length} servers in awesome list.`));
29039
29523
  console.log(` ${chalk2.cyan(r.url)}`);
29040
29524
  }
29041
29525
  console.log(chalk2.dim(`
29042
- ${results.length} result(s). Use \`mcps add --from-registry <id>\` or \`mcps add npx -y <pkg>\` to install.`));
29526
+ Found ${results.length} results across ${sourcesUsed} source${sourcesUsed === 1 ? "" : "s"} (${elapsed}ms)`));
29527
+ if (breakdownParts)
29528
+ console.log(chalk2.dim(` Breakdown: ${breakdownParts}`));
29529
+ console.log(chalk2.dim(`Use \`mcps add --from-registry <id>\` or \`mcps add npx -y <pkg>\` to install.`));
29043
29530
  if (opts.install) {
29044
29531
  let chosen = results[0];
29045
29532
  if (results.length === 1 && opts.yes) {} else {
@@ -29111,14 +29598,26 @@ sourcesCmd.command("list").description("List all search sources").action(() => {
29111
29598
  }
29112
29599
  closeDb();
29113
29600
  });
29114
- sourcesCmd.command("add").description("Add a new search source").option("--name <name>", "Source name (required)").option("--type <type>", "Source type: mcp-registry, awesome-list, npm-search, github-topic (required)").option("--url <url>", "Source URL (required)").option("--description <desc>", "Description").option("--test", "Test the source after adding by running a sample search").action(async (opts) => {
29115
- if (!opts.name || !opts.type || !opts.url) {
29116
- console.error(chalk2.red("Error: --name, --type, and --url are required"));
29601
+ sourcesCmd.command("add").description("Add a new search source").option("--name <name>", "Source name (required)").option("--type <type>", "Source type: mcp-registry, awesome-list, npm-search, github-topic").option("--url <url>", "Source URL (required)").option("--description <desc>", "Description").option("--test", "Test the source after adding by running a sample search").action(async (opts) => {
29602
+ if (!opts.name || !opts.url) {
29603
+ console.error(chalk2.red("Error: --name and --url are required"));
29117
29604
  closeDb();
29118
29605
  process.exit(1);
29119
29606
  }
29120
29607
  const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
29121
- if (!validTypes.includes(opts.type)) {
29608
+ let sourceType = opts.type;
29609
+ if (!sourceType) {
29610
+ const detected = detectSourceType(opts.url);
29611
+ if (detected) {
29612
+ console.log(chalk2.dim(`Auto-detected type: ${detected}`));
29613
+ sourceType = detected;
29614
+ } else {
29615
+ console.error(chalk2.red(`Error: could not auto-detect --type. Please specify one of: ${validTypes.join(", ")}`));
29616
+ closeDb();
29617
+ process.exit(1);
29618
+ }
29619
+ }
29620
+ if (!validTypes.includes(sourceType)) {
29122
29621
  console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
29123
29622
  closeDb();
29124
29623
  process.exit(1);
@@ -29126,7 +29625,7 @@ sourcesCmd.command("add").description("Add a new search source").option("--name
29126
29625
  try {
29127
29626
  const source = addSource({
29128
29627
  name: opts.name,
29129
- type: opts.type,
29628
+ type: sourceType,
29130
29629
  url: opts.url,
29131
29630
  description: opts.description
29132
29631
  });