@hasna/mcps 0.0.2 → 0.0.4

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
@@ -26,6 +26,7 @@ var __export = (target, all) => {
26
26
  set: (newValue) => all[name] = () => newValue
27
27
  });
28
28
  };
29
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
30
  var __require = import.meta.require;
30
31
 
31
32
  // node_modules/commander/lib/error.js
@@ -1867,6 +1868,94 @@ var require_commander = __commonJS((exports) => {
1867
1868
  exports.InvalidOptionArgumentError = InvalidArgumentError;
1868
1869
  });
1869
1870
 
1871
+ // src/lib/config.ts
1872
+ import { join } from "path";
1873
+ import { homedir } from "os";
1874
+ var MCPS_DIR, DB_PATH, REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers", TOOL_PREFIX_SEPARATOR = "__";
1875
+ var init_config = __esm(() => {
1876
+ MCPS_DIR = join(homedir(), ".mcps");
1877
+ DB_PATH = join(MCPS_DIR, "registry.db");
1878
+ });
1879
+
1880
+ // src/lib/db.ts
1881
+ import { Database } from "bun:sqlite";
1882
+ import { mkdirSync } from "fs";
1883
+ function getDb() {
1884
+ if (db)
1885
+ return db;
1886
+ mkdirSync(MCPS_DIR, { recursive: true });
1887
+ db = new Database(DB_PATH, { create: true });
1888
+ db.exec("PRAGMA journal_mode = WAL");
1889
+ db.exec("PRAGMA busy_timeout = 5000");
1890
+ db.exec("PRAGMA foreign_keys = ON");
1891
+ db.exec(`
1892
+ CREATE TABLE IF NOT EXISTS servers (
1893
+ id TEXT PRIMARY KEY,
1894
+ name TEXT NOT NULL,
1895
+ description TEXT,
1896
+ command TEXT NOT NULL,
1897
+ args TEXT NOT NULL DEFAULT '[]',
1898
+ env TEXT NOT NULL DEFAULT '{}',
1899
+ transport TEXT NOT NULL DEFAULT 'stdio',
1900
+ url TEXT,
1901
+ source TEXT NOT NULL DEFAULT 'local',
1902
+ enabled INTEGER NOT NULL DEFAULT 1,
1903
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1904
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1905
+ )
1906
+ `);
1907
+ db.exec(`
1908
+ CREATE TABLE IF NOT EXISTS tool_cache (
1909
+ server_id TEXT NOT NULL,
1910
+ name TEXT NOT NULL,
1911
+ description TEXT NOT NULL DEFAULT '',
1912
+ input_schema TEXT NOT NULL DEFAULT '{}',
1913
+ cached_at TEXT NOT NULL DEFAULT (datetime('now')),
1914
+ PRIMARY KEY (server_id, name),
1915
+ FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
1916
+ )
1917
+ `);
1918
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
1919
+ try {
1920
+ db.exec("ALTER TABLE servers ADD COLUMN last_connected_at TEXT");
1921
+ } catch {}
1922
+ try {
1923
+ db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
1924
+ } catch {}
1925
+ db.exec(`
1926
+ CREATE TABLE IF NOT EXISTS sources (
1927
+ id TEXT PRIMARY KEY,
1928
+ name TEXT NOT NULL,
1929
+ type TEXT NOT NULL,
1930
+ url TEXT NOT NULL,
1931
+ description TEXT,
1932
+ enabled INTEGER NOT NULL DEFAULT 1,
1933
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1934
+ )
1935
+ `);
1936
+ const count = db.query("SELECT COUNT(*) as c FROM sources").get().c;
1937
+ if (count === 0) {
1938
+ db.exec(`
1939
+ INSERT OR IGNORE INTO sources (id, name, type, url, description) VALUES
1940
+ ('official-registry', 'Official MCP Registry', 'mcp-registry', 'https://registry.modelcontextprotocol.io/v0/servers', 'The official Model Context Protocol server registry'),
1941
+ ('awesome-mcp-servers', 'Awesome MCP Servers', 'awesome-list', 'https://raw.githubusercontent.com/punkpeye/awesome-mcp-servers/main/README.md', 'Curated list of MCP servers by punkpeye'),
1942
+ ('npm-mcp', 'npm MCP Packages', 'npm-search', 'https://registry.npmjs.org/-/v1/search', 'Search npm packages for MCP servers'),
1943
+ ('github-mcp-topic', 'GitHub MCP Topic', 'github-topic', 'https://api.github.com/search/repositories', 'GitHub repositories tagged with mcp-server topic')
1944
+ `);
1945
+ }
1946
+ return db;
1947
+ }
1948
+ function closeDb() {
1949
+ if (db) {
1950
+ db.close();
1951
+ db = null;
1952
+ }
1953
+ }
1954
+ var db = null;
1955
+ var init_db = __esm(() => {
1956
+ init_config();
1957
+ });
1958
+
1870
1959
  // node_modules/ajv/dist/compile/codegen/code.js
1871
1960
  var require_code = __commonJS((exports) => {
1872
1961
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -8126,7 +8215,7 @@ var require_formats = __commonJS((exports) => {
8126
8215
  }
8127
8216
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
8128
8217
  function getTime(strictTimeZone) {
8129
- return function time3(str) {
8218
+ return function time(str) {
8130
8219
  const matches = TIME.exec(str);
8131
8220
  if (!matches)
8132
8221
  return false;
@@ -8795,6 +8884,361 @@ var require_cross_spawn = __commonJS((exports, module) => {
8795
8884
  module.exports._enoent = enoent;
8796
8885
  });
8797
8886
 
8887
+ // src/lib/sources.ts
8888
+ var exports_sources = {};
8889
+ __export(exports_sources, {
8890
+ searchSource: () => searchSource,
8891
+ removeSource: () => removeSource,
8892
+ listSources: () => listSources,
8893
+ getSource: () => getSource,
8894
+ findServers: () => findServers,
8895
+ enableSource: () => enableSource,
8896
+ disableSource: () => disableSource,
8897
+ clearCache: () => clearCache,
8898
+ addSource: () => addSource
8899
+ });
8900
+ import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
8901
+ import { join as join2 } from "path";
8902
+ function getCacheFile(sourceId) {
8903
+ return join2(CACHE_DIR, `${sourceId}.json`);
8904
+ }
8905
+ function readCache(sourceId) {
8906
+ try {
8907
+ const file = getCacheFile(sourceId);
8908
+ if (!existsSync(file))
8909
+ return null;
8910
+ const data = JSON.parse(readFileSync(file, "utf-8"));
8911
+ return data;
8912
+ } catch {
8913
+ return null;
8914
+ }
8915
+ }
8916
+ function writeCache(sourceId, results) {
8917
+ try {
8918
+ mkdirSync2(CACHE_DIR, { recursive: true });
8919
+ writeFileSync(getCacheFile(sourceId), JSON.stringify({ results, cachedAt: Date.now() }), "utf-8");
8920
+ } catch {}
8921
+ }
8922
+ function clearCache(sourceId) {
8923
+ try {
8924
+ if (!existsSync(CACHE_DIR))
8925
+ return;
8926
+ const files = readdirSync(CACHE_DIR);
8927
+ for (const file of files) {
8928
+ if (!file.endsWith(".json"))
8929
+ continue;
8930
+ if (!sourceId || file.startsWith(`${sourceId}.`)) {
8931
+ try {
8932
+ unlinkSync(join2(CACHE_DIR, file));
8933
+ } catch {}
8934
+ }
8935
+ }
8936
+ } catch {}
8937
+ }
8938
+ function generateId2(name) {
8939
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8940
+ }
8941
+ function listSources() {
8942
+ const db2 = getDb();
8943
+ const rows = db2.query("SELECT * FROM sources ORDER BY created_at ASC").all();
8944
+ return rows.map((r) => ({ ...r, enabled: r.enabled === 1 }));
8945
+ }
8946
+ function getSource(id) {
8947
+ const db2 = getDb();
8948
+ const row = db2.query("SELECT * FROM sources WHERE id = ?").get(id);
8949
+ if (!row)
8950
+ return null;
8951
+ return { ...row, enabled: row.enabled === 1 };
8952
+ }
8953
+ function addSource(opts) {
8954
+ const db2 = getDb();
8955
+ const id = generateId2(opts.name);
8956
+ db2.run("INSERT INTO sources (id, name, type, url, description) VALUES (?, ?, ?, ?, ?)", [id, opts.name, opts.type, opts.url, opts.description ?? null]);
8957
+ return getSource(id);
8958
+ }
8959
+ function removeSource(id) {
8960
+ const db2 = getDb();
8961
+ db2.run("DELETE FROM sources WHERE id = ?", [id]);
8962
+ }
8963
+ function enableSource(id) {
8964
+ const db2 = getDb();
8965
+ db2.run("UPDATE sources SET enabled = 1 WHERE id = ?", [id]);
8966
+ }
8967
+ function disableSource(id) {
8968
+ const db2 = getDb();
8969
+ db2.run("UPDATE sources SET enabled = 0 WHERE id = ?", [id]);
8970
+ }
8971
+ async function searchMcpRegistry(source, query) {
8972
+ try {
8973
+ const res = await fetch(source.url);
8974
+ if (!res.ok)
8975
+ return [];
8976
+ const data = await res.json();
8977
+ const q = query.toLowerCase();
8978
+ return (data.servers || []).map((e) => e.server).filter((s) => q === "" || s.name.toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q)).map((s) => ({
8979
+ name: s.name,
8980
+ description: s.description || "",
8981
+ source: "registry",
8982
+ sourceId: source.id,
8983
+ url: s.repository?.url,
8984
+ githubRepo: s.repository?.url,
8985
+ installCmd: s.packages?.[0] ? s.packages[0].registryType === "npm" ? `npx -y ${s.packages[0].identifier}` : s.packages[0].identifier : undefined,
8986
+ npmPackage: s.packages?.find((p) => p.registryType === "npm")?.identifier
8987
+ }));
8988
+ } catch {
8989
+ return [];
8990
+ }
8991
+ }
8992
+ async function searchAwesomeList(source, query) {
8993
+ try {
8994
+ const res = await fetch(source.url);
8995
+ if (!res.ok)
8996
+ return [];
8997
+ const text = await res.text();
8998
+ const results = [];
8999
+ const linkPattern = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)(?:\s*[-\u2013]\s*([^\n]+))?/g;
9000
+ const q = query.toLowerCase();
9001
+ let match;
9002
+ while ((match = linkPattern.exec(text)) !== null) {
9003
+ const name = match[1].trim();
9004
+ const url2 = match[2].trim();
9005
+ const description = match[3]?.trim() || "";
9006
+ if ((url2.includes("github.com") || url2.includes("npmjs.com")) && name.length > 2) {
9007
+ if (q === "" || `${name} ${description}`.toLowerCase().includes(q)) {
9008
+ results.push({
9009
+ name,
9010
+ description,
9011
+ source: "awesome",
9012
+ sourceId: source.id,
9013
+ url: url2,
9014
+ githubRepo: url2.includes("github.com") ? url2 : undefined,
9015
+ installCmd: url2.includes("npmjs.com") ? `npx -y ${url2.split("/").pop()}` : undefined
9016
+ });
9017
+ }
9018
+ }
9019
+ }
9020
+ return results;
9021
+ } catch {
9022
+ return [];
9023
+ }
9024
+ }
9025
+ function scoreNpmPackage(pkg) {
9026
+ const name = pkg.name.toLowerCase();
9027
+ const keywords = (pkg.keywords || []).map((k) => k.toLowerCase());
9028
+ let score = 0;
9029
+ if (name.includes("mcp-server"))
9030
+ score += 3;
9031
+ else if (name.startsWith("mcp-") || name.endsWith("-mcp"))
9032
+ score += 2;
9033
+ else if (name.includes("mcp"))
9034
+ score += 1;
9035
+ if (keywords.includes("mcp"))
9036
+ score += 1;
9037
+ if (keywords.includes("mcp-server"))
9038
+ score += 2;
9039
+ if (keywords.some((k) => k.includes("modelcontextprotocol")))
9040
+ score += 2;
9041
+ return score;
9042
+ }
9043
+ async function searchNpm(source, query) {
9044
+ try {
9045
+ const url2 = new URL(source.url);
9046
+ url2.searchParams.set("text", `mcp-server ${query}`);
9047
+ url2.searchParams.set("size", "50");
9048
+ const res = await fetch(url2.toString());
9049
+ if (!res.ok)
9050
+ return [];
9051
+ const data = await res.json();
9052
+ const q = query.toLowerCase();
9053
+ const scored = (data.objects || []).map((o) => ({ pkg: o.package, score: scoreNpmPackage(o.package) })).filter(({ pkg, score }) => {
9054
+ if (score < 1)
9055
+ return false;
9056
+ const text = `${pkg.name} ${pkg.description || ""} ${(pkg.keywords || []).join(" ")}`.toLowerCase();
9057
+ return q === "" || text.includes(q);
9058
+ });
9059
+ scored.sort((a, b) => b.score - a.score);
9060
+ return scored.map(({ pkg }) => ({
9061
+ name: pkg.name,
9062
+ description: pkg.description || "",
9063
+ source: "npm",
9064
+ sourceId: source.id,
9065
+ url: pkg.links?.repository || pkg.links?.npm,
9066
+ npmPackage: pkg.name,
9067
+ installCmd: `npx -y ${pkg.name}`
9068
+ }));
9069
+ } catch {
9070
+ return [];
9071
+ }
9072
+ }
9073
+ async function searchGitHubTopic(source, query) {
9074
+ const token = process.env.GITHUB_TOKEN;
9075
+ try {
9076
+ const url2 = new URL(source.url);
9077
+ const q = query ? `${query} topic:mcp-server` : "topic:mcp-server";
9078
+ url2.searchParams.set("q", q);
9079
+ url2.searchParams.set("sort", "stars");
9080
+ url2.searchParams.set("per_page", "30");
9081
+ const headers = {
9082
+ Accept: "application/vnd.github.v3+json"
9083
+ };
9084
+ if (token)
9085
+ headers["Authorization"] = `Bearer ${token}`;
9086
+ const res = await fetch(url2.toString(), { headers });
9087
+ if (!res.ok)
9088
+ return [];
9089
+ const data = await res.json();
9090
+ return (data.items || []).map((repo) => ({
9091
+ name: repo.full_name,
9092
+ description: repo.description || "",
9093
+ source: "github",
9094
+ sourceId: source.id,
9095
+ url: repo.html_url,
9096
+ githubRepo: repo.html_url,
9097
+ stars: repo.stargazers_count
9098
+ }));
9099
+ } catch {
9100
+ return [];
9101
+ }
9102
+ }
9103
+ async function fetchSource(source) {
9104
+ switch (source.type) {
9105
+ case "mcp-registry":
9106
+ return searchMcpRegistry(source, "");
9107
+ case "awesome-list":
9108
+ return searchAwesomeList(source, "");
9109
+ case "npm-search":
9110
+ return searchNpm(source, "");
9111
+ case "github-topic":
9112
+ return searchGitHubTopic(source, "");
9113
+ default:
9114
+ return [];
9115
+ }
9116
+ }
9117
+ function filterResults(results, query) {
9118
+ if (!query)
9119
+ return results;
9120
+ const q = query.toLowerCase();
9121
+ return results.filter((r) => {
9122
+ const text = `${r.name} ${r.description || ""}`.toLowerCase();
9123
+ return text.includes(q);
9124
+ });
9125
+ }
9126
+ async function searchSource(source, query, noCache = false) {
9127
+ if (!noCache) {
9128
+ const cached2 = readCache(source.id);
9129
+ if (cached2 && Date.now() - cached2.cachedAt < DEFAULT_TTL_MS) {
9130
+ return filterResults(cached2.results, query);
9131
+ }
9132
+ }
9133
+ const results = await fetchSource(source);
9134
+ writeCache(source.id, results);
9135
+ return filterResults(results, query);
9136
+ }
9137
+ function deduplicate(results) {
9138
+ const seen = new Set;
9139
+ return results.filter((r) => {
9140
+ const key = r.npmPackage || r.githubRepo || r.name.toLowerCase();
9141
+ if (seen.has(key))
9142
+ return false;
9143
+ seen.add(key);
9144
+ return true;
9145
+ });
9146
+ }
9147
+ async function findServers(query, opts = {}) {
9148
+ let sources = listSources().filter((s) => s.enabled);
9149
+ if (opts.sources && opts.sources.length > 0) {
9150
+ sources = sources.filter((s) => opts.sources.includes(s.id));
9151
+ }
9152
+ const limit = opts.limit ?? 20;
9153
+ const all = await Promise.all(sources.map((s) => searchSource(s, query, opts.noCache)));
9154
+ const flat = all.flat();
9155
+ const deduped = deduplicate(flat);
9156
+ const typeOrder = {
9157
+ "mcp-registry": 0,
9158
+ "awesome-list": 1,
9159
+ "npm-search": 2,
9160
+ "github-topic": 3
9161
+ };
9162
+ const sourceTypeMap = new Map(sources.map((s) => [s.id, s.type]));
9163
+ deduped.sort((a, b) => {
9164
+ const aType = a.sourceId ? sourceTypeMap.get(a.sourceId) ?? "" : a.source;
9165
+ const bType = b.sourceId ? sourceTypeMap.get(b.sourceId) ?? "" : b.source;
9166
+ const ao = typeOrder[aType] ?? 99;
9167
+ const bo = typeOrder[bType] ?? 99;
9168
+ if (ao !== bo)
9169
+ return ao - bo;
9170
+ return (b.stars ?? 0) - (a.stars ?? 0);
9171
+ });
9172
+ return deduped.slice(0, limit * Math.max(sources.length, 1));
9173
+ }
9174
+ var CACHE_DIR, DEFAULT_TTL_MS;
9175
+ var init_sources = __esm(() => {
9176
+ init_db();
9177
+ init_config();
9178
+ CACHE_DIR = join2(MCPS_DIR, "cache");
9179
+ DEFAULT_TTL_MS = 10 * 60 * 1000;
9180
+ });
9181
+
9182
+ // package.json
9183
+ var require_package = __commonJS((exports, module) => {
9184
+ module.exports = {
9185
+ name: "@hasna/mcps",
9186
+ version: "0.0.4",
9187
+ description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
9188
+ type: "module",
9189
+ main: "dist/index.js",
9190
+ types: "dist/index.d.ts",
9191
+ exports: {
9192
+ ".": {
9193
+ import: "./dist/index.js",
9194
+ types: "./dist/index.d.ts"
9195
+ }
9196
+ },
9197
+ bin: {
9198
+ mcps: "bin/index.js",
9199
+ "mcps-mcp": "bin/mcp.js"
9200
+ },
9201
+ files: [
9202
+ "dist/",
9203
+ "bin/",
9204
+ "dashboard/dist/",
9205
+ "LICENSE",
9206
+ "README.md"
9207
+ ],
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",
9210
+ "build:dashboard": "cd dashboard && bun install && bun run build",
9211
+ dev: "bun run src/cli/index.tsx",
9212
+ "dev:mcp": "bun run src/mcp/index.ts",
9213
+ "dev:dashboard": "cd dashboard && bun run dev",
9214
+ serve: "bun run src/cli/index.tsx serve",
9215
+ test: "bun test",
9216
+ typecheck: "tsc --noEmit",
9217
+ prepublishOnly: "bun run build"
9218
+ },
9219
+ dependencies: {
9220
+ "@modelcontextprotocol/sdk": "^1.26.0",
9221
+ chalk: "^5.3.0",
9222
+ commander: "^12.1.0",
9223
+ ink: "^5.0.1",
9224
+ "ink-select-input": "^6.0.0",
9225
+ "ink-spinner": "^5.0.0",
9226
+ "ink-text-input": "^6.0.0",
9227
+ react: "^18.2.0",
9228
+ zod: "^3.23.0"
9229
+ },
9230
+ devDependencies: {
9231
+ "@types/bun": "latest",
9232
+ "@types/react": "^18.2.0",
9233
+ typescript: "^5"
9234
+ },
9235
+ engines: {
9236
+ bun: ">=1.0.0"
9237
+ },
9238
+ license: "Apache-2.0"
9239
+ };
9240
+ });
9241
+
8798
9242
  // node_modules/cli-spinners/spinners.json
8799
9243
  var require_spinners = __commonJS((exports, module) => {
8800
9244
  module.exports = {
@@ -10455,93 +10899,90 @@ var {
10455
10899
  import React10 from "react";
10456
10900
  import { render } from "ink";
10457
10901
  import chalk2 from "chalk";
10458
-
10459
- // src/lib/db.ts
10460
- import { Database } from "bun:sqlite";
10461
- import { mkdirSync } from "fs";
10462
-
10463
- // src/lib/config.ts
10464
- import { join } from "path";
10465
- import { homedir } from "os";
10466
- var MCPS_DIR = join(homedir(), ".mcps");
10467
- var DB_PATH = join(MCPS_DIR, "registry.db");
10468
- var REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers";
10469
- var TOOL_PREFIX_SEPARATOR = "__";
10470
-
10471
- // src/lib/db.ts
10472
- var db = null;
10473
- function getDb() {
10474
- if (db)
10475
- return db;
10476
- mkdirSync(MCPS_DIR, { recursive: true });
10477
- db = new Database(DB_PATH, { create: true });
10478
- db.exec("PRAGMA journal_mode = WAL");
10479
- db.exec("PRAGMA busy_timeout = 5000");
10480
- db.exec("PRAGMA foreign_keys = ON");
10481
- db.exec(`
10482
- CREATE TABLE IF NOT EXISTS servers (
10483
- id TEXT PRIMARY KEY,
10484
- name TEXT NOT NULL,
10485
- description TEXT,
10486
- command TEXT NOT NULL,
10487
- args TEXT NOT NULL DEFAULT '[]',
10488
- env TEXT NOT NULL DEFAULT '{}',
10489
- transport TEXT NOT NULL DEFAULT 'stdio',
10490
- url TEXT,
10491
- source TEXT NOT NULL DEFAULT 'local',
10492
- enabled INTEGER NOT NULL DEFAULT 1,
10493
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
10494
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10495
- )
10496
- `);
10497
- db.exec(`
10498
- CREATE TABLE IF NOT EXISTS tool_cache (
10499
- server_id TEXT NOT NULL,
10500
- name TEXT NOT NULL,
10501
- description TEXT NOT NULL DEFAULT '',
10502
- input_schema TEXT NOT NULL DEFAULT '{}',
10503
- cached_at TEXT NOT NULL DEFAULT (datetime('now')),
10504
- PRIMARY KEY (server_id, name),
10505
- FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
10506
- )
10507
- `);
10508
- db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
10509
- return db;
10510
- }
10511
- function closeDb() {
10512
- if (db) {
10513
- db.close();
10514
- db = null;
10515
- }
10516
- }
10902
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
10903
+ import { join as join6, dirname as dirname4 } from "path";
10904
+ import { fileURLToPath as fileURLToPath3 } from "url";
10517
10905
 
10518
10906
  // src/lib/registry.ts
10907
+ init_db();
10519
10908
  function parseRow(row) {
10520
10909
  return {
10521
10910
  id: row.id,
10522
10911
  name: row.name,
10523
10912
  description: row.description || null,
10524
10913
  command: row.command,
10525
- args: JSON.parse(row.args),
10526
- env: JSON.parse(row.env),
10914
+ args: safeJsonParse(row.args, []),
10915
+ env: safeJsonParse(row.env, {}),
10527
10916
  transport: row.transport,
10528
10917
  url: row.url || null,
10529
10918
  source: row.source,
10530
10919
  enabled: row.enabled === 1,
10531
10920
  created_at: row.created_at,
10532
- updated_at: row.updated_at
10921
+ updated_at: row.updated_at,
10922
+ last_connected_at: row.last_connected_at ?? null,
10923
+ last_error: row.last_error ?? null
10533
10924
  };
10534
10925
  }
10926
+ function safeJsonParse(value, fallback) {
10927
+ if (typeof value !== "string")
10928
+ return fallback;
10929
+ try {
10930
+ return JSON.parse(value);
10931
+ } catch {
10932
+ return fallback;
10933
+ }
10934
+ }
10535
10935
  function generateId(name) {
10536
10936
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
10537
10937
  }
10938
+ function normalizeCandidate(value) {
10939
+ const trimmed = value?.trim();
10940
+ return trimmed ? trimmed : undefined;
10941
+ }
10942
+ function pickNameFromArgs(args) {
10943
+ if (!args || args.length === 0)
10944
+ return;
10945
+ const ddIndex = args.indexOf("--");
10946
+ if (ddIndex >= 0 && ddIndex < args.length - 1) {
10947
+ const after = normalizeCandidate(args[ddIndex + 1]);
10948
+ if (after)
10949
+ return after;
10950
+ }
10951
+ for (const arg of args) {
10952
+ const candidate = normalizeCandidate(arg);
10953
+ if (!candidate)
10954
+ continue;
10955
+ if (candidate.startsWith("-"))
10956
+ continue;
10957
+ return candidate;
10958
+ }
10959
+ return;
10960
+ }
10961
+ function pickId(candidates) {
10962
+ for (const candidate of candidates) {
10963
+ if (!candidate)
10964
+ continue;
10965
+ const id = generateId(candidate);
10966
+ if (id)
10967
+ return id;
10968
+ }
10969
+ return null;
10970
+ }
10538
10971
  function addServer(opts) {
10539
10972
  const db2 = getDb();
10540
- const name = opts.name || opts.args?.[0] || opts.command;
10541
- const id = generateId(name);
10973
+ const command = normalizeCandidate(opts.command);
10974
+ if (!command) {
10975
+ throw new Error("Command is required");
10976
+ }
10977
+ const argName = pickNameFromArgs(opts.args);
10978
+ const name = normalizeCandidate(opts.name) || argName || command;
10979
+ const id = pickId([normalizeCandidate(opts.name), argName, command]) || null;
10980
+ if (!id) {
10981
+ throw new Error("Unable to generate a valid server ID");
10982
+ }
10542
10983
  const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
10543
10984
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
10544
- RETURNING *`).get(id, name, opts.description || null, opts.command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
10985
+ RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
10545
10986
  return parseRow(row);
10546
10987
  }
10547
10988
  function removeServer(id) {
@@ -10597,6 +11038,9 @@ function updateServer(id, updates) {
10597
11038
  sets.push("updated_at = datetime('now')");
10598
11039
  values.push(id);
10599
11040
  const row = db2.prepare(`UPDATE servers SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...values);
11041
+ if (!row) {
11042
+ throw new Error(`Server "${id}" not found`);
11043
+ }
10600
11044
  return parseRow(row);
10601
11045
  }
10602
11046
  function enableServer(id) {
@@ -10605,84 +11049,79 @@ function enableServer(id) {
10605
11049
  function disableServer(id) {
10606
11050
  return updateServer(id, { enabled: false });
10607
11051
  }
11052
+ function setServerEnv(id, key, value) {
11053
+ const db2 = getDb();
11054
+ const server = getServer(id);
11055
+ if (!server)
11056
+ throw new Error(`Server "${id}" not found`);
11057
+ const env = { ...server.env, [key]: value };
11058
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
11059
+ }
11060
+ function unsetServerEnv(id, key) {
11061
+ const db2 = getDb();
11062
+ const server = getServer(id);
11063
+ if (!server)
11064
+ throw new Error(`Server "${id}" not found`);
11065
+ const env = { ...server.env };
11066
+ delete env[key];
11067
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
11068
+ }
10608
11069
  function cacheTools(serverId, tools) {
10609
11070
  const db2 = getDb();
10610
- db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
10611
11071
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
11072
+ const uniqueTools = [];
11073
+ const seen = new Set;
10612
11074
  for (const tool of tools) {
10613
- insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
11075
+ const name = tool.name?.trim();
11076
+ if (!name || seen.has(name))
11077
+ continue;
11078
+ seen.add(name);
11079
+ uniqueTools.push({
11080
+ name,
11081
+ description: tool.description || "",
11082
+ input_schema: tool.input_schema || {}
11083
+ });
10614
11084
  }
11085
+ const run = db2.transaction((rows) => {
11086
+ db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
11087
+ for (const tool of rows) {
11088
+ insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
11089
+ }
11090
+ });
11091
+ run(uniqueTools);
11092
+ }
11093
+ function getToolCounts() {
11094
+ const db2 = getDb();
11095
+ const rows = db2.prepare("SELECT server_id, COUNT(*) as count FROM tool_cache GROUP BY server_id").all();
11096
+ return new Map(rows.map((row) => [row.server_id, Number(row.count)]));
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
+ });
10615
11112
  }
10616
11113
  function getCachedTools(serverId) {
10617
11114
  const db2 = getDb();
10618
- const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ?").all(serverId);
11115
+ const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
10619
11116
  return rows.map((r) => ({
10620
11117
  name: r.name,
10621
11118
  description: r.description,
10622
- input_schema: JSON.parse(r.input_schema)
11119
+ input_schema: safeJsonParse(r.input_schema, {})
10623
11120
  }));
10624
11121
  }
10625
11122
 
10626
- // src/lib/remote.ts
10627
- function parseRegistryEntry(entry) {
10628
- const s = entry.server;
10629
- return {
10630
- id: s.name,
10631
- name: s.name,
10632
- description: s.description || "",
10633
- repository: s.repository,
10634
- packages: s.packages || []
10635
- };
10636
- }
10637
- async function searchRegistry(query) {
10638
- const res = await fetch(REGISTRY_API_URL);
10639
- if (!res.ok) {
10640
- throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
10641
- }
10642
- const data = await res.json();
10643
- const entries = data.servers || [];
10644
- const q = query.toLowerCase();
10645
- return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
10646
- }
10647
- async function getRegistryServer(id) {
10648
- const res = await fetch(REGISTRY_API_URL);
10649
- if (!res.ok) {
10650
- throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
10651
- }
10652
- const data = await res.json();
10653
- const entries = data.servers || [];
10654
- const all = entries.map(parseRegistryEntry);
10655
- return all.find((s) => s.id === id) || null;
10656
- }
10657
- async function installFromRegistry(id) {
10658
- const server = await getRegistryServer(id);
10659
- if (!server) {
10660
- throw new Error(`Server "${id}" not found in registry`);
10661
- }
10662
- const pkg = server.packages?.[0];
10663
- let command = "npx";
10664
- let args = [];
10665
- let transport = "stdio";
10666
- if (pkg) {
10667
- if (pkg.registryType === "npm") {
10668
- command = "npx";
10669
- args = ["-y", pkg.identifier];
10670
- } else {
10671
- command = pkg.identifier;
10672
- }
10673
- if (pkg.transport?.type) {
10674
- transport = pkg.transport.type;
10675
- }
10676
- }
10677
- return addServer({
10678
- name: server.name,
10679
- description: server.description,
10680
- command,
10681
- args,
10682
- transport,
10683
- source: "registry"
10684
- });
10685
- }
11123
+ // src/lib/doctor.ts
11124
+ import { execFileSync } from "child_process";
10686
11125
 
10687
11126
  // node_modules/zod/v3/external.js
10688
11127
  var exports_external = {};
@@ -24909,54 +25348,127 @@ class StreamableHTTPClientTransport {
24909
25348
  }
24910
25349
 
24911
25350
  // src/lib/proxy.ts
25351
+ init_config();
25352
+ init_db();
24912
25353
  var connections = new Map;
25354
+ var inflightConnections = new Map;
25355
+ var CONNECT_CONCURRENCY = 4;
25356
+ function buildEnv(extra) {
25357
+ const merged = {};
25358
+ for (const [key, value] of Object.entries(process.env)) {
25359
+ if (typeof value === "string")
25360
+ merged[key] = value;
25361
+ }
25362
+ for (const [key, value] of Object.entries(extra || {})) {
25363
+ if (value === undefined || value === null)
25364
+ continue;
25365
+ merged[key] = String(value);
25366
+ }
25367
+ return merged;
25368
+ }
25369
+ function requireUrl(entry) {
25370
+ if (!entry.url) {
25371
+ throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
25372
+ }
25373
+ try {
25374
+ return new URL(entry.url);
25375
+ } catch {
25376
+ throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
25377
+ }
25378
+ }
24913
25379
  async function connectToServer(entry) {
24914
25380
  if (connections.has(entry.id)) {
24915
25381
  return connections.get(entry.id);
24916
25382
  }
25383
+ const inflight = inflightConnections.get(entry.id);
25384
+ if (inflight) {
25385
+ return inflight;
25386
+ }
24917
25387
  const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
24918
25388
  let transport;
24919
- if (entry.transport === "stdio") {
24920
- transport = new StdioClientTransport({
24921
- command: entry.command,
24922
- args: entry.args,
24923
- env: { ...process.env, ...entry.env }
24924
- });
24925
- } else if (entry.transport === "sse") {
24926
- transport = new SSEClientTransport(new URL(entry.url));
24927
- } else {
24928
- transport = new StreamableHTTPClientTransport(new URL(entry.url));
24929
- }
24930
- await client.connect(transport);
24931
- const result = await client.listTools();
24932
- const tools = (result.tools || []).map((t) => ({
24933
- server_id: entry.id,
24934
- name: t.name,
24935
- description: t.description || "",
24936
- input_schema: t.inputSchema || {}
24937
- }));
24938
- cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
24939
- const connected = {
24940
- entry,
24941
- tools,
24942
- disconnect: async () => {
24943
- await client.close();
24944
- connections.delete(entry.id);
25389
+ const connectPromise = (async () => {
25390
+ try {
25391
+ if (entry.transport === "stdio") {
25392
+ if (!entry.command?.trim()) {
25393
+ throw new Error(`Server "${entry.id}" is missing a command`);
25394
+ }
25395
+ transport = new StdioClientTransport({
25396
+ command: entry.command,
25397
+ args: entry.args,
25398
+ env: buildEnv(entry.env)
25399
+ });
25400
+ } else if (entry.transport === "sse") {
25401
+ transport = new SSEClientTransport(requireUrl(entry));
25402
+ } else {
25403
+ transport = new StreamableHTTPClientTransport(requireUrl(entry));
25404
+ }
25405
+ await client.connect(transport);
25406
+ const result = await client.listTools();
25407
+ const tools = (result.tools || []).map((t) => ({
25408
+ server_id: entry.id,
25409
+ name: t.name,
25410
+ description: t.description || "",
25411
+ input_schema: t.inputSchema || {}
25412
+ }));
25413
+ cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
25414
+ const connected = {
25415
+ entry,
25416
+ tools,
25417
+ disconnect: async () => {
25418
+ try {
25419
+ await client.close();
25420
+ } finally {
25421
+ connections.delete(entry.id);
25422
+ }
25423
+ }
25424
+ };
25425
+ connected._client = client;
25426
+ connections.set(entry.id, connected);
25427
+ try {
25428
+ getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
25429
+ } catch {}
25430
+ return connected;
25431
+ } catch (err) {
25432
+ try {
25433
+ getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
25434
+ } catch {}
25435
+ try {
25436
+ await client.close();
25437
+ } catch {}
25438
+ throw err;
24945
25439
  }
24946
- };
24947
- connected._client = client;
24948
- connections.set(entry.id, connected);
24949
- return connected;
25440
+ })();
25441
+ inflightConnections.set(entry.id, connectPromise);
25442
+ try {
25443
+ return await connectPromise;
25444
+ } finally {
25445
+ inflightConnections.delete(entry.id);
25446
+ }
24950
25447
  }
24951
25448
  async function disconnectServer(id) {
24952
25449
  const conn = connections.get(id);
24953
25450
  if (conn) {
24954
25451
  await conn.disconnect();
25452
+ return;
25453
+ }
25454
+ const inflight = inflightConnections.get(id);
25455
+ if (inflight) {
25456
+ try {
25457
+ const pending = await inflight;
25458
+ await pending.disconnect();
25459
+ } catch {}
24955
25460
  }
24956
25461
  }
24957
25462
  async function disconnectAll() {
24958
25463
  const ids = Array.from(connections.keys());
24959
- await Promise.all(ids.map((id) => disconnectServer(id)));
25464
+ await Promise.allSettled(ids.map((id) => disconnectServer(id)));
25465
+ const inflight = Array.from(inflightConnections.values());
25466
+ await Promise.allSettled(inflight.map(async (promise2) => {
25467
+ try {
25468
+ const conn = await promise2;
25469
+ await conn.disconnect();
25470
+ } catch {}
25471
+ }));
24960
25472
  }
24961
25473
  function listAllTools() {
24962
25474
  const tools = [];
@@ -24971,12 +25483,19 @@ function listAllTools() {
24971
25483
  return tools;
24972
25484
  }
24973
25485
  async function callTool(prefixedName, args) {
24974
- const sepIdx = prefixedName.indexOf(TOOL_PREFIX_SEPARATOR);
25486
+ const normalized = prefixedName.trim();
25487
+ if (!normalized) {
25488
+ throw new Error("Tool name is required");
25489
+ }
25490
+ const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
24975
25491
  if (sepIdx === -1) {
24976
25492
  throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
24977
25493
  }
24978
- const serverId = prefixedName.slice(0, sepIdx);
24979
- const toolName = prefixedName.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length);
25494
+ const serverId = normalized.slice(0, sepIdx).trim();
25495
+ const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
25496
+ if (!serverId || !toolName) {
25497
+ throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
25498
+ }
24980
25499
  const conn = connections.get(serverId);
24981
25500
  if (!conn) {
24982
25501
  throw new Error(`Server "${serverId}" is not connected`);
@@ -24992,17 +25511,247 @@ async function callTool(prefixedName, args) {
24992
25511
  async function connectAllEnabled() {
24993
25512
  const servers = listServers().filter((s) => s.enabled);
24994
25513
  const results = [];
24995
- for (const server of servers) {
25514
+ let index = 0;
25515
+ const workerCount = Math.min(CONNECT_CONCURRENCY, servers.length);
25516
+ const workers = Array.from({ length: workerCount }, async () => {
25517
+ while (true) {
25518
+ const current = index++;
25519
+ if (current >= servers.length)
25520
+ return;
25521
+ const server = servers[current];
25522
+ try {
25523
+ const conn = await connectToServer(server);
25524
+ results.push(conn);
25525
+ } catch (err) {
25526
+ console.error(`Failed to connect to ${server.name}: ${err.message}`);
25527
+ }
25528
+ }
25529
+ });
25530
+ await Promise.all(workers);
25531
+ return results;
25532
+ }
25533
+
25534
+ // src/lib/doctor.ts
25535
+ async function diagnoseServer(server) {
25536
+ const checks4 = [];
25537
+ if (server.transport === "stdio") {
25538
+ try {
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})` : ""}` });
25546
+ } catch {
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
+ });
25554
+ }
25555
+ }
25556
+ const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
25557
+ if (Object.keys(server.env).length === 0) {
25558
+ checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
25559
+ } else if (missingEnv.length > 0) {
25560
+ checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
25561
+ } else {
25562
+ checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
25563
+ }
25564
+ if (server.transport !== "stdio" && server.url) {
24996
25565
  try {
24997
- const conn = await connectToServer(server);
24998
- results.push(conn);
25566
+ const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
25567
+ checks4.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
24999
25568
  } catch (err) {
25000
- console.error(`Failed to connect to ${server.name}: ${err.message}`);
25569
+ checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
25001
25570
  }
25002
25571
  }
25003
- return results;
25572
+ if (server.enabled) {
25573
+ try {
25574
+ await Promise.race([
25575
+ connectToServer(server),
25576
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
25577
+ ]);
25578
+ await disconnectServer(server.id);
25579
+ checks4.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
25580
+ } catch (err) {
25581
+ checks4.push({ name: "connect & list tools", pass: false, message: err.message });
25582
+ }
25583
+ } else {
25584
+ checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
25585
+ }
25586
+ return {
25587
+ server,
25588
+ checks: checks4,
25589
+ healthy: checks4.every((c) => c.pass)
25590
+ };
25004
25591
  }
25005
25592
 
25593
+ // src/lib/remote.ts
25594
+ init_config();
25595
+ function parseRegistryEntry(entry) {
25596
+ const s = entry.server;
25597
+ return {
25598
+ id: s.name,
25599
+ name: s.name,
25600
+ description: s.description || "",
25601
+ repository: s.repository,
25602
+ packages: s.packages || []
25603
+ };
25604
+ }
25605
+ async function searchRegistry(query) {
25606
+ const res = await fetch(REGISTRY_API_URL);
25607
+ if (!res.ok) {
25608
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
25609
+ }
25610
+ const data = await res.json();
25611
+ const entries = data.servers || [];
25612
+ const q = query.toLowerCase();
25613
+ return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
25614
+ }
25615
+ async function getRegistryServer(id) {
25616
+ const res = await fetch(REGISTRY_API_URL);
25617
+ if (!res.ok) {
25618
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
25619
+ }
25620
+ const data = await res.json();
25621
+ const entries = data.servers || [];
25622
+ const all = entries.map(parseRegistryEntry);
25623
+ return all.find((s) => s.id === id) || null;
25624
+ }
25625
+ async function installFromRegistry(id) {
25626
+ const server = await getRegistryServer(id);
25627
+ if (!server) {
25628
+ throw new Error(`Server "${id}" not found in registry`);
25629
+ }
25630
+ const pkg = server.packages?.[0];
25631
+ let command = "npx";
25632
+ let args = [];
25633
+ let transport = "stdio";
25634
+ if (pkg) {
25635
+ if (pkg.registryType === "npm") {
25636
+ command = "npx";
25637
+ args = ["-y", pkg.identifier];
25638
+ } else {
25639
+ command = pkg.identifier;
25640
+ }
25641
+ if (pkg.transport?.type) {
25642
+ transport = pkg.transport.type;
25643
+ }
25644
+ }
25645
+ return addServer({
25646
+ name: server.name,
25647
+ description: server.description,
25648
+ command,
25649
+ args,
25650
+ transport,
25651
+ source: "registry"
25652
+ });
25653
+ }
25654
+
25655
+ // src/lib/finder.ts
25656
+ init_sources();
25657
+ async function listAwesomeServers() {
25658
+ const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
25659
+ const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
25660
+ if (!source)
25661
+ return [];
25662
+ return searchSource2(source, "");
25663
+ }
25664
+
25665
+ // src/cli/index.tsx
25666
+ init_sources();
25667
+
25668
+ // src/lib/install.ts
25669
+ import { execFileSync as execFileSync2 } from "child_process";
25670
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
25671
+ import { join as join3 } from "path";
25672
+ import { homedir as homedir2 } from "os";
25673
+ function installToClaude(entry) {
25674
+ try {
25675
+ const args = [
25676
+ "mcp",
25677
+ "add",
25678
+ "--transport",
25679
+ entry.transport,
25680
+ "--scope",
25681
+ "user"
25682
+ ];
25683
+ for (const [k, v] of Object.entries(entry.env)) {
25684
+ args.push("--env", `${k}=${v}`);
25685
+ }
25686
+ args.push(entry.id, "--", entry.command, ...entry.args);
25687
+ execFileSync2("claude", args, { stdio: "pipe" });
25688
+ return { agent: "claude", success: true };
25689
+ } catch (err) {
25690
+ return { agent: "claude", success: false, error: err.message };
25691
+ }
25692
+ }
25693
+ function installToCodex(entry) {
25694
+ try {
25695
+ const configDir = join3(homedir2(), ".codex");
25696
+ const configPath = join3(configDir, "config.toml");
25697
+ if (!existsSync2(configDir)) {
25698
+ mkdirSync3(configDir, { recursive: true });
25699
+ }
25700
+ const block = `
25701
+ [mcp_servers.${entry.id}]
25702
+ ` + `command = ${JSON.stringify(entry.command)}
25703
+ ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
25704
+ `;
25705
+ const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
25706
+ if (existing.includes(`[mcp_servers.${entry.id}]`)) {
25707
+ return { agent: "codex", success: true };
25708
+ }
25709
+ writeFileSync2(configPath, existing + block, "utf-8");
25710
+ return { agent: "codex", success: true };
25711
+ } catch (err) {
25712
+ return { agent: "codex", success: false, error: err.message };
25713
+ }
25714
+ }
25715
+ function installToGemini(entry) {
25716
+ try {
25717
+ const configDir = join3(homedir2(), ".gemini");
25718
+ const configPath = join3(configDir, "settings.json");
25719
+ if (!existsSync2(configDir)) {
25720
+ mkdirSync3(configDir, { recursive: true });
25721
+ }
25722
+ let settings = {};
25723
+ if (existsSync2(configPath)) {
25724
+ settings = JSON.parse(readFileSync2(configPath, "utf-8"));
25725
+ }
25726
+ if (!settings.mcpServers)
25727
+ settings.mcpServers = {};
25728
+ settings.mcpServers[entry.id] = {
25729
+ command: entry.command,
25730
+ args: entry.args,
25731
+ ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
25732
+ };
25733
+ writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
25734
+ return { agent: "gemini", success: true };
25735
+ } catch (err) {
25736
+ return { agent: "gemini", success: false, error: err.message };
25737
+ }
25738
+ }
25739
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
25740
+ return targets.map((target) => {
25741
+ if (target === "claude")
25742
+ return installToClaude(entry);
25743
+ if (target === "codex")
25744
+ return installToCodex(entry);
25745
+ if (target === "gemini")
25746
+ return installToGemini(entry);
25747
+ return { agent: target, success: false, error: "Unknown target" };
25748
+ });
25749
+ }
25750
+
25751
+ // src/cli/index.tsx
25752
+ init_db();
25753
+ import * as readline from "readline";
25754
+
25006
25755
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
25007
25756
  class ExperimentalServerTasks {
25008
25757
  constructor(_server) {
@@ -26224,14 +26973,31 @@ class StdioServerTransport {
26224
26973
  }
26225
26974
 
26226
26975
  // src/mcp/index.ts
26976
+ import { readFileSync as readFileSync3 } from "fs";
26977
+ import { join as join4, dirname as dirname2 } from "path";
26978
+ import { fileURLToPath } from "url";
26979
+ init_sources();
26980
+ init_config();
26981
+ function redactServerEnv(server) {
26982
+ return { ...server, env: {} };
26983
+ }
26984
+ var VERSION = (() => {
26985
+ try {
26986
+ const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
26987
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
26988
+ return pkg.version || "0.0.1";
26989
+ } catch {
26990
+ return "0.0.1";
26991
+ }
26992
+ })();
26227
26993
  var server = new McpServer({
26228
26994
  name: "mcps",
26229
- version: "0.0.1"
26995
+ version: VERSION
26230
26996
  });
26231
26997
  server.tool("list_servers", "List all registered MCP servers", {}, async () => {
26232
26998
  const servers = listServers();
26233
26999
  return {
26234
- content: [{ type: "text", text: JSON.stringify(servers, null, 2) }]
27000
+ content: [{ type: "text", text: JSON.stringify(servers.map(redactServerEnv), null, 2) }]
26235
27001
  };
26236
27002
  });
26237
27003
  server.tool("search_registry", "Search the official MCP registry for servers", { query: exports_external.string().describe("Search query") }, async ({ query }) => {
@@ -26282,17 +27048,84 @@ server.tool("remove_server", "Remove a registered MCP server", { id: exports_ext
26282
27048
  };
26283
27049
  });
26284
27050
  server.tool("enable_server", "Enable a registered MCP server", { id: exports_external.string().describe("Server ID to enable") }, async ({ id }) => {
27051
+ const existing = getServer(id);
27052
+ if (!existing) {
27053
+ return {
27054
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27055
+ isError: true
27056
+ };
27057
+ }
26285
27058
  const entry = enableServer(id);
26286
27059
  return {
26287
27060
  content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
26288
27061
  };
26289
27062
  });
26290
27063
  server.tool("disable_server", "Disable a registered MCP server", { id: exports_external.string().describe("Server ID to disable") }, async ({ id }) => {
27064
+ const existing = getServer(id);
27065
+ if (!existing) {
27066
+ return {
27067
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27068
+ isError: true
27069
+ };
27070
+ }
26291
27071
  const entry = disableServer(id);
26292
27072
  return {
26293
27073
  content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
26294
27074
  };
26295
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
+ });
26296
27129
  server.tool("get_server_info", "Get detailed information about a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
26297
27130
  const entry = getServer(id);
26298
27131
  if (!entry) {
@@ -26302,12 +27135,108 @@ server.tool("get_server_info", "Get detailed information about a registered MCP
26302
27135
  };
26303
27136
  }
26304
27137
  return {
26305
- content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
27138
+ content: [{ type: "text", text: JSON.stringify(redactServerEnv(entry), null, 2) }]
26306
27139
  };
26307
27140
  });
26308
- server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
26309
- await connectAllEnabled();
26310
- const tools = listAllTools();
27141
+ server.tool("find_mcp_servers", "Search for MCP servers across configured sources (official registry, npm, GitHub topics, awesome lists). Use list_sources to see available source IDs.", {
27142
+ query: exports_external.string().describe("Search query (e.g. 'filesystem', 'postgres', 'browser')"),
27143
+ sources: exports_external.array(exports_external.string()).optional().describe("Source IDs to search (default: all enabled). Use list_sources to get IDs."),
27144
+ limit: exports_external.number().optional().describe("Max results per source (default: 20)")
27145
+ }, async ({ query, sources, limit }) => {
27146
+ const results = await findServers(query, { sources, limit });
27147
+ return {
27148
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
27149
+ };
27150
+ });
27151
+ server.tool("list_sources", "List all configured search sources for finding MCP servers", {}, async () => {
27152
+ const sources = listSources();
27153
+ return {
27154
+ content: [{ type: "text", text: JSON.stringify(sources, null, 2) }]
27155
+ };
27156
+ });
27157
+ server.tool("add_source", "Add a new search source for finding MCP servers", {
27158
+ name: exports_external.string().describe("Source name"),
27159
+ type: exports_external.enum(["mcp-registry", "awesome-list", "npm-search", "github-topic"]).describe("Source type"),
27160
+ url: exports_external.string().describe("Source URL endpoint"),
27161
+ description: exports_external.string().optional().describe("Description")
27162
+ }, async ({ name, type, url: url2, description }) => {
27163
+ const source = addSource({ name, type, url: url2, description });
27164
+ return {
27165
+ content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
27166
+ };
27167
+ });
27168
+ server.tool("remove_source", "Remove a search source by ID", { id: exports_external.string().describe("Source ID to remove") }, async ({ id }) => {
27169
+ const existing = getSource(id);
27170
+ if (!existing) {
27171
+ return {
27172
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
27173
+ isError: true
27174
+ };
27175
+ }
27176
+ removeSource(id);
27177
+ return {
27178
+ content: [{ type: "text", text: `Removed source: ${existing.name} [${id}]` }]
27179
+ };
27180
+ });
27181
+ server.tool("enable_source_finder", "Enable a search source", { id: exports_external.string().describe("Source ID to enable") }, async ({ id }) => {
27182
+ const existing = getSource(id);
27183
+ if (!existing) {
27184
+ return {
27185
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
27186
+ isError: true
27187
+ };
27188
+ }
27189
+ enableSource(id);
27190
+ return {
27191
+ content: [{ type: "text", text: `Enabled source: ${existing.name}` }]
27192
+ };
27193
+ });
27194
+ server.tool("disable_source_finder", "Disable a search source", { id: exports_external.string().describe("Source ID to disable") }, async ({ id }) => {
27195
+ const existing = getSource(id);
27196
+ if (!existing) {
27197
+ return {
27198
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
27199
+ isError: true
27200
+ };
27201
+ }
27202
+ disableSource(id);
27203
+ return {
27204
+ content: [{ type: "text", text: `Disabled source: ${existing.name}` }]
27205
+ };
27206
+ });
27207
+ server.tool("install_to_agents", "Install a registered MCP server into Claude Code, Codex, and/or Gemini", {
27208
+ id: exports_external.string().describe("Server ID to install (from list_servers)"),
27209
+ targets: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
27210
+ }, async ({ id, targets }) => {
27211
+ const entry = getServer(id);
27212
+ if (!entry) {
27213
+ return {
27214
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27215
+ isError: true
27216
+ };
27217
+ }
27218
+ const agentTargets = targets ?? ["claude", "codex", "gemini"];
27219
+ const results = installToAgents(entry, agentTargets);
27220
+ return {
27221
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
27222
+ };
27223
+ });
27224
+ server.tool("list_awesome_servers", "List all MCP servers from the curated punkpeye/awesome-mcp-servers GitHub list", {}, async () => {
27225
+ const results = await listAwesomeServers();
27226
+ return {
27227
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
27228
+ };
27229
+ });
27230
+ server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
27231
+ let tools = [];
27232
+ try {
27233
+ await connectAllEnabled();
27234
+ tools = listAllTools();
27235
+ } finally {
27236
+ await disconnectAll().catch(() => {
27237
+ return;
27238
+ });
27239
+ }
26311
27240
  return {
26312
27241
  content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
26313
27242
  };
@@ -26317,6 +27246,28 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
26317
27246
  arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
26318
27247
  }, async ({ tool_name, arguments: args }) => {
26319
27248
  try {
27249
+ const sepIdx = tool_name.indexOf(TOOL_PREFIX_SEPARATOR);
27250
+ if (sepIdx === -1) {
27251
+ return {
27252
+ content: [{ type: "text", text: `Error: Invalid tool name "${tool_name}"` }],
27253
+ isError: true
27254
+ };
27255
+ }
27256
+ const serverId = tool_name.slice(0, sepIdx);
27257
+ const entry = getServer(serverId);
27258
+ if (!entry) {
27259
+ return {
27260
+ content: [{ type: "text", text: `Error: Server "${serverId}" not found.` }],
27261
+ isError: true
27262
+ };
27263
+ }
27264
+ if (!entry.enabled) {
27265
+ return {
27266
+ content: [{ type: "text", text: `Error: Server "${serverId}" is disabled.` }],
27267
+ isError: true
27268
+ };
27269
+ }
27270
+ await connectToServer(entry);
26320
27271
  const result = await callTool(tool_name, args || {});
26321
27272
  return { content: result.content };
26322
27273
  } catch (err) {
@@ -26326,6 +27277,13 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
26326
27277
  };
26327
27278
  }
26328
27279
  });
27280
+ server.tool("diagnose_server", "Run health checks on a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
27281
+ const entry = getServer(id);
27282
+ if (!entry)
27283
+ return { content: [{ type: "text", text: `Server "${id}" not found.` }], isError: true };
27284
+ const report = await diagnoseServer(entry);
27285
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
27286
+ });
26329
27287
  async function startMcpServer() {
26330
27288
  const transport = new StdioServerTransport;
26331
27289
  await server.connect(transport);
@@ -26339,27 +27297,32 @@ if (isDirectRun) {
26339
27297
  }
26340
27298
 
26341
27299
  // src/server/serve.ts
26342
- import { existsSync } from "fs";
26343
- import { join as join2, dirname, extname } from "path";
26344
- import { fileURLToPath } from "url";
27300
+ import { existsSync as existsSync3 } from "fs";
27301
+ import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
27302
+ import { fileURLToPath as fileURLToPath2 } from "url";
27303
+ init_sources();
27304
+ init_db();
27305
+ function redactServer(server2) {
27306
+ return { ...server2, env: {} };
27307
+ }
26345
27308
  function resolveDashboardDir() {
26346
27309
  const candidates = [];
26347
27310
  try {
26348
- const scriptDir = dirname(fileURLToPath(import.meta.url));
26349
- candidates.push(join2(scriptDir, "..", "dashboard", "dist"));
26350
- candidates.push(join2(scriptDir, "..", "..", "dashboard", "dist"));
27311
+ const scriptDir = dirname3(fileURLToPath2(import.meta.url));
27312
+ candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
27313
+ candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
26351
27314
  } catch {}
26352
27315
  if (process.argv[1]) {
26353
- const mainDir = dirname(process.argv[1]);
26354
- candidates.push(join2(mainDir, "..", "dashboard", "dist"));
26355
- candidates.push(join2(mainDir, "..", "..", "dashboard", "dist"));
27316
+ const mainDir = dirname3(process.argv[1]);
27317
+ candidates.push(join5(mainDir, "..", "dashboard", "dist"));
27318
+ candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
26356
27319
  }
26357
- candidates.push(join2(process.cwd(), "dashboard", "dist"));
27320
+ candidates.push(join5(process.cwd(), "dashboard", "dist"));
26358
27321
  for (const candidate of candidates) {
26359
- if (existsSync(candidate))
27322
+ if (existsSync3(candidate))
26360
27323
  return candidate;
26361
27324
  }
26362
- return join2(process.cwd(), "dashboard", "dist");
27325
+ return join5(process.cwd(), "dashboard", "dist");
26363
27326
  }
26364
27327
  var MIME_TYPES = {
26365
27328
  ".html": "text/html; charset=utf-8",
@@ -26375,13 +27338,15 @@ var MIME_TYPES = {
26375
27338
  };
26376
27339
  var SECURITY_HEADERS = {
26377
27340
  "X-Content-Type-Options": "nosniff",
26378
- "X-Frame-Options": "DENY"
27341
+ "X-Frame-Options": "DENY",
27342
+ "Referrer-Policy": "no-referrer"
26379
27343
  };
26380
27344
  function json(data, status = 200, port) {
26381
27345
  return new Response(JSON.stringify(data), {
26382
27346
  status,
26383
27347
  headers: {
26384
27348
  "Content-Type": "application/json",
27349
+ "Cache-Control": "no-store",
26385
27350
  "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
26386
27351
  ...SECURITY_HEADERS
26387
27352
  }
@@ -26391,27 +27356,94 @@ function isValidId(id) {
26391
27356
  return /^[a-z0-9-]+$/.test(id);
26392
27357
  }
26393
27358
  var MAX_BODY_SIZE = 1024 * 1024;
27359
+ function isLoopbackHost(hostname2) {
27360
+ return hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1";
27361
+ }
27362
+ function isAllowedOrigin(req, port, host) {
27363
+ const origin = req.headers.get("origin");
27364
+ if (!origin)
27365
+ return true;
27366
+ if (origin === "null")
27367
+ return false;
27368
+ try {
27369
+ const url2 = new URL(origin);
27370
+ if (url2.port && url2.port !== String(port))
27371
+ return false;
27372
+ if (isLoopbackHost(url2.hostname))
27373
+ return true;
27374
+ const normalizedHost = host === "0.0.0.0" ? "127.0.0.1" : host;
27375
+ return url2.hostname === normalizedHost;
27376
+ } catch {
27377
+ return false;
27378
+ }
27379
+ }
27380
+ function isAuthorized(req, host) {
27381
+ const token = process.env.MCPS_API_TOKEN;
27382
+ if (!token) {
27383
+ return isLoopbackHost(host);
27384
+ }
27385
+ const auth2 = req.headers.get("authorization");
27386
+ if (auth2 === `Bearer ${token}`)
27387
+ return true;
27388
+ return isLoopbackHost(host);
27389
+ }
27390
+ function unauthorizedResponse(port) {
27391
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
27392
+ status: 401,
27393
+ headers: {
27394
+ "Content-Type": "application/json",
27395
+ "Cache-Control": "no-store",
27396
+ "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
27397
+ "WWW-Authenticate": "Bearer",
27398
+ ...SECURITY_HEADERS
27399
+ }
27400
+ });
27401
+ }
26394
27402
  function getAllServersWithToolCount() {
26395
27403
  const servers = listServers();
27404
+ if (servers.length === 0)
27405
+ return [];
27406
+ const counts = getToolCounts();
26396
27407
  return servers.map((s) => ({
26397
- ...s,
26398
- toolCount: getCachedTools(s.id).length
27408
+ ...redactServer(s),
27409
+ toolCount: counts.get(s.id) ?? 0
26399
27410
  }));
26400
27411
  }
26401
27412
  function serveStaticFile(filePath) {
26402
- if (!existsSync(filePath))
27413
+ if (!existsSync3(filePath))
26403
27414
  return null;
26404
27415
  const ext = extname(filePath);
26405
27416
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
26406
27417
  return new Response(Bun.file(filePath), {
26407
- headers: { "Content-Type": contentType }
27418
+ headers: { "Content-Type": contentType, ...SECURITY_HEADERS }
26408
27419
  });
26409
27420
  }
27421
+ function resolveStaticPath(baseDir, urlPath) {
27422
+ let decoded;
27423
+ try {
27424
+ decoded = decodeURIComponent(urlPath);
27425
+ } catch {
27426
+ return null;
27427
+ }
27428
+ const stripped = decoded.replace(/^\/+/, "");
27429
+ const resolved = resolve(baseDir, stripped);
27430
+ const rel = relative(baseDir, resolved);
27431
+ if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
27432
+ return null;
27433
+ }
27434
+ return resolved;
27435
+ }
27436
+ function formatHostForUrl(host) {
27437
+ if (host.includes(":") && !host.startsWith("["))
27438
+ return `[${host}]`;
27439
+ return host;
27440
+ }
26410
27441
  async function startServer(port, options) {
26411
27442
  const shouldOpen = options?.open ?? true;
27443
+ const host = options?.host ?? "127.0.0.1";
26412
27444
  getDb();
26413
27445
  const dashboardDir = resolveDashboardDir();
26414
- const dashboardExists = existsSync(dashboardDir);
27446
+ const dashboardExists = existsSync3(dashboardDir);
26415
27447
  if (!dashboardExists) {
26416
27448
  console.error(`
26417
27449
  Dashboard not found at: ${dashboardDir}`);
@@ -26422,10 +27454,19 @@ Dashboard not found at: ${dashboardDir}`);
26422
27454
  }
26423
27455
  const server2 = Bun.serve({
26424
27456
  port,
27457
+ hostname: host,
26425
27458
  async fetch(req) {
26426
27459
  const url2 = new URL(req.url);
26427
27460
  const path = url2.pathname;
26428
27461
  const method = req.method;
27462
+ if (path.startsWith("/api/") && method !== "OPTIONS") {
27463
+ if (!isAuthorized(req, host)) {
27464
+ return unauthorizedResponse(port);
27465
+ }
27466
+ if (!isAllowedOrigin(req, port, host)) {
27467
+ return json({ error: "Forbidden" }, 403, port);
27468
+ }
27469
+ }
26429
27470
  if (path === "/api/servers" && method === "GET") {
26430
27471
  return json(getAllServersWithToolCount(), 200, port);
26431
27472
  }
@@ -26434,15 +27475,39 @@ Dashboard not found at: ${dashboardDir}`);
26434
27475
  const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
26435
27476
  if (contentLength > MAX_BODY_SIZE)
26436
27477
  return json({ error: "Request body too large" }, 413, port);
26437
- const body = await req.json();
26438
- if (!body.command)
27478
+ let body;
27479
+ try {
27480
+ body = await req.json();
27481
+ } catch {
27482
+ return json({ error: "Invalid JSON body" }, 400, port);
27483
+ }
27484
+ const command = body.command?.trim();
27485
+ if (!command)
26439
27486
  return json({ error: "Missing 'command'" }, 400, port);
27487
+ const transport = body.transport || "stdio";
27488
+ if (!["stdio", "sse", "streamable-http"].includes(transport)) {
27489
+ return json({ error: "Invalid transport type" }, 400, port);
27490
+ }
27491
+ if (transport !== "stdio" && !body.url) {
27492
+ return json({ error: "Missing 'url' for non-stdio transport" }, 400, port);
27493
+ }
27494
+ if (body.url) {
27495
+ try {
27496
+ new URL(body.url);
27497
+ } catch {
27498
+ return json({ error: "Invalid 'url' format" }, 400, port);
27499
+ }
27500
+ }
27501
+ if (body.args && (!Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string"))) {
27502
+ return json({ error: "Invalid 'args' format" }, 400, port);
27503
+ }
27504
+ const args = body.args || [];
26440
27505
  const entry = addServer({
26441
27506
  name: body.name,
26442
- command: body.command,
26443
- args: body.args || [],
27507
+ command,
27508
+ args,
26444
27509
  description: body.description,
26445
- transport: body.transport || "stdio",
27510
+ transport,
26446
27511
  url: body.url
26447
27512
  });
26448
27513
  return json(entry, 200, port);
@@ -26459,7 +27524,7 @@ Dashboard not found at: ${dashboardDir}`);
26459
27524
  if (!entry)
26460
27525
  return json({ error: `Server '${id}' not found` }, 404, port);
26461
27526
  const tools = getCachedTools(id);
26462
- return json({ ...entry, toolCount: tools.length, tools }, 200, port);
27527
+ return json({ ...redactServer(entry), toolCount: tools.length, tools }, 200, port);
26463
27528
  }
26464
27529
  if (singleMatch && method === "DELETE") {
26465
27530
  const id = singleMatch[1];
@@ -26477,6 +27542,9 @@ Dashboard not found at: ${dashboardDir}`);
26477
27542
  if (!isValidId(id))
26478
27543
  return json({ error: "Invalid server ID" }, 400, port);
26479
27544
  try {
27545
+ const existing = getServer(id);
27546
+ if (!existing)
27547
+ return json({ error: `Server '${id}' not found` }, 404, port);
26480
27548
  enableServer(id);
26481
27549
  return json({ success: true }, 200, port);
26482
27550
  } catch (e) {
@@ -26489,29 +27557,277 @@ Dashboard not found at: ${dashboardDir}`);
26489
27557
  if (!isValidId(id))
26490
27558
  return json({ error: "Invalid server ID" }, 400, port);
26491
27559
  try {
27560
+ const existing = getServer(id);
27561
+ if (!existing)
27562
+ return json({ error: `Server '${id}' not found` }, 404, port);
26492
27563
  disableServer(id);
26493
27564
  return json({ success: true }, 200, port);
26494
27565
  } catch (e) {
26495
27566
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
26496
27567
  }
26497
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
+ }
27781
+ if (path === "/api/update" && method === "POST") {
27782
+ if (!isLoopbackHost(host)) {
27783
+ return json({ error: "Update only allowed on loopback host" }, 403, port);
27784
+ }
27785
+ try {
27786
+ const { execFileSync: execFileSync3 } = await import("child_process");
27787
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
27788
+ const currentVersion = pkg.version;
27789
+ const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
27790
+ encoding: "utf-8"
27791
+ }).trim();
27792
+ if (latest === currentVersion) {
27793
+ return json({ success: true, current: currentVersion, latest, upToDate: true }, 200, port);
27794
+ }
27795
+ execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "pipe" });
27796
+ return json({ success: true, current: currentVersion, latest, upToDate: false, updated: true }, 200, port);
27797
+ } catch (e) {
27798
+ return json({ error: e instanceof Error ? e.message : "Update failed" }, 500, port);
27799
+ }
27800
+ }
27801
+ if (path === "/api/version" && method === "GET") {
27802
+ try {
27803
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
27804
+ return json({ version: pkg.version }, 200, port);
27805
+ } catch {
27806
+ return json({ version: "unknown" }, 200, port);
27807
+ }
27808
+ }
26498
27809
  if (method === "OPTIONS") {
27810
+ if (!isAllowedOrigin(req, port, host)) {
27811
+ return json({ error: "Forbidden" }, 403, port);
27812
+ }
26499
27813
  return new Response(null, {
26500
27814
  headers: {
26501
27815
  "Access-Control-Allow-Origin": `http://localhost:${port}`,
26502
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
27816
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
26503
27817
  "Access-Control-Allow-Headers": "Content-Type"
26504
27818
  }
26505
27819
  });
26506
27820
  }
26507
27821
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
26508
27822
  if (path !== "/") {
26509
- const filePath = join2(dashboardDir, path);
26510
- const res2 = serveStaticFile(filePath);
26511
- if (res2)
26512
- return res2;
27823
+ const safePath = resolveStaticPath(dashboardDir, path);
27824
+ if (safePath) {
27825
+ const res2 = serveStaticFile(safePath);
27826
+ if (res2)
27827
+ return res2;
27828
+ }
26513
27829
  }
26514
- const indexPath = join2(dashboardDir, "index.html");
27830
+ const indexPath = join5(dashboardDir, "index.html");
26515
27831
  const res = serveStaticFile(indexPath);
26516
27832
  if (res)
26517
27833
  return res;
@@ -26526,19 +27842,24 @@ Dashboard not found at: ${dashboardDir}`);
26526
27842
  };
26527
27843
  process.on("SIGINT", shutdown);
26528
27844
  process.on("SIGTERM", shutdown);
26529
- const serverUrl = `http://localhost:${port}`;
27845
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
27846
+ const serverUrl = `http://${formatHostForUrl(displayHost)}:${port}`;
26530
27847
  console.log(`MCPs Dashboard running at ${serverUrl}`);
26531
27848
  if (shouldOpen) {
26532
27849
  try {
26533
- const { exec } = await import("child_process");
26534
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
26535
- exec(`${openCmd} ${serverUrl}`);
27850
+ const { execFile } = await import("child_process");
27851
+ if (process.platform === "win32") {
27852
+ execFile("cmd", ["/c", "start", "", serverUrl]);
27853
+ } else {
27854
+ const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
27855
+ execFile(openCmd, [serverUrl]);
27856
+ }
26536
27857
  } catch {}
26537
27858
  }
26538
27859
  }
26539
27860
 
26540
27861
  // src/cli/components/App.tsx
26541
- import { useState as useState7 } from "react";
27862
+ import { useEffect as useEffect5, useState as useState7 } from "react";
26542
27863
  import { Box as Box7, Text as Text9, useApp, useInput as useInput4 } from "ink";
26543
27864
 
26544
27865
  // src/cli/components/ServerList.tsx
@@ -26951,6 +28272,7 @@ var SelectInput_default = SelectInput;
26951
28272
  import { jsxDEV } from "react/jsx-dev-runtime";
26952
28273
  function ServerList({ onSelect, onSearch }) {
26953
28274
  const servers = listServers();
28275
+ const toolCounts = getToolCounts();
26954
28276
  useInput2((input) => {
26955
28277
  if (input === "s") {
26956
28278
  onSearch();
@@ -26972,10 +28294,9 @@ function ServerList({ onSelect, onSearch }) {
26972
28294
  }, undefined, true, undefined, this);
26973
28295
  }
26974
28296
  const items = servers.map((s) => {
26975
- const cached2 = getCachedTools(s.id);
28297
+ const cachedCount = toolCounts.get(s.id) ?? 0;
26976
28298
  const status = s.enabled ? "\u25CF" : "\u25CB";
26977
- const statusColor = s.enabled ? "green" : "red";
26978
- const toolInfo = cached2.length > 0 ? ` (${cached2.length} tools)` : "";
28299
+ const toolInfo = cachedCount > 0 ? ` (${cachedCount} tools)` : "";
26979
28300
  return {
26980
28301
  label: `${status} ${s.name} [${s.id}]${toolInfo}`,
26981
28302
  value: s.id,
@@ -27033,7 +28354,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
27033
28354
  const [loading, setLoading] = useState3(false);
27034
28355
  const [error2, setError] = useState3(null);
27035
28356
  const cachedTools = getCachedTools(server2.id);
28357
+ const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
27036
28358
  useEffect3(() => {
28359
+ setLoading(false);
28360
+ setError(null);
27037
28361
  if (cachedTools.length > 0) {
27038
28362
  setTools(cachedTools.map((t) => ({
27039
28363
  server_id: server2.id,
@@ -27041,8 +28365,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
27041
28365
  description: t.description,
27042
28366
  input_schema: t.input_schema
27043
28367
  })));
28368
+ } else {
28369
+ setTools([]);
27044
28370
  }
27045
- }, [server2.id]);
28371
+ }, [server2.id, cachedKey]);
27046
28372
  const handleConnect = async () => {
27047
28373
  setLoading(true);
27048
28374
  setError(null);
@@ -27366,6 +28692,7 @@ function SearchView({ onBack }) {
27366
28692
  // src/cli/components/ToolCall.tsx
27367
28693
  import { useState as useState6 } from "react";
27368
28694
  import { Box as Box6, Text as Text8 } from "ink";
28695
+ init_config();
27369
28696
  import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
27370
28697
  function ToolCall({ server: server2, tool, onBack }) {
27371
28698
  const [argsInput, setArgsInput] = useState6("{}");
@@ -27496,6 +28823,7 @@ function ToolCall({ server: server2, tool, onBack }) {
27496
28823
  }
27497
28824
 
27498
28825
  // src/cli/components/App.tsx
28826
+ init_db();
27499
28827
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
27500
28828
  function App() {
27501
28829
  const { exit } = useApp();
@@ -27503,7 +28831,7 @@ function App() {
27503
28831
  const [selectedServer, setSelectedServer] = useState7(null);
27504
28832
  const [selectedTool, setSelectedTool] = useState7(null);
27505
28833
  useInput4((input, key) => {
27506
- if (input === "q" && view === "servers") {
28834
+ if (input === "q" && view !== "search" && view !== "call") {
27507
28835
  exit();
27508
28836
  }
27509
28837
  if (key.escape) {
@@ -27520,6 +28848,13 @@ function App() {
27520
28848
  }
27521
28849
  }
27522
28850
  });
28851
+ useEffect5(() => {
28852
+ return () => {
28853
+ disconnectAll().catch(() => {
28854
+ return;
28855
+ }).finally(() => closeDb());
28856
+ };
28857
+ }, []);
27523
28858
  const handleSelectServer = (server2) => {
27524
28859
  setSelectedServer(server2);
27525
28860
  setView("detail");
@@ -27581,7 +28916,7 @@ function App() {
27581
28916
  marginTop: 1,
27582
28917
  children: /* @__PURE__ */ jsxDEV5(Text9, {
27583
28918
  dimColor: true,
27584
- children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : "esc back \xB7 q quit"
28919
+ children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : view === "detail" ? "esc back \xB7 q quit" : "esc back"
27585
28920
  }, undefined, false, undefined, this)
27586
28921
  }, undefined, false, undefined, this)
27587
28922
  ]
@@ -27589,23 +28924,62 @@ function App() {
27589
28924
  }
27590
28925
 
27591
28926
  // src/cli/index.tsx
28927
+ var VERSION2 = (() => {
28928
+ try {
28929
+ const pkgPath = join6(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
28930
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
28931
+ return pkg.version || "0.0.1";
28932
+ } catch {
28933
+ return "0.0.1";
28934
+ }
28935
+ })();
27592
28936
  var program2 = new Command;
27593
- program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version("0.0.1").enablePositionalOptions();
27594
- program2.command("list").description("List registered MCP servers").action(() => {
28937
+ program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
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) => {
27595
28939
  const servers = listServers();
28940
+ if (opts.json) {
28941
+ const toolCounts2 = getToolCounts();
28942
+ console.log(JSON.stringify(servers.map((s) => ({ ...s, toolCount: toolCounts2.get(s.id) ?? 0 })), null, 2));
28943
+ closeDb();
28944
+ return;
28945
+ }
27596
28946
  if (servers.length === 0) {
27597
28947
  console.log(chalk2.dim("No servers registered. Use `mcps add` or `mcps search` to get started."));
27598
28948
  closeDb();
27599
28949
  return;
27600
28950
  }
28951
+ const toolCounts = getToolCounts();
27601
28952
  for (const s of servers) {
27602
28953
  const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
27603
- const cached2 = getCachedTools(s.id);
27604
- const toolCount = cached2.length > 0 ? chalk2.dim(` (${cached2.length} tools)`) : "";
27605
- console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}`);
28954
+ const cachedCount = toolCounts.get(s.id) ?? 0;
28955
+ const toolCount = cachedCount > 0 ? chalk2.dim(` (${cachedCount} tools)`) : "";
28956
+ const errorWarning = s.last_error ? chalk2.red(" \u26A0") : "";
28957
+ console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
27606
28958
  if (s.description)
27607
28959
  console.log(` ${chalk2.dim(s.description)}`);
27608
- 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
+ }
27609
28983
  }
27610
28984
  closeDb();
27611
28985
  });
@@ -27630,10 +29004,25 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
27630
29004
  } catch (err) {
27631
29005
  console.error(chalk2.red(`Search failed: ${err.message}`));
27632
29006
  process.exit(1);
29007
+ } finally {
29008
+ closeDb();
27633
29009
  }
27634
- closeDb();
27635
29010
  });
27636
- 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) => {
27637
29026
  try {
27638
29027
  if (opts.fromRegistry) {
27639
29028
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
@@ -27643,14 +29032,84 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
27643
29032
  closeDb();
27644
29033
  return;
27645
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
+ }
27646
29092
  if (!command) {
27647
- 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)"));
29094
+ closeDb();
27648
29095
  process.exit(1);
27649
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
+ }
27650
29107
  const envMap = {};
27651
29108
  if (opts.env) {
27652
29109
  for (const pair of opts.env) {
27653
29110
  const [key, ...rest] = pair.split("=");
29111
+ if (!key)
29112
+ continue;
27654
29113
  envMap[key] = rest.join("=");
27655
29114
  }
27656
29115
  }
@@ -27671,6 +29130,43 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
27671
29130
  } else {
27672
29131
  console.error(chalk2.red(`Failed to add server: ${err.message}`));
27673
29132
  }
29133
+ closeDb();
29134
+ process.exit(1);
29135
+ }
29136
+ closeDb();
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();
27674
29170
  process.exit(1);
27675
29171
  }
27676
29172
  closeDb();
@@ -27679,6 +29175,7 @@ program2.command("remove").argument("<id>", "Server ID to remove").description("
27679
29175
  const server2 = getServer(id);
27680
29176
  if (!server2) {
27681
29177
  console.error(chalk2.red(`Server "${id}" not found.`));
29178
+ closeDb();
27682
29179
  process.exit(1);
27683
29180
  }
27684
29181
  removeServer(id);
@@ -27689,6 +29186,7 @@ program2.command("enable").argument("<id>", "Server ID to enable").description("
27689
29186
  const server2 = getServer(id);
27690
29187
  if (!server2) {
27691
29188
  console.error(chalk2.red(`Server "${id}" not found.`));
29189
+ closeDb();
27692
29190
  process.exit(1);
27693
29191
  }
27694
29192
  enableServer(id);
@@ -27699,6 +29197,7 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
27699
29197
  const server2 = getServer(id);
27700
29198
  if (!server2) {
27701
29199
  console.error(chalk2.red(`Server "${id}" not found.`));
29200
+ closeDb();
27702
29201
  process.exit(1);
27703
29202
  }
27704
29203
  disableServer(id);
@@ -27778,6 +29277,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
27778
29277
  }
27779
29278
  }
27780
29279
  }
29280
+ let exitCode = 0;
27781
29281
  try {
27782
29282
  console.log(chalk2.dim(`Connecting to servers...`));
27783
29283
  await connectAllEnabled();
@@ -27786,17 +29286,22 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
27786
29286
  for (const c of result.content) {
27787
29287
  console.log(c.text);
27788
29288
  }
27789
- await disconnectAll();
27790
29289
  } catch (err) {
27791
29290
  console.error(chalk2.red(`Call failed: ${err.message}`));
27792
- process.exit(1);
29291
+ exitCode = 1;
27793
29292
  }
29293
+ await disconnectAll().catch(() => {
29294
+ return;
29295
+ });
27794
29296
  closeDb();
29297
+ if (exitCode !== 0)
29298
+ process.exit(exitCode);
27795
29299
  });
27796
29300
  program2.command("info").argument("<id>", "Server ID").description("Show server details & tools").action((id) => {
27797
29301
  const server2 = getServer(id);
27798
29302
  if (!server2) {
27799
29303
  console.error(chalk2.red(`Server "${id}" not found.`));
29304
+ closeDb();
27800
29305
  process.exit(1);
27801
29306
  }
27802
29307
  console.log(chalk2.bold(server2.name) + " " + chalk2.dim(`[${server2.id}]`));
@@ -27825,21 +29330,553 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
27825
29330
  }
27826
29331
  closeDb();
27827
29332
  });
27828
- program2.command("status").description("Show registry stats").action(() => {
29333
+ program2.command("status").description("Show registry stats").option("--json", "Output as JSON").action((opts) => {
27829
29334
  const servers = listServers();
27830
29335
  const enabled = servers.filter((s) => s.enabled).length;
27831
29336
  const disabled = servers.length - enabled;
29337
+ const toolCounts = getToolCounts();
27832
29338
  let totalTools = 0;
27833
- for (const s of servers) {
27834
- totalTools += getCachedTools(s.id).length;
29339
+ for (const s of servers)
29340
+ totalTools += toolCounts.get(s.id) ?? 0;
29341
+ if (opts.json) {
29342
+ console.log(JSON.stringify({ total: servers.length, enabled, disabled, totalTools }, null, 2));
29343
+ closeDb();
29344
+ return;
27835
29345
  }
27836
29346
  console.log(chalk2.bold("Registry Status"));
27837
29347
  console.log(` Servers: ${servers.length} (${chalk2.green(`${enabled} enabled`)}, ${chalk2.red(`${disabled} disabled`)})`);
27838
29348
  console.log(` Tools: ${totalTools} (cached)`);
27839
29349
  closeDb();
27840
29350
  });
27841
- program2.command("serve").description("Start the web dashboard").option("--port <port>", "Port to listen on", "19427").option("--no-open", "Don't open browser automatically").action(async (opts) => {
27842
- await startServer(parseInt(opts.port, 10), { open: opts.open });
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");
29353
+ const servers = id ? [getServer(id)].filter(Boolean) : listServers();
29354
+ if (servers.length === 0) {
29355
+ console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
29356
+ closeDb();
29357
+ return;
29358
+ }
29359
+ let allHealthy = true;
29360
+ for (const server2 of servers) {
29361
+ console.log(chalk2.bold(`
29362
+ ${server2.name} [${server2.id}]`));
29363
+ const report = await diagnoseServer(server2);
29364
+ for (const check2 of report.checks) {
29365
+ const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
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
+ }
29376
+ }
29377
+ if (!report.healthy)
29378
+ allHealthy = false;
29379
+ }
29380
+ console.log("");
29381
+ if (allHealthy) {
29382
+ console.log(chalk2.green("All checks passed."));
29383
+ } else {
29384
+ console.log(chalk2.red("Some checks failed. Fix issues above."));
29385
+ }
29386
+ closeDb();
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
+ });
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) => {
29423
+ await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
29424
+ });
29425
+ program2.command("update").description("Update mcps to the latest version").action(async () => {
29426
+ const { execFileSync: execFileSync3 } = await import("child_process");
29427
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
29428
+ const currentVersion = pkg.version;
29429
+ console.log(chalk2.dim(`Current version: ${currentVersion}`));
29430
+ console.log(chalk2.dim("Checking for updates..."));
29431
+ try {
29432
+ const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
29433
+ encoding: "utf-8"
29434
+ }).trim();
29435
+ if (latest === currentVersion) {
29436
+ console.log(chalk2.green(`Already on the latest version (${currentVersion}).`));
29437
+ return;
29438
+ }
29439
+ console.log(chalk2.dim(`New version available: ${latest}`));
29440
+ console.log(chalk2.dim("Updating..."));
29441
+ execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "inherit" });
29442
+ console.log(chalk2.green(`Updated to ${latest}`));
29443
+ } catch (err) {
29444
+ console.error(chalk2.red(`Update failed: ${err.message}`));
29445
+ closeDb();
29446
+ process.exit(1);
29447
+ }
29448
+ });
29449
+ program2.command("find").argument("[query]", "Search query (omit to list all from awesome list)").description("Find MCP servers across npm, GitHub, official registry, and awesome lists").option("--source <sources...>", "Source IDs to search (see `mcps sources list`)").option("--limit <n>", "Max results per source", "20").option("--awesome", "List curated servers from punkpeye/awesome-mcp-servers").option("--json", "Output as JSON").option("--install", "After showing results, prompt to select one and install it").option("--yes", "Auto-install without prompting (only when there is exactly 1 result)").option("--no-cache", "Bypass source cache and fetch fresh results").action(async (query, opts) => {
29450
+ try {
29451
+ if (opts.awesome) {
29452
+ console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
29453
+ const results2 = await listAwesomeServers();
29454
+ if (opts.json) {
29455
+ console.log(JSON.stringify(results2, null, 2));
29456
+ closeDb();
29457
+ return;
29458
+ }
29459
+ const allSources = listSources();
29460
+ const sourceNameMap2 = new Map(allSources.map((s) => [s.id, s.name]));
29461
+ for (const r of results2) {
29462
+ const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
29463
+ console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
29464
+ if (r.description)
29465
+ console.log(` ${chalk2.dim(r.description)}`);
29466
+ if (r.url)
29467
+ console.log(` ${chalk2.cyan(r.url)}`);
29468
+ }
29469
+ console.log(chalk2.dim(`
29470
+ ${results2.length} servers in awesome list.`));
29471
+ closeDb();
29472
+ return;
29473
+ }
29474
+ const q = query || "";
29475
+ const sources = opts.source;
29476
+ const limit = parseInt(opts.limit, 10) || 20;
29477
+ const noCache = opts.cache === false;
29478
+ if (!q) {
29479
+ console.log(chalk2.dim("Tip: provide a query to search, or use --awesome to browse the curated list."));
29480
+ } else {
29481
+ console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
29482
+ }
29483
+ const t0 = Date.now();
29484
+ const results = await findServers(q, { sources, limit, noCache });
29485
+ const elapsed = Date.now() - t0;
29486
+ if (opts.json) {
29487
+ console.log(JSON.stringify(results, null, 2));
29488
+ closeDb();
29489
+ return;
29490
+ }
29491
+ if (results.length === 0) {
29492
+ console.log(chalk2.dim("No servers found."));
29493
+ closeDb();
29494
+ return;
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(", ");
29505
+ const sourceColors = {
29506
+ registry: chalk2.blue,
29507
+ npm: chalk2.red,
29508
+ awesome: chalk2.yellow,
29509
+ github: chalk2.magenta
29510
+ };
29511
+ for (let i = 0;i < results.length; i++) {
29512
+ const r = results[i];
29513
+ const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
29514
+ const sourceLabel = (sourceColors[r.source] ?? chalk2.dim)(`[${sourceName}]`);
29515
+ const stars = r.stars ? chalk2.dim(` \u2605${r.stars}`) : "";
29516
+ const idx = opts.install ? chalk2.dim(`${i + 1}. `) : " ";
29517
+ console.log(`${idx}${chalk2.bold(r.name)} ${sourceLabel}${stars}`);
29518
+ if (r.description)
29519
+ console.log(` ${chalk2.dim(r.description)}`);
29520
+ if (r.installCmd)
29521
+ console.log(` ${chalk2.green(`Install: ${r.installCmd}`)}`);
29522
+ else if (r.url)
29523
+ console.log(` ${chalk2.cyan(r.url)}`);
29524
+ }
29525
+ console.log(chalk2.dim(`
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.`));
29530
+ if (opts.install) {
29531
+ let chosen = results[0];
29532
+ if (results.length === 1 && opts.yes) {} else {
29533
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
29534
+ const answer = await new Promise((resolve2) => {
29535
+ rl.question(chalk2.cyan(`
29536
+ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
29537
+ });
29538
+ rl.close();
29539
+ const num = parseInt(answer, 10);
29540
+ if (!num || num < 1 || num > results.length) {
29541
+ console.log(chalk2.dim("Installation cancelled."));
29542
+ closeDb();
29543
+ return;
29544
+ }
29545
+ chosen = results[num - 1];
29546
+ }
29547
+ if (!chosen.npmPackage && !chosen.installCmd) {
29548
+ console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
29549
+ closeDb();
29550
+ return;
29551
+ }
29552
+ const pkg = chosen.npmPackage ?? chosen.installCmd?.replace(/^npx -y /, "");
29553
+ if (!pkg) {
29554
+ console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
29555
+ closeDb();
29556
+ return;
29557
+ }
29558
+ console.log(chalk2.dim(`Installing ${chosen.name}...`));
29559
+ const server2 = addServer({
29560
+ command: "npx",
29561
+ args: ["-y", pkg],
29562
+ name: chosen.name,
29563
+ description: chosen.description,
29564
+ transport: "stdio"
29565
+ });
29566
+ const results2 = installToAgents(server2, ["claude", "codex", "gemini"]);
29567
+ for (const r of results2) {
29568
+ if (r.success) {
29569
+ console.log(chalk2.green(` \u2713 ${r.agent}`));
29570
+ } else {
29571
+ console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
29572
+ }
29573
+ }
29574
+ console.log(chalk2.green(`
29575
+ Installed ${server2.name} [${server2.id}]`));
29576
+ }
29577
+ } catch (err) {
29578
+ console.error(chalk2.red(`Find failed: ${err.message}`));
29579
+ process.exit(1);
29580
+ } finally {
29581
+ closeDb();
29582
+ }
29583
+ });
29584
+ var sourcesCmd = program2.command("sources").description("Manage search sources");
29585
+ sourcesCmd.command("list").description("List all search sources").action(() => {
29586
+ const sources = listSources();
29587
+ if (sources.length === 0) {
29588
+ console.log(chalk2.dim("No sources configured."));
29589
+ closeDb();
29590
+ return;
29591
+ }
29592
+ for (const s of sources) {
29593
+ const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
29594
+ console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${chalk2.dim(s.type)} \u2014 ${status}`);
29595
+ if (s.description)
29596
+ console.log(` ${chalk2.dim(s.description)}`);
29597
+ console.log(` ${chalk2.cyan(s.url)}`);
29598
+ }
29599
+ closeDb();
29600
+ });
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"));
29604
+ closeDb();
29605
+ process.exit(1);
29606
+ }
29607
+ const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
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)) {
29621
+ console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
29622
+ closeDb();
29623
+ process.exit(1);
29624
+ }
29625
+ try {
29626
+ const source = addSource({
29627
+ name: opts.name,
29628
+ type: sourceType,
29629
+ url: opts.url,
29630
+ description: opts.description
29631
+ });
29632
+ console.log(chalk2.green(`Added source: ${source.name} [${source.id}]`));
29633
+ if (opts.test) {
29634
+ console.log(chalk2.dim("Testing source..."));
29635
+ const testResults = await searchSource(source, "");
29636
+ console.log(chalk2.dim(` Found ${testResults.length} results`));
29637
+ if (testResults.length === 0)
29638
+ console.log(chalk2.yellow(" Warning: source returned no results"));
29639
+ }
29640
+ } catch (err) {
29641
+ console.error(chalk2.red(`Failed to add source: ${err.message}`));
29642
+ closeDb();
29643
+ process.exit(1);
29644
+ }
29645
+ closeDb();
29646
+ });
29647
+ sourcesCmd.command("remove").argument("<id>", "Source ID to remove").description("Remove a search source").action((id) => {
29648
+ const source = getSource(id);
29649
+ if (!source) {
29650
+ console.error(chalk2.red(`Source "${id}" not found.`));
29651
+ closeDb();
29652
+ process.exit(1);
29653
+ }
29654
+ removeSource(id);
29655
+ console.log(chalk2.green(`Removed source: ${source.name} [${id}]`));
29656
+ closeDb();
29657
+ });
29658
+ sourcesCmd.command("enable").argument("<id>", "Source ID to enable").description("Enable a search source").action((id) => {
29659
+ const source = getSource(id);
29660
+ if (!source) {
29661
+ console.error(chalk2.red(`Source "${id}" not found.`));
29662
+ closeDb();
29663
+ process.exit(1);
29664
+ }
29665
+ enableSource(id);
29666
+ console.log(chalk2.green(`Enabled source: ${source.name}`));
29667
+ closeDb();
29668
+ });
29669
+ sourcesCmd.command("disable").argument("<id>", "Source ID to disable").description("Disable a search source").action((id) => {
29670
+ const source = getSource(id);
29671
+ if (!source) {
29672
+ console.error(chalk2.red(`Source "${id}" not found.`));
29673
+ closeDb();
29674
+ process.exit(1);
29675
+ }
29676
+ disableSource(id);
29677
+ console.log(chalk2.yellow(`Disabled source: ${source.name}`));
29678
+ closeDb();
29679
+ });
29680
+ sourcesCmd.command("refresh").argument("[id]", "Source ID to refresh (omit to refresh all)").description("Clear cached results for a source").action((id) => {
29681
+ clearCache(id);
29682
+ if (id) {
29683
+ console.log(chalk2.green(`Cleared cache for source: ${id}`));
29684
+ } else {
29685
+ console.log(chalk2.green("Cleared all source caches."));
29686
+ }
29687
+ closeDb();
29688
+ });
29689
+ sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Test a source by running a sample search").action(async (id) => {
29690
+ const source = getSource(id);
29691
+ if (!source) {
29692
+ console.error(chalk2.red(`Source "${id}" not found.`));
29693
+ closeDb();
29694
+ process.exit(1);
29695
+ }
29696
+ console.log(chalk2.dim(`Testing source "${source.name}"...`));
29697
+ try {
29698
+ const results = await searchSource(source, "", true);
29699
+ console.log(chalk2.green(`\u2713 Source returned ${results.length} results`));
29700
+ if (results.length > 0) {
29701
+ console.log(chalk2.dim(" Sample results:"));
29702
+ for (const r of results.slice(0, 3)) {
29703
+ console.log(` ${chalk2.bold(r.name)}: ${chalk2.dim(r.description?.slice(0, 60) || "no description")}`);
29704
+ }
29705
+ }
29706
+ } catch (err) {
29707
+ console.error(chalk2.red(`\u2717 Source test failed: ${err.message}`));
29708
+ }
29709
+ closeDb();
29710
+ });
29711
+ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to install into AI agents").description("Install a registered MCP server into Claude Code, Codex, and/or Gemini").option("--claude", "Install to Claude Code").option("--codex", "Install to Codex").option("--gemini", "Install to Gemini").option("--all", "Install to all agents (default if none specified)").option("--to <agents...>", "Agents to install to: claude, codex, gemini").option("--from-registry <id>", "Add from official registry and install in one step").option("--npm <package>", "Add an npm package as a server and install in one step").action(async (id, opts) => {
29712
+ const targets = [];
29713
+ if (opts.to) {
29714
+ for (const t of opts.to) {
29715
+ if (t === "claude" || t === "codex" || t === "gemini")
29716
+ targets.push(t);
29717
+ }
29718
+ }
29719
+ if (opts.claude && !targets.includes("claude"))
29720
+ targets.push("claude");
29721
+ if (opts.codex && !targets.includes("codex"))
29722
+ targets.push("codex");
29723
+ if (opts.gemini && !targets.includes("gemini"))
29724
+ targets.push("gemini");
29725
+ if (opts.all || targets.length === 0) {
29726
+ if (!targets.includes("claude"))
29727
+ targets.push("claude");
29728
+ if (!targets.includes("codex"))
29729
+ targets.push("codex");
29730
+ if (!targets.includes("gemini"))
29731
+ targets.push("gemini");
29732
+ }
29733
+ let server2;
29734
+ if (opts.fromRegistry) {
29735
+ console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
29736
+ try {
29737
+ server2 = await installFromRegistry(opts.fromRegistry);
29738
+ console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
29739
+ } catch (err) {
29740
+ console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
29741
+ closeDb();
29742
+ process.exit(1);
29743
+ }
29744
+ } else if (opts.npm) {
29745
+ const pkg = opts.npm;
29746
+ server2 = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
29747
+ console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
29748
+ } else {
29749
+ if (!id) {
29750
+ console.error(chalk2.red("Error: server ID is required (or use --from-registry or --npm)"));
29751
+ closeDb();
29752
+ process.exit(1);
29753
+ }
29754
+ server2 = getServer(id);
29755
+ if (!server2) {
29756
+ console.error(chalk2.red(`Server "${id}" not found. Use \`mcps add\` first.`));
29757
+ closeDb();
29758
+ process.exit(1);
29759
+ }
29760
+ }
29761
+ console.log(chalk2.dim(`Installing "${server2.name}" to: ${targets.join(", ")}...`));
29762
+ const results = installToAgents(server2, targets);
29763
+ for (const r of results) {
29764
+ if (r.success) {
29765
+ console.log(chalk2.green(` \u2713 ${r.agent}`));
29766
+ } else {
29767
+ console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
29768
+ }
29769
+ }
29770
+ closeDb();
29771
+ });
29772
+ program2.command("export").description("Export all servers and sources to a JSON file").option("--file <path>", "Output file path", `${process.env.HOME ?? "~"}/.mcps/export.json`).option("--stdout", "Write to stdout instead of a file").action((opts) => {
29773
+ const servers = listServers();
29774
+ const sources = listSources();
29775
+ const payload = {
29776
+ version: 1,
29777
+ exported_at: new Date().toISOString(),
29778
+ servers,
29779
+ sources
29780
+ };
29781
+ const json2 = JSON.stringify(payload, null, 2);
29782
+ if (opts.stdout) {
29783
+ console.log(json2);
29784
+ } else {
29785
+ writeFileSync3(opts.file, json2, "utf-8");
29786
+ console.log(chalk2.green(`Exported ${servers.length} server(s) and ${sources.length} source(s) to ${opts.file}`));
29787
+ }
29788
+ closeDb();
29789
+ });
29790
+ program2.command("import").argument("<file>", "Path to the export JSON file").description("Import servers and sources from a JSON export file").option("--overwrite", "Overwrite existing entries with matching IDs").action((file, opts) => {
29791
+ let payload;
29792
+ try {
29793
+ payload = JSON.parse(readFileSync4(file, "utf-8"));
29794
+ } catch (err) {
29795
+ console.error(chalk2.red(`Failed to read file: ${err.message}`));
29796
+ closeDb();
29797
+ process.exit(1);
29798
+ }
29799
+ if (!Array.isArray(payload.servers) || !Array.isArray(payload.sources)) {
29800
+ console.error(chalk2.red("Invalid export file format."));
29801
+ closeDb();
29802
+ process.exit(1);
29803
+ }
29804
+ const db2 = getDb();
29805
+ const overwrite = opts.overwrite;
29806
+ const orReplace = overwrite ? "OR REPLACE" : "OR IGNORE";
29807
+ let serversImported = 0;
29808
+ let serversSkipped = 0;
29809
+ for (const s of payload.servers) {
29810
+ const existing = getServer(s.id);
29811
+ if (existing && !overwrite) {
29812
+ serversSkipped++;
29813
+ continue;
29814
+ }
29815
+ db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, [s.id, s.name, s.description, s.command, JSON.stringify(s.args ?? []), JSON.stringify(s.env ?? {}), s.transport, s.url, s.source, s.enabled ? 1 : 0, s.created_at, s.updated_at]);
29816
+ serversImported++;
29817
+ }
29818
+ let sourcesImported = 0;
29819
+ let sourcesSkipped = 0;
29820
+ for (const s of payload.sources) {
29821
+ const existing = getSource(s.id);
29822
+ if (existing && !overwrite) {
29823
+ sourcesSkipped++;
29824
+ continue;
29825
+ }
29826
+ db2.run(`INSERT ${orReplace} INTO sources (id, name, type, url, description, enabled, created_at) VALUES (?,?,?,?,?,?,?)`, [s.id, s.name, s.type, s.url, s.description, s.enabled ? 1 : 0, s.created_at]);
29827
+ sourcesImported++;
29828
+ }
29829
+ console.log(chalk2.green(`Servers: ${serversImported} imported, ${serversSkipped} skipped.`));
29830
+ console.log(chalk2.green(`Sources: ${sourcesImported} imported, ${sourcesSkipped} skipped.`));
29831
+ closeDb();
29832
+ });
29833
+ var envCmd = program2.command("env").description("Manage server environment variables");
29834
+ envCmd.command("list").argument("<id>").description("List env vars for a server").action((id) => {
29835
+ const server2 = getServer(id);
29836
+ if (!server2) {
29837
+ console.error(chalk2.red(`Server "${id}" not found.`));
29838
+ closeDb();
29839
+ process.exit(1);
29840
+ }
29841
+ const entries = Object.entries(server2.env);
29842
+ if (entries.length === 0) {
29843
+ console.log(chalk2.dim("No env vars set."));
29844
+ closeDb();
29845
+ return;
29846
+ }
29847
+ for (const [k, v] of entries)
29848
+ console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
29849
+ closeDb();
29850
+ });
29851
+ envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
29852
+ const eqIdx = pair.indexOf("=");
29853
+ if (eqIdx === -1) {
29854
+ console.error(chalk2.red("Format: KEY=VALUE"));
29855
+ closeDb();
29856
+ process.exit(1);
29857
+ }
29858
+ const key = pair.slice(0, eqIdx);
29859
+ const value = pair.slice(eqIdx + 1);
29860
+ try {
29861
+ setServerEnv(id, key, value);
29862
+ console.log(chalk2.green(`Set ${key} on ${id}`));
29863
+ } catch (err) {
29864
+ console.error(chalk2.red(err.message));
29865
+ closeDb();
29866
+ process.exit(1);
29867
+ }
29868
+ closeDb();
29869
+ });
29870
+ envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
29871
+ try {
29872
+ unsetServerEnv(id, key);
29873
+ console.log(chalk2.green(`Unset ${key} on ${id}`));
29874
+ } catch (err) {
29875
+ console.error(chalk2.red(err.message));
29876
+ closeDb();
29877
+ process.exit(1);
29878
+ }
29879
+ closeDb();
27843
29880
  });
27844
29881
  program2.command("mcp").description("Start meta-MCP server (stdio)").action(async () => {
27845
29882
  await startMcpServer();