@hasna/mcps 0.0.1 → 0.0.3

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.3",
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,64 @@ 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)]));
10615
11097
  }
10616
11098
  function getCachedTools(serverId) {
10617
11099
  const db2 = getDb();
10618
- const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ?").all(serverId);
11100
+ const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
10619
11101
  return rows.map((r) => ({
10620
11102
  name: r.name,
10621
11103
  description: r.description,
10622
- input_schema: JSON.parse(r.input_schema)
11104
+ input_schema: safeJsonParse(r.input_schema, {})
10623
11105
  }));
10624
11106
  }
10625
11107
 
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
- }
11108
+ // src/lib/doctor.ts
11109
+ import { execFileSync } from "child_process";
10686
11110
 
10687
11111
  // node_modules/zod/v3/external.js
10688
11112
  var exports_external = {};
@@ -24909,54 +25333,127 @@ class StreamableHTTPClientTransport {
24909
25333
  }
24910
25334
 
24911
25335
  // src/lib/proxy.ts
25336
+ init_config();
25337
+ init_db();
24912
25338
  var connections = new Map;
25339
+ var inflightConnections = new Map;
25340
+ var CONNECT_CONCURRENCY = 4;
25341
+ function buildEnv(extra) {
25342
+ const merged = {};
25343
+ for (const [key, value] of Object.entries(process.env)) {
25344
+ if (typeof value === "string")
25345
+ merged[key] = value;
25346
+ }
25347
+ for (const [key, value] of Object.entries(extra || {})) {
25348
+ if (value === undefined || value === null)
25349
+ continue;
25350
+ merged[key] = String(value);
25351
+ }
25352
+ return merged;
25353
+ }
25354
+ function requireUrl(entry) {
25355
+ if (!entry.url) {
25356
+ throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
25357
+ }
25358
+ try {
25359
+ return new URL(entry.url);
25360
+ } catch {
25361
+ throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
25362
+ }
25363
+ }
24913
25364
  async function connectToServer(entry) {
24914
25365
  if (connections.has(entry.id)) {
24915
25366
  return connections.get(entry.id);
24916
25367
  }
25368
+ const inflight = inflightConnections.get(entry.id);
25369
+ if (inflight) {
25370
+ return inflight;
25371
+ }
24917
25372
  const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
24918
25373
  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);
25374
+ const connectPromise = (async () => {
25375
+ try {
25376
+ if (entry.transport === "stdio") {
25377
+ if (!entry.command?.trim()) {
25378
+ throw new Error(`Server "${entry.id}" is missing a command`);
25379
+ }
25380
+ transport = new StdioClientTransport({
25381
+ command: entry.command,
25382
+ args: entry.args,
25383
+ env: buildEnv(entry.env)
25384
+ });
25385
+ } else if (entry.transport === "sse") {
25386
+ transport = new SSEClientTransport(requireUrl(entry));
25387
+ } else {
25388
+ transport = new StreamableHTTPClientTransport(requireUrl(entry));
25389
+ }
25390
+ await client.connect(transport);
25391
+ const result = await client.listTools();
25392
+ const tools = (result.tools || []).map((t) => ({
25393
+ server_id: entry.id,
25394
+ name: t.name,
25395
+ description: t.description || "",
25396
+ input_schema: t.inputSchema || {}
25397
+ }));
25398
+ cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
25399
+ const connected = {
25400
+ entry,
25401
+ tools,
25402
+ disconnect: async () => {
25403
+ try {
25404
+ await client.close();
25405
+ } finally {
25406
+ connections.delete(entry.id);
25407
+ }
25408
+ }
25409
+ };
25410
+ connected._client = client;
25411
+ connections.set(entry.id, connected);
25412
+ try {
25413
+ getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
25414
+ } catch {}
25415
+ return connected;
25416
+ } catch (err) {
25417
+ try {
25418
+ getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
25419
+ } catch {}
25420
+ try {
25421
+ await client.close();
25422
+ } catch {}
25423
+ throw err;
24945
25424
  }
24946
- };
24947
- connected._client = client;
24948
- connections.set(entry.id, connected);
24949
- return connected;
25425
+ })();
25426
+ inflightConnections.set(entry.id, connectPromise);
25427
+ try {
25428
+ return await connectPromise;
25429
+ } finally {
25430
+ inflightConnections.delete(entry.id);
25431
+ }
24950
25432
  }
24951
25433
  async function disconnectServer(id) {
24952
25434
  const conn = connections.get(id);
24953
25435
  if (conn) {
24954
25436
  await conn.disconnect();
25437
+ return;
25438
+ }
25439
+ const inflight = inflightConnections.get(id);
25440
+ if (inflight) {
25441
+ try {
25442
+ const pending = await inflight;
25443
+ await pending.disconnect();
25444
+ } catch {}
24955
25445
  }
24956
25446
  }
24957
25447
  async function disconnectAll() {
24958
25448
  const ids = Array.from(connections.keys());
24959
- await Promise.all(ids.map((id) => disconnectServer(id)));
25449
+ await Promise.allSettled(ids.map((id) => disconnectServer(id)));
25450
+ const inflight = Array.from(inflightConnections.values());
25451
+ await Promise.allSettled(inflight.map(async (promise2) => {
25452
+ try {
25453
+ const conn = await promise2;
25454
+ await conn.disconnect();
25455
+ } catch {}
25456
+ }));
24960
25457
  }
24961
25458
  function listAllTools() {
24962
25459
  const tools = [];
@@ -24971,12 +25468,19 @@ function listAllTools() {
24971
25468
  return tools;
24972
25469
  }
24973
25470
  async function callTool(prefixedName, args) {
24974
- const sepIdx = prefixedName.indexOf(TOOL_PREFIX_SEPARATOR);
25471
+ const normalized = prefixedName.trim();
25472
+ if (!normalized) {
25473
+ throw new Error("Tool name is required");
25474
+ }
25475
+ const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
24975
25476
  if (sepIdx === -1) {
24976
25477
  throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
24977
25478
  }
24978
- const serverId = prefixedName.slice(0, sepIdx);
24979
- const toolName = prefixedName.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length);
25479
+ const serverId = normalized.slice(0, sepIdx).trim();
25480
+ const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
25481
+ if (!serverId || !toolName) {
25482
+ throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
25483
+ }
24980
25484
  const conn = connections.get(serverId);
24981
25485
  if (!conn) {
24982
25486
  throw new Error(`Server "${serverId}" is not connected`);
@@ -24992,16 +25496,235 @@ async function callTool(prefixedName, args) {
24992
25496
  async function connectAllEnabled() {
24993
25497
  const servers = listServers().filter((s) => s.enabled);
24994
25498
  const results = [];
24995
- for (const server of servers) {
25499
+ let index = 0;
25500
+ const workerCount = Math.min(CONNECT_CONCURRENCY, servers.length);
25501
+ const workers = Array.from({ length: workerCount }, async () => {
25502
+ while (true) {
25503
+ const current = index++;
25504
+ if (current >= servers.length)
25505
+ return;
25506
+ const server = servers[current];
25507
+ try {
25508
+ const conn = await connectToServer(server);
25509
+ results.push(conn);
25510
+ } catch (err) {
25511
+ console.error(`Failed to connect to ${server.name}: ${err.message}`);
25512
+ }
25513
+ }
25514
+ });
25515
+ await Promise.all(workers);
25516
+ return results;
25517
+ }
25518
+
25519
+ // src/lib/doctor.ts
25520
+ async function diagnoseServer(server) {
25521
+ const checks4 = [];
25522
+ if (server.transport === "stdio") {
25523
+ try {
25524
+ execFileSync("which", [server.command], { stdio: "pipe" });
25525
+ checks4.push({ name: "command on PATH", pass: true, message: `${server.command} found` });
25526
+ } catch {
25527
+ checks4.push({ name: "command on PATH", pass: false, message: `${server.command} not found on PATH` });
25528
+ }
25529
+ }
25530
+ const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
25531
+ if (Object.keys(server.env).length === 0) {
25532
+ checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
25533
+ } else if (missingEnv.length > 0) {
25534
+ checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
25535
+ } else {
25536
+ checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
25537
+ }
25538
+ if (server.transport !== "stdio" && server.url) {
24996
25539
  try {
24997
- const conn = await connectToServer(server);
24998
- results.push(conn);
25540
+ const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
25541
+ checks4.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
24999
25542
  } catch (err) {
25000
- console.error(`Failed to connect to ${server.name}: ${err.message}`);
25543
+ checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
25001
25544
  }
25002
25545
  }
25003
- return results;
25546
+ if (server.enabled) {
25547
+ try {
25548
+ await Promise.race([
25549
+ connectToServer(server),
25550
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
25551
+ ]);
25552
+ await disconnectServer(server.id);
25553
+ checks4.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
25554
+ } catch (err) {
25555
+ checks4.push({ name: "connect & list tools", pass: false, message: err.message });
25556
+ }
25557
+ } else {
25558
+ checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
25559
+ }
25560
+ return {
25561
+ server,
25562
+ checks: checks4,
25563
+ healthy: checks4.every((c) => c.pass)
25564
+ };
25565
+ }
25566
+
25567
+ // src/lib/remote.ts
25568
+ init_config();
25569
+ function parseRegistryEntry(entry) {
25570
+ const s = entry.server;
25571
+ return {
25572
+ id: s.name,
25573
+ name: s.name,
25574
+ description: s.description || "",
25575
+ repository: s.repository,
25576
+ packages: s.packages || []
25577
+ };
25578
+ }
25579
+ async function searchRegistry(query) {
25580
+ const res = await fetch(REGISTRY_API_URL);
25581
+ if (!res.ok) {
25582
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
25583
+ }
25584
+ const data = await res.json();
25585
+ const entries = data.servers || [];
25586
+ const q = query.toLowerCase();
25587
+ return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
25588
+ }
25589
+ async function getRegistryServer(id) {
25590
+ const res = await fetch(REGISTRY_API_URL);
25591
+ if (!res.ok) {
25592
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
25593
+ }
25594
+ const data = await res.json();
25595
+ const entries = data.servers || [];
25596
+ const all = entries.map(parseRegistryEntry);
25597
+ return all.find((s) => s.id === id) || null;
25598
+ }
25599
+ async function installFromRegistry(id) {
25600
+ const server = await getRegistryServer(id);
25601
+ if (!server) {
25602
+ throw new Error(`Server "${id}" not found in registry`);
25603
+ }
25604
+ const pkg = server.packages?.[0];
25605
+ let command = "npx";
25606
+ let args = [];
25607
+ let transport = "stdio";
25608
+ if (pkg) {
25609
+ if (pkg.registryType === "npm") {
25610
+ command = "npx";
25611
+ args = ["-y", pkg.identifier];
25612
+ } else {
25613
+ command = pkg.identifier;
25614
+ }
25615
+ if (pkg.transport?.type) {
25616
+ transport = pkg.transport.type;
25617
+ }
25618
+ }
25619
+ return addServer({
25620
+ name: server.name,
25621
+ description: server.description,
25622
+ command,
25623
+ args,
25624
+ transport,
25625
+ source: "registry"
25626
+ });
25627
+ }
25628
+
25629
+ // src/lib/finder.ts
25630
+ init_sources();
25631
+ async function listAwesomeServers() {
25632
+ const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
25633
+ const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
25634
+ if (!source)
25635
+ return [];
25636
+ return searchSource2(source, "");
25637
+ }
25638
+
25639
+ // src/cli/index.tsx
25640
+ init_sources();
25641
+
25642
+ // src/lib/install.ts
25643
+ import { execFileSync as execFileSync2 } from "child_process";
25644
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
25645
+ import { join as join3 } from "path";
25646
+ import { homedir as homedir2 } from "os";
25647
+ function installToClaude(entry) {
25648
+ try {
25649
+ const args = [
25650
+ "mcp",
25651
+ "add",
25652
+ "--transport",
25653
+ entry.transport,
25654
+ "--scope",
25655
+ "user"
25656
+ ];
25657
+ for (const [k, v] of Object.entries(entry.env)) {
25658
+ args.push("--env", `${k}=${v}`);
25659
+ }
25660
+ args.push(entry.id, "--", entry.command, ...entry.args);
25661
+ execFileSync2("claude", args, { stdio: "pipe" });
25662
+ return { agent: "claude", success: true };
25663
+ } catch (err) {
25664
+ return { agent: "claude", success: false, error: err.message };
25665
+ }
25666
+ }
25667
+ function installToCodex(entry) {
25668
+ try {
25669
+ const configDir = join3(homedir2(), ".codex");
25670
+ const configPath = join3(configDir, "config.toml");
25671
+ if (!existsSync2(configDir)) {
25672
+ mkdirSync3(configDir, { recursive: true });
25673
+ }
25674
+ const block = `
25675
+ [mcp_servers.${entry.id}]
25676
+ ` + `command = ${JSON.stringify(entry.command)}
25677
+ ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
25678
+ `;
25679
+ const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
25680
+ if (existing.includes(`[mcp_servers.${entry.id}]`)) {
25681
+ return { agent: "codex", success: true };
25682
+ }
25683
+ writeFileSync2(configPath, existing + block, "utf-8");
25684
+ return { agent: "codex", success: true };
25685
+ } catch (err) {
25686
+ return { agent: "codex", success: false, error: err.message };
25687
+ }
25004
25688
  }
25689
+ function installToGemini(entry) {
25690
+ try {
25691
+ const configDir = join3(homedir2(), ".gemini");
25692
+ const configPath = join3(configDir, "settings.json");
25693
+ if (!existsSync2(configDir)) {
25694
+ mkdirSync3(configDir, { recursive: true });
25695
+ }
25696
+ let settings = {};
25697
+ if (existsSync2(configPath)) {
25698
+ settings = JSON.parse(readFileSync2(configPath, "utf-8"));
25699
+ }
25700
+ if (!settings.mcpServers)
25701
+ settings.mcpServers = {};
25702
+ settings.mcpServers[entry.id] = {
25703
+ command: entry.command,
25704
+ args: entry.args,
25705
+ ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
25706
+ };
25707
+ writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
25708
+ return { agent: "gemini", success: true };
25709
+ } catch (err) {
25710
+ return { agent: "gemini", success: false, error: err.message };
25711
+ }
25712
+ }
25713
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
25714
+ return targets.map((target) => {
25715
+ if (target === "claude")
25716
+ return installToClaude(entry);
25717
+ if (target === "codex")
25718
+ return installToCodex(entry);
25719
+ if (target === "gemini")
25720
+ return installToGemini(entry);
25721
+ return { agent: target, success: false, error: "Unknown target" };
25722
+ });
25723
+ }
25724
+
25725
+ // src/cli/index.tsx
25726
+ init_db();
25727
+ import * as readline from "readline";
25005
25728
 
25006
25729
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
25007
25730
  class ExperimentalServerTasks {
@@ -26224,14 +26947,31 @@ class StdioServerTransport {
26224
26947
  }
26225
26948
 
26226
26949
  // src/mcp/index.ts
26950
+ import { readFileSync as readFileSync3 } from "fs";
26951
+ import { join as join4, dirname as dirname2 } from "path";
26952
+ import { fileURLToPath } from "url";
26953
+ init_sources();
26954
+ init_config();
26955
+ function redactServerEnv(server) {
26956
+ return { ...server, env: {} };
26957
+ }
26958
+ var VERSION = (() => {
26959
+ try {
26960
+ const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
26961
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
26962
+ return pkg.version || "0.0.1";
26963
+ } catch {
26964
+ return "0.0.1";
26965
+ }
26966
+ })();
26227
26967
  var server = new McpServer({
26228
26968
  name: "mcps",
26229
- version: "0.0.1"
26969
+ version: VERSION
26230
26970
  });
26231
26971
  server.tool("list_servers", "List all registered MCP servers", {}, async () => {
26232
26972
  const servers = listServers();
26233
26973
  return {
26234
- content: [{ type: "text", text: JSON.stringify(servers, null, 2) }]
26974
+ content: [{ type: "text", text: JSON.stringify(servers.map(redactServerEnv), null, 2) }]
26235
26975
  };
26236
26976
  });
26237
26977
  server.tool("search_registry", "Search the official MCP registry for servers", { query: exports_external.string().describe("Search query") }, async ({ query }) => {
@@ -26282,12 +27022,26 @@ server.tool("remove_server", "Remove a registered MCP server", { id: exports_ext
26282
27022
  };
26283
27023
  });
26284
27024
  server.tool("enable_server", "Enable a registered MCP server", { id: exports_external.string().describe("Server ID to enable") }, async ({ id }) => {
27025
+ const existing = getServer(id);
27026
+ if (!existing) {
27027
+ return {
27028
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27029
+ isError: true
27030
+ };
27031
+ }
26285
27032
  const entry = enableServer(id);
26286
27033
  return {
26287
27034
  content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
26288
27035
  };
26289
27036
  });
26290
27037
  server.tool("disable_server", "Disable a registered MCP server", { id: exports_external.string().describe("Server ID to disable") }, async ({ id }) => {
27038
+ const existing = getServer(id);
27039
+ if (!existing) {
27040
+ return {
27041
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27042
+ isError: true
27043
+ };
27044
+ }
26291
27045
  const entry = disableServer(id);
26292
27046
  return {
26293
27047
  content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
@@ -26302,36 +27056,161 @@ server.tool("get_server_info", "Get detailed information about a registered MCP
26302
27056
  };
26303
27057
  }
26304
27058
  return {
26305
- content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
27059
+ content: [{ type: "text", text: JSON.stringify(redactServerEnv(entry), null, 2) }]
26306
27060
  };
26307
27061
  });
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();
27062
+ 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.", {
27063
+ query: exports_external.string().describe("Search query (e.g. 'filesystem', 'postgres', 'browser')"),
27064
+ sources: exports_external.array(exports_external.string()).optional().describe("Source IDs to search (default: all enabled). Use list_sources to get IDs."),
27065
+ limit: exports_external.number().optional().describe("Max results per source (default: 20)")
27066
+ }, async ({ query, sources, limit }) => {
27067
+ const results = await findServers(query, { sources, limit });
26311
27068
  return {
26312
- content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
27069
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
26313
27070
  };
26314
27071
  });
26315
- server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`, {
26316
- tool_name: exports_external.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
26317
- arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
26318
- }, async ({ tool_name, arguments: args }) => {
26319
- try {
26320
- const result = await callTool(tool_name, args || {});
26321
- return { content: result.content };
26322
- } catch (err) {
27072
+ server.tool("list_sources", "List all configured search sources for finding MCP servers", {}, async () => {
27073
+ const sources = listSources();
27074
+ return {
27075
+ content: [{ type: "text", text: JSON.stringify(sources, null, 2) }]
27076
+ };
27077
+ });
27078
+ server.tool("add_source", "Add a new search source for finding MCP servers", {
27079
+ name: exports_external.string().describe("Source name"),
27080
+ type: exports_external.enum(["mcp-registry", "awesome-list", "npm-search", "github-topic"]).describe("Source type"),
27081
+ url: exports_external.string().describe("Source URL endpoint"),
27082
+ description: exports_external.string().optional().describe("Description")
27083
+ }, async ({ name, type, url: url2, description }) => {
27084
+ const source = addSource({ name, type, url: url2, description });
27085
+ return {
27086
+ content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
27087
+ };
27088
+ });
27089
+ server.tool("remove_source", "Remove a search source by ID", { id: exports_external.string().describe("Source ID to remove") }, async ({ id }) => {
27090
+ const existing = getSource(id);
27091
+ if (!existing) {
26323
27092
  return {
26324
- content: [{ type: "text", text: `Error: ${err.message}` }],
27093
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
26325
27094
  isError: true
26326
27095
  };
26327
27096
  }
27097
+ removeSource(id);
27098
+ return {
27099
+ content: [{ type: "text", text: `Removed source: ${existing.name} [${id}]` }]
27100
+ };
26328
27101
  });
26329
- async function startMcpServer() {
26330
- const transport = new StdioServerTransport;
26331
- await server.connect(transport);
26332
- }
26333
- var isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/mcp/index.ts") || process.argv[1]?.endsWith("/bin/mcp.js");
26334
- if (isDirectRun) {
27102
+ server.tool("enable_source_finder", "Enable a search source", { id: exports_external.string().describe("Source ID to enable") }, async ({ id }) => {
27103
+ const existing = getSource(id);
27104
+ if (!existing) {
27105
+ return {
27106
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
27107
+ isError: true
27108
+ };
27109
+ }
27110
+ enableSource(id);
27111
+ return {
27112
+ content: [{ type: "text", text: `Enabled source: ${existing.name}` }]
27113
+ };
27114
+ });
27115
+ server.tool("disable_source_finder", "Disable a search source", { id: exports_external.string().describe("Source ID to disable") }, async ({ id }) => {
27116
+ const existing = getSource(id);
27117
+ if (!existing) {
27118
+ return {
27119
+ content: [{ type: "text", text: `Source "${id}" not found.` }],
27120
+ isError: true
27121
+ };
27122
+ }
27123
+ disableSource(id);
27124
+ return {
27125
+ content: [{ type: "text", text: `Disabled source: ${existing.name}` }]
27126
+ };
27127
+ });
27128
+ server.tool("install_to_agents", "Install a registered MCP server into Claude Code, Codex, and/or Gemini", {
27129
+ id: exports_external.string().describe("Server ID to install (from list_servers)"),
27130
+ targets: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
27131
+ }, async ({ id, targets }) => {
27132
+ const entry = getServer(id);
27133
+ if (!entry) {
27134
+ return {
27135
+ content: [{ type: "text", text: `Server "${id}" not found.` }],
27136
+ isError: true
27137
+ };
27138
+ }
27139
+ const agentTargets = targets ?? ["claude", "codex", "gemini"];
27140
+ const results = installToAgents(entry, agentTargets);
27141
+ return {
27142
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
27143
+ };
27144
+ });
27145
+ server.tool("list_awesome_servers", "List all MCP servers from the curated punkpeye/awesome-mcp-servers GitHub list", {}, async () => {
27146
+ const results = await listAwesomeServers();
27147
+ return {
27148
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
27149
+ };
27150
+ });
27151
+ server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
27152
+ let tools = [];
27153
+ try {
27154
+ await connectAllEnabled();
27155
+ tools = listAllTools();
27156
+ } finally {
27157
+ await disconnectAll().catch(() => {
27158
+ return;
27159
+ });
27160
+ }
27161
+ return {
27162
+ content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
27163
+ };
27164
+ });
27165
+ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`, {
27166
+ tool_name: exports_external.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
27167
+ arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
27168
+ }, async ({ tool_name, arguments: args }) => {
27169
+ try {
27170
+ const sepIdx = tool_name.indexOf(TOOL_PREFIX_SEPARATOR);
27171
+ if (sepIdx === -1) {
27172
+ return {
27173
+ content: [{ type: "text", text: `Error: Invalid tool name "${tool_name}"` }],
27174
+ isError: true
27175
+ };
27176
+ }
27177
+ const serverId = tool_name.slice(0, sepIdx);
27178
+ const entry = getServer(serverId);
27179
+ if (!entry) {
27180
+ return {
27181
+ content: [{ type: "text", text: `Error: Server "${serverId}" not found.` }],
27182
+ isError: true
27183
+ };
27184
+ }
27185
+ if (!entry.enabled) {
27186
+ return {
27187
+ content: [{ type: "text", text: `Error: Server "${serverId}" is disabled.` }],
27188
+ isError: true
27189
+ };
27190
+ }
27191
+ await connectToServer(entry);
27192
+ const result = await callTool(tool_name, args || {});
27193
+ return { content: result.content };
27194
+ } catch (err) {
27195
+ return {
27196
+ content: [{ type: "text", text: `Error: ${err.message}` }],
27197
+ isError: true
27198
+ };
27199
+ }
27200
+ });
27201
+ server.tool("diagnose_server", "Run health checks on a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
27202
+ const entry = getServer(id);
27203
+ if (!entry)
27204
+ return { content: [{ type: "text", text: `Server "${id}" not found.` }], isError: true };
27205
+ const report = await diagnoseServer(entry);
27206
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
27207
+ });
27208
+ async function startMcpServer() {
27209
+ const transport = new StdioServerTransport;
27210
+ await server.connect(transport);
27211
+ }
27212
+ var isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/mcp/index.ts") || process.argv[1]?.endsWith("/bin/mcp.js");
27213
+ if (isDirectRun) {
26335
27214
  startMcpServer().catch((error2) => {
26336
27215
  console.error("MCP server error:", error2);
26337
27216
  process.exit(1);
@@ -26339,27 +27218,31 @@ if (isDirectRun) {
26339
27218
  }
26340
27219
 
26341
27220
  // src/server/serve.ts
26342
- import { existsSync } from "fs";
26343
- import { join as join2, dirname, extname } from "path";
26344
- import { fileURLToPath } from "url";
27221
+ import { existsSync as existsSync3 } from "fs";
27222
+ import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
27223
+ import { fileURLToPath as fileURLToPath2 } from "url";
27224
+ init_db();
27225
+ function redactServer(server2) {
27226
+ return { ...server2, env: {} };
27227
+ }
26345
27228
  function resolveDashboardDir() {
26346
27229
  const candidates = [];
26347
27230
  try {
26348
- const scriptDir = dirname(fileURLToPath(import.meta.url));
26349
- candidates.push(join2(scriptDir, "..", "dashboard", "dist"));
26350
- candidates.push(join2(scriptDir, "..", "..", "dashboard", "dist"));
27231
+ const scriptDir = dirname3(fileURLToPath2(import.meta.url));
27232
+ candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
27233
+ candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
26351
27234
  } catch {}
26352
27235
  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"));
27236
+ const mainDir = dirname3(process.argv[1]);
27237
+ candidates.push(join5(mainDir, "..", "dashboard", "dist"));
27238
+ candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
26356
27239
  }
26357
- candidates.push(join2(process.cwd(), "dashboard", "dist"));
27240
+ candidates.push(join5(process.cwd(), "dashboard", "dist"));
26358
27241
  for (const candidate of candidates) {
26359
- if (existsSync(candidate))
27242
+ if (existsSync3(candidate))
26360
27243
  return candidate;
26361
27244
  }
26362
- return join2(process.cwd(), "dashboard", "dist");
27245
+ return join5(process.cwd(), "dashboard", "dist");
26363
27246
  }
26364
27247
  var MIME_TYPES = {
26365
27248
  ".html": "text/html; charset=utf-8",
@@ -26375,13 +27258,15 @@ var MIME_TYPES = {
26375
27258
  };
26376
27259
  var SECURITY_HEADERS = {
26377
27260
  "X-Content-Type-Options": "nosniff",
26378
- "X-Frame-Options": "DENY"
27261
+ "X-Frame-Options": "DENY",
27262
+ "Referrer-Policy": "no-referrer"
26379
27263
  };
26380
27264
  function json(data, status = 200, port) {
26381
27265
  return new Response(JSON.stringify(data), {
26382
27266
  status,
26383
27267
  headers: {
26384
27268
  "Content-Type": "application/json",
27269
+ "Cache-Control": "no-store",
26385
27270
  "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
26386
27271
  ...SECURITY_HEADERS
26387
27272
  }
@@ -26391,27 +27276,94 @@ function isValidId(id) {
26391
27276
  return /^[a-z0-9-]+$/.test(id);
26392
27277
  }
26393
27278
  var MAX_BODY_SIZE = 1024 * 1024;
27279
+ function isLoopbackHost(hostname2) {
27280
+ return hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1";
27281
+ }
27282
+ function isAllowedOrigin(req, port, host) {
27283
+ const origin = req.headers.get("origin");
27284
+ if (!origin)
27285
+ return true;
27286
+ if (origin === "null")
27287
+ return false;
27288
+ try {
27289
+ const url2 = new URL(origin);
27290
+ if (url2.port && url2.port !== String(port))
27291
+ return false;
27292
+ if (isLoopbackHost(url2.hostname))
27293
+ return true;
27294
+ const normalizedHost = host === "0.0.0.0" ? "127.0.0.1" : host;
27295
+ return url2.hostname === normalizedHost;
27296
+ } catch {
27297
+ return false;
27298
+ }
27299
+ }
27300
+ function isAuthorized(req, host) {
27301
+ const token = process.env.MCPS_API_TOKEN;
27302
+ if (!token) {
27303
+ return isLoopbackHost(host);
27304
+ }
27305
+ const auth2 = req.headers.get("authorization");
27306
+ if (auth2 === `Bearer ${token}`)
27307
+ return true;
27308
+ return isLoopbackHost(host);
27309
+ }
27310
+ function unauthorizedResponse(port) {
27311
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
27312
+ status: 401,
27313
+ headers: {
27314
+ "Content-Type": "application/json",
27315
+ "Cache-Control": "no-store",
27316
+ "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
27317
+ "WWW-Authenticate": "Bearer",
27318
+ ...SECURITY_HEADERS
27319
+ }
27320
+ });
27321
+ }
26394
27322
  function getAllServersWithToolCount() {
26395
27323
  const servers = listServers();
27324
+ if (servers.length === 0)
27325
+ return [];
27326
+ const counts = getToolCounts();
26396
27327
  return servers.map((s) => ({
26397
- ...s,
26398
- toolCount: getCachedTools(s.id).length
27328
+ ...redactServer(s),
27329
+ toolCount: counts.get(s.id) ?? 0
26399
27330
  }));
26400
27331
  }
26401
27332
  function serveStaticFile(filePath) {
26402
- if (!existsSync(filePath))
27333
+ if (!existsSync3(filePath))
26403
27334
  return null;
26404
27335
  const ext = extname(filePath);
26405
27336
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
26406
27337
  return new Response(Bun.file(filePath), {
26407
- headers: { "Content-Type": contentType }
27338
+ headers: { "Content-Type": contentType, ...SECURITY_HEADERS }
26408
27339
  });
26409
27340
  }
27341
+ function resolveStaticPath(baseDir, urlPath) {
27342
+ let decoded;
27343
+ try {
27344
+ decoded = decodeURIComponent(urlPath);
27345
+ } catch {
27346
+ return null;
27347
+ }
27348
+ const stripped = decoded.replace(/^\/+/, "");
27349
+ const resolved = resolve(baseDir, stripped);
27350
+ const rel = relative(baseDir, resolved);
27351
+ if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
27352
+ return null;
27353
+ }
27354
+ return resolved;
27355
+ }
27356
+ function formatHostForUrl(host) {
27357
+ if (host.includes(":") && !host.startsWith("["))
27358
+ return `[${host}]`;
27359
+ return host;
27360
+ }
26410
27361
  async function startServer(port, options) {
26411
27362
  const shouldOpen = options?.open ?? true;
27363
+ const host = options?.host ?? "127.0.0.1";
26412
27364
  getDb();
26413
27365
  const dashboardDir = resolveDashboardDir();
26414
- const dashboardExists = existsSync(dashboardDir);
27366
+ const dashboardExists = existsSync3(dashboardDir);
26415
27367
  if (!dashboardExists) {
26416
27368
  console.error(`
26417
27369
  Dashboard not found at: ${dashboardDir}`);
@@ -26422,10 +27374,19 @@ Dashboard not found at: ${dashboardDir}`);
26422
27374
  }
26423
27375
  const server2 = Bun.serve({
26424
27376
  port,
27377
+ hostname: host,
26425
27378
  async fetch(req) {
26426
27379
  const url2 = new URL(req.url);
26427
27380
  const path = url2.pathname;
26428
27381
  const method = req.method;
27382
+ if (path.startsWith("/api/") && method !== "OPTIONS") {
27383
+ if (!isAuthorized(req, host)) {
27384
+ return unauthorizedResponse(port);
27385
+ }
27386
+ if (!isAllowedOrigin(req, port, host)) {
27387
+ return json({ error: "Forbidden" }, 403, port);
27388
+ }
27389
+ }
26429
27390
  if (path === "/api/servers" && method === "GET") {
26430
27391
  return json(getAllServersWithToolCount(), 200, port);
26431
27392
  }
@@ -26434,15 +27395,39 @@ Dashboard not found at: ${dashboardDir}`);
26434
27395
  const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
26435
27396
  if (contentLength > MAX_BODY_SIZE)
26436
27397
  return json({ error: "Request body too large" }, 413, port);
26437
- const body = await req.json();
26438
- if (!body.command)
27398
+ let body;
27399
+ try {
27400
+ body = await req.json();
27401
+ } catch {
27402
+ return json({ error: "Invalid JSON body" }, 400, port);
27403
+ }
27404
+ const command = body.command?.trim();
27405
+ if (!command)
26439
27406
  return json({ error: "Missing 'command'" }, 400, port);
27407
+ const transport = body.transport || "stdio";
27408
+ if (!["stdio", "sse", "streamable-http"].includes(transport)) {
27409
+ return json({ error: "Invalid transport type" }, 400, port);
27410
+ }
27411
+ if (transport !== "stdio" && !body.url) {
27412
+ return json({ error: "Missing 'url' for non-stdio transport" }, 400, port);
27413
+ }
27414
+ if (body.url) {
27415
+ try {
27416
+ new URL(body.url);
27417
+ } catch {
27418
+ return json({ error: "Invalid 'url' format" }, 400, port);
27419
+ }
27420
+ }
27421
+ if (body.args && (!Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string"))) {
27422
+ return json({ error: "Invalid 'args' format" }, 400, port);
27423
+ }
27424
+ const args = body.args || [];
26440
27425
  const entry = addServer({
26441
27426
  name: body.name,
26442
- command: body.command,
26443
- args: body.args || [],
27427
+ command,
27428
+ args,
26444
27429
  description: body.description,
26445
- transport: body.transport || "stdio",
27430
+ transport,
26446
27431
  url: body.url
26447
27432
  });
26448
27433
  return json(entry, 200, port);
@@ -26459,7 +27444,7 @@ Dashboard not found at: ${dashboardDir}`);
26459
27444
  if (!entry)
26460
27445
  return json({ error: `Server '${id}' not found` }, 404, port);
26461
27446
  const tools = getCachedTools(id);
26462
- return json({ ...entry, toolCount: tools.length, tools }, 200, port);
27447
+ return json({ ...redactServer(entry), toolCount: tools.length, tools }, 200, port);
26463
27448
  }
26464
27449
  if (singleMatch && method === "DELETE") {
26465
27450
  const id = singleMatch[1];
@@ -26477,6 +27462,9 @@ Dashboard not found at: ${dashboardDir}`);
26477
27462
  if (!isValidId(id))
26478
27463
  return json({ error: "Invalid server ID" }, 400, port);
26479
27464
  try {
27465
+ const existing = getServer(id);
27466
+ if (!existing)
27467
+ return json({ error: `Server '${id}' not found` }, 404, port);
26480
27468
  enableServer(id);
26481
27469
  return json({ success: true }, 200, port);
26482
27470
  } catch (e) {
@@ -26489,13 +27477,47 @@ Dashboard not found at: ${dashboardDir}`);
26489
27477
  if (!isValidId(id))
26490
27478
  return json({ error: "Invalid server ID" }, 400, port);
26491
27479
  try {
27480
+ const existing = getServer(id);
27481
+ if (!existing)
27482
+ return json({ error: `Server '${id}' not found` }, 404, port);
26492
27483
  disableServer(id);
26493
27484
  return json({ success: true }, 200, port);
26494
27485
  } catch (e) {
26495
27486
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
26496
27487
  }
26497
27488
  }
27489
+ if (path === "/api/update" && method === "POST") {
27490
+ if (!isLoopbackHost(host)) {
27491
+ return json({ error: "Update only allowed on loopback host" }, 403, port);
27492
+ }
27493
+ try {
27494
+ const { execFileSync: execFileSync3 } = await import("child_process");
27495
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
27496
+ const currentVersion = pkg.version;
27497
+ const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
27498
+ encoding: "utf-8"
27499
+ }).trim();
27500
+ if (latest === currentVersion) {
27501
+ return json({ success: true, current: currentVersion, latest, upToDate: true }, 200, port);
27502
+ }
27503
+ execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "pipe" });
27504
+ return json({ success: true, current: currentVersion, latest, upToDate: false, updated: true }, 200, port);
27505
+ } catch (e) {
27506
+ return json({ error: e instanceof Error ? e.message : "Update failed" }, 500, port);
27507
+ }
27508
+ }
27509
+ if (path === "/api/version" && method === "GET") {
27510
+ try {
27511
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
27512
+ return json({ version: pkg.version }, 200, port);
27513
+ } catch {
27514
+ return json({ version: "unknown" }, 200, port);
27515
+ }
27516
+ }
26498
27517
  if (method === "OPTIONS") {
27518
+ if (!isAllowedOrigin(req, port, host)) {
27519
+ return json({ error: "Forbidden" }, 403, port);
27520
+ }
26499
27521
  return new Response(null, {
26500
27522
  headers: {
26501
27523
  "Access-Control-Allow-Origin": `http://localhost:${port}`,
@@ -26506,12 +27528,14 @@ Dashboard not found at: ${dashboardDir}`);
26506
27528
  }
26507
27529
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
26508
27530
  if (path !== "/") {
26509
- const filePath = join2(dashboardDir, path);
26510
- const res2 = serveStaticFile(filePath);
26511
- if (res2)
26512
- return res2;
27531
+ const safePath = resolveStaticPath(dashboardDir, path);
27532
+ if (safePath) {
27533
+ const res2 = serveStaticFile(safePath);
27534
+ if (res2)
27535
+ return res2;
27536
+ }
26513
27537
  }
26514
- const indexPath = join2(dashboardDir, "index.html");
27538
+ const indexPath = join5(dashboardDir, "index.html");
26515
27539
  const res = serveStaticFile(indexPath);
26516
27540
  if (res)
26517
27541
  return res;
@@ -26526,19 +27550,24 @@ Dashboard not found at: ${dashboardDir}`);
26526
27550
  };
26527
27551
  process.on("SIGINT", shutdown);
26528
27552
  process.on("SIGTERM", shutdown);
26529
- const serverUrl = `http://localhost:${port}`;
27553
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
27554
+ const serverUrl = `http://${formatHostForUrl(displayHost)}:${port}`;
26530
27555
  console.log(`MCPs Dashboard running at ${serverUrl}`);
26531
27556
  if (shouldOpen) {
26532
27557
  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}`);
27558
+ const { execFile } = await import("child_process");
27559
+ if (process.platform === "win32") {
27560
+ execFile("cmd", ["/c", "start", "", serverUrl]);
27561
+ } else {
27562
+ const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
27563
+ execFile(openCmd, [serverUrl]);
27564
+ }
26536
27565
  } catch {}
26537
27566
  }
26538
27567
  }
26539
27568
 
26540
27569
  // src/cli/components/App.tsx
26541
- import { useState as useState7 } from "react";
27570
+ import { useEffect as useEffect5, useState as useState7 } from "react";
26542
27571
  import { Box as Box7, Text as Text9, useApp, useInput as useInput4 } from "ink";
26543
27572
 
26544
27573
  // src/cli/components/ServerList.tsx
@@ -26951,6 +27980,7 @@ var SelectInput_default = SelectInput;
26951
27980
  import { jsxDEV } from "react/jsx-dev-runtime";
26952
27981
  function ServerList({ onSelect, onSearch }) {
26953
27982
  const servers = listServers();
27983
+ const toolCounts = getToolCounts();
26954
27984
  useInput2((input) => {
26955
27985
  if (input === "s") {
26956
27986
  onSearch();
@@ -26972,10 +28002,9 @@ function ServerList({ onSelect, onSearch }) {
26972
28002
  }, undefined, true, undefined, this);
26973
28003
  }
26974
28004
  const items = servers.map((s) => {
26975
- const cached2 = getCachedTools(s.id);
28005
+ const cachedCount = toolCounts.get(s.id) ?? 0;
26976
28006
  const status = s.enabled ? "\u25CF" : "\u25CB";
26977
- const statusColor = s.enabled ? "green" : "red";
26978
- const toolInfo = cached2.length > 0 ? ` (${cached2.length} tools)` : "";
28007
+ const toolInfo = cachedCount > 0 ? ` (${cachedCount} tools)` : "";
26979
28008
  return {
26980
28009
  label: `${status} ${s.name} [${s.id}]${toolInfo}`,
26981
28010
  value: s.id,
@@ -27033,7 +28062,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
27033
28062
  const [loading, setLoading] = useState3(false);
27034
28063
  const [error2, setError] = useState3(null);
27035
28064
  const cachedTools = getCachedTools(server2.id);
28065
+ const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
27036
28066
  useEffect3(() => {
28067
+ setLoading(false);
28068
+ setError(null);
27037
28069
  if (cachedTools.length > 0) {
27038
28070
  setTools(cachedTools.map((t) => ({
27039
28071
  server_id: server2.id,
@@ -27041,8 +28073,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
27041
28073
  description: t.description,
27042
28074
  input_schema: t.input_schema
27043
28075
  })));
28076
+ } else {
28077
+ setTools([]);
27044
28078
  }
27045
- }, [server2.id]);
28079
+ }, [server2.id, cachedKey]);
27046
28080
  const handleConnect = async () => {
27047
28081
  setLoading(true);
27048
28082
  setError(null);
@@ -27366,6 +28400,7 @@ function SearchView({ onBack }) {
27366
28400
  // src/cli/components/ToolCall.tsx
27367
28401
  import { useState as useState6 } from "react";
27368
28402
  import { Box as Box6, Text as Text8 } from "ink";
28403
+ init_config();
27369
28404
  import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
27370
28405
  function ToolCall({ server: server2, tool, onBack }) {
27371
28406
  const [argsInput, setArgsInput] = useState6("{}");
@@ -27496,6 +28531,7 @@ function ToolCall({ server: server2, tool, onBack }) {
27496
28531
  }
27497
28532
 
27498
28533
  // src/cli/components/App.tsx
28534
+ init_db();
27499
28535
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
27500
28536
  function App() {
27501
28537
  const { exit } = useApp();
@@ -27503,7 +28539,7 @@ function App() {
27503
28539
  const [selectedServer, setSelectedServer] = useState7(null);
27504
28540
  const [selectedTool, setSelectedTool] = useState7(null);
27505
28541
  useInput4((input, key) => {
27506
- if (input === "q" && view === "servers") {
28542
+ if (input === "q" && view !== "search" && view !== "call") {
27507
28543
  exit();
27508
28544
  }
27509
28545
  if (key.escape) {
@@ -27520,6 +28556,13 @@ function App() {
27520
28556
  }
27521
28557
  }
27522
28558
  });
28559
+ useEffect5(() => {
28560
+ return () => {
28561
+ disconnectAll().catch(() => {
28562
+ return;
28563
+ }).finally(() => closeDb());
28564
+ };
28565
+ }, []);
27523
28566
  const handleSelectServer = (server2) => {
27524
28567
  setSelectedServer(server2);
27525
28568
  setView("detail");
@@ -27581,7 +28624,7 @@ function App() {
27581
28624
  marginTop: 1,
27582
28625
  children: /* @__PURE__ */ jsxDEV5(Text9, {
27583
28626
  dimColor: true,
27584
- children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : "esc back \xB7 q quit"
28627
+ 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
28628
  }, undefined, false, undefined, this)
27586
28629
  }, undefined, false, undefined, this)
27587
28630
  ]
@@ -27589,20 +28632,37 @@ function App() {
27589
28632
  }
27590
28633
 
27591
28634
  // src/cli/index.tsx
28635
+ var VERSION2 = (() => {
28636
+ try {
28637
+ const pkgPath = join6(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
28638
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
28639
+ return pkg.version || "0.0.1";
28640
+ } catch {
28641
+ return "0.0.1";
28642
+ }
28643
+ })();
27592
28644
  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(() => {
28645
+ program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
28646
+ program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").action((opts) => {
27595
28647
  const servers = listServers();
28648
+ if (opts.json) {
28649
+ const toolCounts2 = getToolCounts();
28650
+ console.log(JSON.stringify(servers.map((s) => ({ ...s, toolCount: toolCounts2.get(s.id) ?? 0 })), null, 2));
28651
+ closeDb();
28652
+ return;
28653
+ }
27596
28654
  if (servers.length === 0) {
27597
28655
  console.log(chalk2.dim("No servers registered. Use `mcps add` or `mcps search` to get started."));
27598
28656
  closeDb();
27599
28657
  return;
27600
28658
  }
28659
+ const toolCounts = getToolCounts();
27601
28660
  for (const s of servers) {
27602
28661
  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}`);
28662
+ const cachedCount = toolCounts.get(s.id) ?? 0;
28663
+ const toolCount = cachedCount > 0 ? chalk2.dim(` (${cachedCount} tools)`) : "";
28664
+ const errorWarning = s.last_error ? chalk2.red(" \u26A0") : "";
28665
+ console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
27606
28666
  if (s.description)
27607
28667
  console.log(` ${chalk2.dim(s.description)}`);
27608
28668
  console.log(` ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
@@ -27630,8 +28690,9 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
27630
28690
  } catch (err) {
27631
28691
  console.error(chalk2.red(`Search failed: ${err.message}`));
27632
28692
  process.exit(1);
28693
+ } finally {
28694
+ closeDb();
27633
28695
  }
27634
- closeDb();
27635
28696
  });
27636
28697
  program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").description("Add a local MCP server").action(async (command, args, opts) => {
27637
28698
  try {
@@ -27645,12 +28706,15 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
27645
28706
  }
27646
28707
  if (!command) {
27647
28708
  console.error(chalk2.red("Error: command is required (or use --from-registry)"));
28709
+ closeDb();
27648
28710
  process.exit(1);
27649
28711
  }
27650
28712
  const envMap = {};
27651
28713
  if (opts.env) {
27652
28714
  for (const pair of opts.env) {
27653
28715
  const [key, ...rest] = pair.split("=");
28716
+ if (!key)
28717
+ continue;
27654
28718
  envMap[key] = rest.join("=");
27655
28719
  }
27656
28720
  }
@@ -27671,6 +28735,7 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
27671
28735
  } else {
27672
28736
  console.error(chalk2.red(`Failed to add server: ${err.message}`));
27673
28737
  }
28738
+ closeDb();
27674
28739
  process.exit(1);
27675
28740
  }
27676
28741
  closeDb();
@@ -27679,6 +28744,7 @@ program2.command("remove").argument("<id>", "Server ID to remove").description("
27679
28744
  const server2 = getServer(id);
27680
28745
  if (!server2) {
27681
28746
  console.error(chalk2.red(`Server "${id}" not found.`));
28747
+ closeDb();
27682
28748
  process.exit(1);
27683
28749
  }
27684
28750
  removeServer(id);
@@ -27689,6 +28755,7 @@ program2.command("enable").argument("<id>", "Server ID to enable").description("
27689
28755
  const server2 = getServer(id);
27690
28756
  if (!server2) {
27691
28757
  console.error(chalk2.red(`Server "${id}" not found.`));
28758
+ closeDb();
27692
28759
  process.exit(1);
27693
28760
  }
27694
28761
  enableServer(id);
@@ -27699,6 +28766,7 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
27699
28766
  const server2 = getServer(id);
27700
28767
  if (!server2) {
27701
28768
  console.error(chalk2.red(`Server "${id}" not found.`));
28769
+ closeDb();
27702
28770
  process.exit(1);
27703
28771
  }
27704
28772
  disableServer(id);
@@ -27778,6 +28846,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
27778
28846
  }
27779
28847
  }
27780
28848
  }
28849
+ let exitCode = 0;
27781
28850
  try {
27782
28851
  console.log(chalk2.dim(`Connecting to servers...`));
27783
28852
  await connectAllEnabled();
@@ -27786,17 +28855,22 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
27786
28855
  for (const c of result.content) {
27787
28856
  console.log(c.text);
27788
28857
  }
27789
- await disconnectAll();
27790
28858
  } catch (err) {
27791
28859
  console.error(chalk2.red(`Call failed: ${err.message}`));
27792
- process.exit(1);
28860
+ exitCode = 1;
27793
28861
  }
28862
+ await disconnectAll().catch(() => {
28863
+ return;
28864
+ });
27794
28865
  closeDb();
28866
+ if (exitCode !== 0)
28867
+ process.exit(exitCode);
27795
28868
  });
27796
28869
  program2.command("info").argument("<id>", "Server ID").description("Show server details & tools").action((id) => {
27797
28870
  const server2 = getServer(id);
27798
28871
  if (!server2) {
27799
28872
  console.error(chalk2.red(`Server "${id}" not found.`));
28873
+ closeDb();
27800
28874
  process.exit(1);
27801
28875
  }
27802
28876
  console.log(chalk2.bold(server2.name) + " " + chalk2.dim(`[${server2.id}]`));
@@ -27825,21 +28899,485 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
27825
28899
  }
27826
28900
  closeDb();
27827
28901
  });
27828
- program2.command("status").description("Show registry stats").action(() => {
28902
+ program2.command("status").description("Show registry stats").option("--json", "Output as JSON").action((opts) => {
27829
28903
  const servers = listServers();
27830
28904
  const enabled = servers.filter((s) => s.enabled).length;
27831
28905
  const disabled = servers.length - enabled;
28906
+ const toolCounts = getToolCounts();
27832
28907
  let totalTools = 0;
27833
- for (const s of servers) {
27834
- totalTools += getCachedTools(s.id).length;
28908
+ for (const s of servers)
28909
+ totalTools += toolCounts.get(s.id) ?? 0;
28910
+ if (opts.json) {
28911
+ console.log(JSON.stringify({ total: servers.length, enabled, disabled, totalTools }, null, 2));
28912
+ closeDb();
28913
+ return;
27835
28914
  }
27836
28915
  console.log(chalk2.bold("Registry Status"));
27837
28916
  console.log(` Servers: ${servers.length} (${chalk2.green(`${enabled} enabled`)}, ${chalk2.red(`${disabled} disabled`)})`);
27838
28917
  console.log(` Tools: ${totalTools} (cached)`);
27839
28918
  closeDb();
27840
28919
  });
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 });
28920
+ program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").action(async (id) => {
28921
+ const servers = id ? [getServer(id)].filter(Boolean) : listServers();
28922
+ if (servers.length === 0) {
28923
+ console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
28924
+ closeDb();
28925
+ return;
28926
+ }
28927
+ let allHealthy = true;
28928
+ for (const server2 of servers) {
28929
+ console.log(chalk2.bold(`
28930
+ ${server2.name} [${server2.id}]`));
28931
+ const report = await diagnoseServer(server2);
28932
+ for (const check2 of report.checks) {
28933
+ const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
28934
+ console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
28935
+ }
28936
+ if (!report.healthy)
28937
+ allHealthy = false;
28938
+ }
28939
+ console.log("");
28940
+ if (allHealthy) {
28941
+ console.log(chalk2.green("All checks passed."));
28942
+ } else {
28943
+ console.log(chalk2.red("Some checks failed. Fix issues above."));
28944
+ }
28945
+ closeDb();
28946
+ });
28947
+ program2.command("serve").description("Start the web dashboard").option("--port <port>", "Port to listen on", "19427").option("--host <host>", "Host to bind (default: 127.0.0.1)", "127.0.0.1").option("--no-open", "Don't open browser automatically").action(async (opts) => {
28948
+ await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
28949
+ });
28950
+ program2.command("update").description("Update mcps to the latest version").action(async () => {
28951
+ const { execFileSync: execFileSync3 } = await import("child_process");
28952
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
28953
+ const currentVersion = pkg.version;
28954
+ console.log(chalk2.dim(`Current version: ${currentVersion}`));
28955
+ console.log(chalk2.dim("Checking for updates..."));
28956
+ try {
28957
+ const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
28958
+ encoding: "utf-8"
28959
+ }).trim();
28960
+ if (latest === currentVersion) {
28961
+ console.log(chalk2.green(`Already on the latest version (${currentVersion}).`));
28962
+ return;
28963
+ }
28964
+ console.log(chalk2.dim(`New version available: ${latest}`));
28965
+ console.log(chalk2.dim("Updating..."));
28966
+ execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "inherit" });
28967
+ console.log(chalk2.green(`Updated to ${latest}`));
28968
+ } catch (err) {
28969
+ console.error(chalk2.red(`Update failed: ${err.message}`));
28970
+ closeDb();
28971
+ process.exit(1);
28972
+ }
28973
+ });
28974
+ 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) => {
28975
+ try {
28976
+ if (opts.awesome) {
28977
+ console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
28978
+ const results2 = await listAwesomeServers();
28979
+ if (opts.json) {
28980
+ console.log(JSON.stringify(results2, null, 2));
28981
+ closeDb();
28982
+ return;
28983
+ }
28984
+ const allSources2 = listSources();
28985
+ const sourceNameMap2 = new Map(allSources2.map((s) => [s.id, s.name]));
28986
+ for (const r of results2) {
28987
+ const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
28988
+ console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
28989
+ if (r.description)
28990
+ console.log(` ${chalk2.dim(r.description)}`);
28991
+ if (r.url)
28992
+ console.log(` ${chalk2.cyan(r.url)}`);
28993
+ }
28994
+ console.log(chalk2.dim(`
28995
+ ${results2.length} servers in awesome list.`));
28996
+ closeDb();
28997
+ return;
28998
+ }
28999
+ const q = query || "";
29000
+ const sources = opts.source;
29001
+ const limit = parseInt(opts.limit, 10) || 20;
29002
+ const noCache = opts.cache === false;
29003
+ if (!q) {
29004
+ console.log(chalk2.dim("Tip: provide a query to search, or use --awesome to browse the curated list."));
29005
+ } else {
29006
+ console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
29007
+ }
29008
+ const results = await findServers(q, { sources, limit, noCache });
29009
+ if (opts.json) {
29010
+ console.log(JSON.stringify(results, null, 2));
29011
+ closeDb();
29012
+ return;
29013
+ }
29014
+ if (results.length === 0) {
29015
+ console.log(chalk2.dim("No servers found."));
29016
+ closeDb();
29017
+ return;
29018
+ }
29019
+ const sourceColors = {
29020
+ registry: chalk2.blue,
29021
+ npm: chalk2.red,
29022
+ awesome: chalk2.yellow,
29023
+ github: chalk2.magenta
29024
+ };
29025
+ const allSources = listSources();
29026
+ const sourceNameMap = new Map(allSources.map((s) => [s.id, s.name]));
29027
+ for (let i = 0;i < results.length; i++) {
29028
+ const r = results[i];
29029
+ const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
29030
+ const sourceLabel = (sourceColors[r.source] ?? chalk2.dim)(`[${sourceName}]`);
29031
+ const stars = r.stars ? chalk2.dim(` \u2605${r.stars}`) : "";
29032
+ const idx = opts.install ? chalk2.dim(`${i + 1}. `) : " ";
29033
+ console.log(`${idx}${chalk2.bold(r.name)} ${sourceLabel}${stars}`);
29034
+ if (r.description)
29035
+ console.log(` ${chalk2.dim(r.description)}`);
29036
+ if (r.installCmd)
29037
+ console.log(` ${chalk2.green(`Install: ${r.installCmd}`)}`);
29038
+ else if (r.url)
29039
+ console.log(` ${chalk2.cyan(r.url)}`);
29040
+ }
29041
+ console.log(chalk2.dim(`
29042
+ ${results.length} result(s). Use \`mcps add --from-registry <id>\` or \`mcps add npx -y <pkg>\` to install.`));
29043
+ if (opts.install) {
29044
+ let chosen = results[0];
29045
+ if (results.length === 1 && opts.yes) {} else {
29046
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
29047
+ const answer = await new Promise((resolve2) => {
29048
+ rl.question(chalk2.cyan(`
29049
+ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
29050
+ });
29051
+ rl.close();
29052
+ const num = parseInt(answer, 10);
29053
+ if (!num || num < 1 || num > results.length) {
29054
+ console.log(chalk2.dim("Installation cancelled."));
29055
+ closeDb();
29056
+ return;
29057
+ }
29058
+ chosen = results[num - 1];
29059
+ }
29060
+ if (!chosen.npmPackage && !chosen.installCmd) {
29061
+ console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
29062
+ closeDb();
29063
+ return;
29064
+ }
29065
+ const pkg = chosen.npmPackage ?? chosen.installCmd?.replace(/^npx -y /, "");
29066
+ if (!pkg) {
29067
+ console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
29068
+ closeDb();
29069
+ return;
29070
+ }
29071
+ console.log(chalk2.dim(`Installing ${chosen.name}...`));
29072
+ const server2 = addServer({
29073
+ command: "npx",
29074
+ args: ["-y", pkg],
29075
+ name: chosen.name,
29076
+ description: chosen.description,
29077
+ transport: "stdio"
29078
+ });
29079
+ const results2 = installToAgents(server2, ["claude", "codex", "gemini"]);
29080
+ for (const r of results2) {
29081
+ if (r.success) {
29082
+ console.log(chalk2.green(` \u2713 ${r.agent}`));
29083
+ } else {
29084
+ console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
29085
+ }
29086
+ }
29087
+ console.log(chalk2.green(`
29088
+ Installed ${server2.name} [${server2.id}]`));
29089
+ }
29090
+ } catch (err) {
29091
+ console.error(chalk2.red(`Find failed: ${err.message}`));
29092
+ process.exit(1);
29093
+ } finally {
29094
+ closeDb();
29095
+ }
29096
+ });
29097
+ var sourcesCmd = program2.command("sources").description("Manage search sources");
29098
+ sourcesCmd.command("list").description("List all search sources").action(() => {
29099
+ const sources = listSources();
29100
+ if (sources.length === 0) {
29101
+ console.log(chalk2.dim("No sources configured."));
29102
+ closeDb();
29103
+ return;
29104
+ }
29105
+ for (const s of sources) {
29106
+ const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
29107
+ console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${chalk2.dim(s.type)} \u2014 ${status}`);
29108
+ if (s.description)
29109
+ console.log(` ${chalk2.dim(s.description)}`);
29110
+ console.log(` ${chalk2.cyan(s.url)}`);
29111
+ }
29112
+ closeDb();
29113
+ });
29114
+ sourcesCmd.command("add").description("Add a new search source").option("--name <name>", "Source name (required)").option("--type <type>", "Source type: mcp-registry, awesome-list, npm-search, github-topic (required)").option("--url <url>", "Source URL (required)").option("--description <desc>", "Description").option("--test", "Test the source after adding by running a sample search").action(async (opts) => {
29115
+ if (!opts.name || !opts.type || !opts.url) {
29116
+ console.error(chalk2.red("Error: --name, --type, and --url are required"));
29117
+ closeDb();
29118
+ process.exit(1);
29119
+ }
29120
+ const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
29121
+ if (!validTypes.includes(opts.type)) {
29122
+ console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
29123
+ closeDb();
29124
+ process.exit(1);
29125
+ }
29126
+ try {
29127
+ const source = addSource({
29128
+ name: opts.name,
29129
+ type: opts.type,
29130
+ url: opts.url,
29131
+ description: opts.description
29132
+ });
29133
+ console.log(chalk2.green(`Added source: ${source.name} [${source.id}]`));
29134
+ if (opts.test) {
29135
+ console.log(chalk2.dim("Testing source..."));
29136
+ const testResults = await searchSource(source, "");
29137
+ console.log(chalk2.dim(` Found ${testResults.length} results`));
29138
+ if (testResults.length === 0)
29139
+ console.log(chalk2.yellow(" Warning: source returned no results"));
29140
+ }
29141
+ } catch (err) {
29142
+ console.error(chalk2.red(`Failed to add source: ${err.message}`));
29143
+ closeDb();
29144
+ process.exit(1);
29145
+ }
29146
+ closeDb();
29147
+ });
29148
+ sourcesCmd.command("remove").argument("<id>", "Source ID to remove").description("Remove a search source").action((id) => {
29149
+ const source = getSource(id);
29150
+ if (!source) {
29151
+ console.error(chalk2.red(`Source "${id}" not found.`));
29152
+ closeDb();
29153
+ process.exit(1);
29154
+ }
29155
+ removeSource(id);
29156
+ console.log(chalk2.green(`Removed source: ${source.name} [${id}]`));
29157
+ closeDb();
29158
+ });
29159
+ sourcesCmd.command("enable").argument("<id>", "Source ID to enable").description("Enable a search source").action((id) => {
29160
+ const source = getSource(id);
29161
+ if (!source) {
29162
+ console.error(chalk2.red(`Source "${id}" not found.`));
29163
+ closeDb();
29164
+ process.exit(1);
29165
+ }
29166
+ enableSource(id);
29167
+ console.log(chalk2.green(`Enabled source: ${source.name}`));
29168
+ closeDb();
29169
+ });
29170
+ sourcesCmd.command("disable").argument("<id>", "Source ID to disable").description("Disable a search source").action((id) => {
29171
+ const source = getSource(id);
29172
+ if (!source) {
29173
+ console.error(chalk2.red(`Source "${id}" not found.`));
29174
+ closeDb();
29175
+ process.exit(1);
29176
+ }
29177
+ disableSource(id);
29178
+ console.log(chalk2.yellow(`Disabled source: ${source.name}`));
29179
+ closeDb();
29180
+ });
29181
+ sourcesCmd.command("refresh").argument("[id]", "Source ID to refresh (omit to refresh all)").description("Clear cached results for a source").action((id) => {
29182
+ clearCache(id);
29183
+ if (id) {
29184
+ console.log(chalk2.green(`Cleared cache for source: ${id}`));
29185
+ } else {
29186
+ console.log(chalk2.green("Cleared all source caches."));
29187
+ }
29188
+ closeDb();
29189
+ });
29190
+ sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Test a source by running a sample search").action(async (id) => {
29191
+ const source = getSource(id);
29192
+ if (!source) {
29193
+ console.error(chalk2.red(`Source "${id}" not found.`));
29194
+ closeDb();
29195
+ process.exit(1);
29196
+ }
29197
+ console.log(chalk2.dim(`Testing source "${source.name}"...`));
29198
+ try {
29199
+ const results = await searchSource(source, "", true);
29200
+ console.log(chalk2.green(`\u2713 Source returned ${results.length} results`));
29201
+ if (results.length > 0) {
29202
+ console.log(chalk2.dim(" Sample results:"));
29203
+ for (const r of results.slice(0, 3)) {
29204
+ console.log(` ${chalk2.bold(r.name)}: ${chalk2.dim(r.description?.slice(0, 60) || "no description")}`);
29205
+ }
29206
+ }
29207
+ } catch (err) {
29208
+ console.error(chalk2.red(`\u2717 Source test failed: ${err.message}`));
29209
+ }
29210
+ closeDb();
29211
+ });
29212
+ 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) => {
29213
+ const targets = [];
29214
+ if (opts.to) {
29215
+ for (const t of opts.to) {
29216
+ if (t === "claude" || t === "codex" || t === "gemini")
29217
+ targets.push(t);
29218
+ }
29219
+ }
29220
+ if (opts.claude && !targets.includes("claude"))
29221
+ targets.push("claude");
29222
+ if (opts.codex && !targets.includes("codex"))
29223
+ targets.push("codex");
29224
+ if (opts.gemini && !targets.includes("gemini"))
29225
+ targets.push("gemini");
29226
+ if (opts.all || targets.length === 0) {
29227
+ if (!targets.includes("claude"))
29228
+ targets.push("claude");
29229
+ if (!targets.includes("codex"))
29230
+ targets.push("codex");
29231
+ if (!targets.includes("gemini"))
29232
+ targets.push("gemini");
29233
+ }
29234
+ let server2;
29235
+ if (opts.fromRegistry) {
29236
+ console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
29237
+ try {
29238
+ server2 = await installFromRegistry(opts.fromRegistry);
29239
+ console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
29240
+ } catch (err) {
29241
+ console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
29242
+ closeDb();
29243
+ process.exit(1);
29244
+ }
29245
+ } else if (opts.npm) {
29246
+ const pkg = opts.npm;
29247
+ server2 = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
29248
+ console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
29249
+ } else {
29250
+ if (!id) {
29251
+ console.error(chalk2.red("Error: server ID is required (or use --from-registry or --npm)"));
29252
+ closeDb();
29253
+ process.exit(1);
29254
+ }
29255
+ server2 = getServer(id);
29256
+ if (!server2) {
29257
+ console.error(chalk2.red(`Server "${id}" not found. Use \`mcps add\` first.`));
29258
+ closeDb();
29259
+ process.exit(1);
29260
+ }
29261
+ }
29262
+ console.log(chalk2.dim(`Installing "${server2.name}" to: ${targets.join(", ")}...`));
29263
+ const results = installToAgents(server2, targets);
29264
+ for (const r of results) {
29265
+ if (r.success) {
29266
+ console.log(chalk2.green(` \u2713 ${r.agent}`));
29267
+ } else {
29268
+ console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
29269
+ }
29270
+ }
29271
+ closeDb();
29272
+ });
29273
+ 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) => {
29274
+ const servers = listServers();
29275
+ const sources = listSources();
29276
+ const payload = {
29277
+ version: 1,
29278
+ exported_at: new Date().toISOString(),
29279
+ servers,
29280
+ sources
29281
+ };
29282
+ const json2 = JSON.stringify(payload, null, 2);
29283
+ if (opts.stdout) {
29284
+ console.log(json2);
29285
+ } else {
29286
+ writeFileSync3(opts.file, json2, "utf-8");
29287
+ console.log(chalk2.green(`Exported ${servers.length} server(s) and ${sources.length} source(s) to ${opts.file}`));
29288
+ }
29289
+ closeDb();
29290
+ });
29291
+ 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) => {
29292
+ let payload;
29293
+ try {
29294
+ payload = JSON.parse(readFileSync4(file, "utf-8"));
29295
+ } catch (err) {
29296
+ console.error(chalk2.red(`Failed to read file: ${err.message}`));
29297
+ closeDb();
29298
+ process.exit(1);
29299
+ }
29300
+ if (!Array.isArray(payload.servers) || !Array.isArray(payload.sources)) {
29301
+ console.error(chalk2.red("Invalid export file format."));
29302
+ closeDb();
29303
+ process.exit(1);
29304
+ }
29305
+ const db2 = getDb();
29306
+ const overwrite = opts.overwrite;
29307
+ const orReplace = overwrite ? "OR REPLACE" : "OR IGNORE";
29308
+ let serversImported = 0;
29309
+ let serversSkipped = 0;
29310
+ for (const s of payload.servers) {
29311
+ const existing = getServer(s.id);
29312
+ if (existing && !overwrite) {
29313
+ serversSkipped++;
29314
+ continue;
29315
+ }
29316
+ 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]);
29317
+ serversImported++;
29318
+ }
29319
+ let sourcesImported = 0;
29320
+ let sourcesSkipped = 0;
29321
+ for (const s of payload.sources) {
29322
+ const existing = getSource(s.id);
29323
+ if (existing && !overwrite) {
29324
+ sourcesSkipped++;
29325
+ continue;
29326
+ }
29327
+ 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]);
29328
+ sourcesImported++;
29329
+ }
29330
+ console.log(chalk2.green(`Servers: ${serversImported} imported, ${serversSkipped} skipped.`));
29331
+ console.log(chalk2.green(`Sources: ${sourcesImported} imported, ${sourcesSkipped} skipped.`));
29332
+ closeDb();
29333
+ });
29334
+ var envCmd = program2.command("env").description("Manage server environment variables");
29335
+ envCmd.command("list").argument("<id>").description("List env vars for a server").action((id) => {
29336
+ const server2 = getServer(id);
29337
+ if (!server2) {
29338
+ console.error(chalk2.red(`Server "${id}" not found.`));
29339
+ closeDb();
29340
+ process.exit(1);
29341
+ }
29342
+ const entries = Object.entries(server2.env);
29343
+ if (entries.length === 0) {
29344
+ console.log(chalk2.dim("No env vars set."));
29345
+ closeDb();
29346
+ return;
29347
+ }
29348
+ for (const [k, v] of entries)
29349
+ console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
29350
+ closeDb();
29351
+ });
29352
+ envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
29353
+ const eqIdx = pair.indexOf("=");
29354
+ if (eqIdx === -1) {
29355
+ console.error(chalk2.red("Format: KEY=VALUE"));
29356
+ closeDb();
29357
+ process.exit(1);
29358
+ }
29359
+ const key = pair.slice(0, eqIdx);
29360
+ const value = pair.slice(eqIdx + 1);
29361
+ try {
29362
+ setServerEnv(id, key, value);
29363
+ console.log(chalk2.green(`Set ${key} on ${id}`));
29364
+ } catch (err) {
29365
+ console.error(chalk2.red(err.message));
29366
+ closeDb();
29367
+ process.exit(1);
29368
+ }
29369
+ closeDb();
29370
+ });
29371
+ envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
29372
+ try {
29373
+ unsetServerEnv(id, key);
29374
+ console.log(chalk2.green(`Unset ${key} on ${id}`));
29375
+ } catch (err) {
29376
+ console.error(chalk2.red(err.message));
29377
+ closeDb();
29378
+ process.exit(1);
29379
+ }
29380
+ closeDb();
27843
29381
  });
27844
29382
  program2.command("mcp").description("Start meta-MCP server (stdio)").action(async () => {
27845
29383
  await startMcpServer();