@hasna/mcps 0.0.2 → 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/dist/index.js CHANGED
@@ -25,8 +25,97 @@ var __export = (target, all) => {
25
25
  set: (newValue) => all[name] = () => newValue
26
26
  });
27
27
  };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
29
  var __require = import.meta.require;
29
30
 
31
+ // src/lib/config.ts
32
+ import { join } from "path";
33
+ import { homedir } from "os";
34
+ var MCPS_DIR, DB_PATH, REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers", TOOL_PREFIX_SEPARATOR = "__";
35
+ var init_config = __esm(() => {
36
+ MCPS_DIR = join(homedir(), ".mcps");
37
+ DB_PATH = join(MCPS_DIR, "registry.db");
38
+ });
39
+
40
+ // src/lib/db.ts
41
+ import { Database } from "bun:sqlite";
42
+ import { mkdirSync } from "fs";
43
+ function getDb() {
44
+ if (db)
45
+ return db;
46
+ mkdirSync(MCPS_DIR, { recursive: true });
47
+ db = new Database(DB_PATH, { create: true });
48
+ db.exec("PRAGMA journal_mode = WAL");
49
+ db.exec("PRAGMA busy_timeout = 5000");
50
+ db.exec("PRAGMA foreign_keys = ON");
51
+ db.exec(`
52
+ CREATE TABLE IF NOT EXISTS servers (
53
+ id TEXT PRIMARY KEY,
54
+ name TEXT NOT NULL,
55
+ description TEXT,
56
+ command TEXT NOT NULL,
57
+ args TEXT NOT NULL DEFAULT '[]',
58
+ env TEXT NOT NULL DEFAULT '{}',
59
+ transport TEXT NOT NULL DEFAULT 'stdio',
60
+ url TEXT,
61
+ source TEXT NOT NULL DEFAULT 'local',
62
+ enabled INTEGER NOT NULL DEFAULT 1,
63
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
64
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
65
+ )
66
+ `);
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS tool_cache (
69
+ server_id TEXT NOT NULL,
70
+ name TEXT NOT NULL,
71
+ description TEXT NOT NULL DEFAULT '',
72
+ input_schema TEXT NOT NULL DEFAULT '{}',
73
+ cached_at TEXT NOT NULL DEFAULT (datetime('now')),
74
+ PRIMARY KEY (server_id, name),
75
+ FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
76
+ )
77
+ `);
78
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
79
+ try {
80
+ db.exec("ALTER TABLE servers ADD COLUMN last_connected_at TEXT");
81
+ } catch {}
82
+ try {
83
+ db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
84
+ } catch {}
85
+ db.exec(`
86
+ CREATE TABLE IF NOT EXISTS sources (
87
+ id TEXT PRIMARY KEY,
88
+ name TEXT NOT NULL,
89
+ type TEXT NOT NULL,
90
+ url TEXT NOT NULL,
91
+ description TEXT,
92
+ enabled INTEGER NOT NULL DEFAULT 1,
93
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
94
+ )
95
+ `);
96
+ const count = db.query("SELECT COUNT(*) as c FROM sources").get().c;
97
+ if (count === 0) {
98
+ db.exec(`
99
+ INSERT OR IGNORE INTO sources (id, name, type, url, description) VALUES
100
+ ('official-registry', 'Official MCP Registry', 'mcp-registry', 'https://registry.modelcontextprotocol.io/v0/servers', 'The official Model Context Protocol server registry'),
101
+ ('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'),
102
+ ('npm-mcp', 'npm MCP Packages', 'npm-search', 'https://registry.npmjs.org/-/v1/search', 'Search npm packages for MCP servers'),
103
+ ('github-mcp-topic', 'GitHub MCP Topic', 'github-topic', 'https://api.github.com/search/repositories', 'GitHub repositories tagged with mcp-server topic')
104
+ `);
105
+ }
106
+ return db;
107
+ }
108
+ function closeDb() {
109
+ if (db) {
110
+ db.close();
111
+ db = null;
112
+ }
113
+ }
114
+ var db = null;
115
+ var init_db = __esm(() => {
116
+ init_config();
117
+ });
118
+
30
119
  // node_modules/ajv/dist/compile/codegen/code.js
31
120
  var require_code = __commonJS((exports) => {
32
121
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -6286,7 +6375,7 @@ var require_formats = __commonJS((exports) => {
6286
6375
  }
6287
6376
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6288
6377
  function getTime(strictTimeZone) {
6289
- return function time3(str) {
6378
+ return function time(str) {
6290
6379
  const matches = TIME.exec(str);
6291
6380
  if (!matches)
6292
6381
  return false;
@@ -6955,92 +7044,381 @@ var require_cross_spawn = __commonJS((exports, module) => {
6955
7044
  module.exports._enoent = enoent;
6956
7045
  });
6957
7046
 
6958
- // src/lib/db.ts
6959
- import { Database } from "bun:sqlite";
6960
- import { mkdirSync } from "fs";
6961
-
6962
- // src/lib/config.ts
6963
- import { join } from "path";
6964
- import { homedir } from "os";
6965
- var MCPS_DIR = join(homedir(), ".mcps");
6966
- var DB_PATH = join(MCPS_DIR, "registry.db");
6967
- var REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers";
6968
- var TOOL_PREFIX_SEPARATOR = "__";
6969
-
6970
- // src/lib/db.ts
6971
- var db = null;
6972
- function getDb() {
6973
- if (db)
6974
- return db;
6975
- mkdirSync(MCPS_DIR, { recursive: true });
6976
- db = new Database(DB_PATH, { create: true });
6977
- db.exec("PRAGMA journal_mode = WAL");
6978
- db.exec("PRAGMA busy_timeout = 5000");
6979
- db.exec("PRAGMA foreign_keys = ON");
6980
- db.exec(`
6981
- CREATE TABLE IF NOT EXISTS servers (
6982
- id TEXT PRIMARY KEY,
6983
- name TEXT NOT NULL,
6984
- description TEXT,
6985
- command TEXT NOT NULL,
6986
- args TEXT NOT NULL DEFAULT '[]',
6987
- env TEXT NOT NULL DEFAULT '{}',
6988
- transport TEXT NOT NULL DEFAULT 'stdio',
6989
- url TEXT,
6990
- source TEXT NOT NULL DEFAULT 'local',
6991
- enabled INTEGER NOT NULL DEFAULT 1,
6992
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
6993
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
6994
- )
6995
- `);
6996
- db.exec(`
6997
- CREATE TABLE IF NOT EXISTS tool_cache (
6998
- server_id TEXT NOT NULL,
6999
- name TEXT NOT NULL,
7000
- description TEXT NOT NULL DEFAULT '',
7001
- input_schema TEXT NOT NULL DEFAULT '{}',
7002
- cached_at TEXT NOT NULL DEFAULT (datetime('now')),
7003
- PRIMARY KEY (server_id, name),
7004
- FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
7005
- )
7006
- `);
7007
- db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
7008
- return db;
7047
+ // src/lib/sources.ts
7048
+ var exports_sources = {};
7049
+ __export(exports_sources, {
7050
+ searchSource: () => searchSource,
7051
+ removeSource: () => removeSource,
7052
+ listSources: () => listSources,
7053
+ getSource: () => getSource,
7054
+ findServers: () => findServers,
7055
+ enableSource: () => enableSource,
7056
+ disableSource: () => disableSource,
7057
+ clearCache: () => clearCache,
7058
+ addSource: () => addSource
7059
+ });
7060
+ import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
7061
+ import { join as join2 } from "path";
7062
+ function getCacheFile(sourceId) {
7063
+ return join2(CACHE_DIR, `${sourceId}.json`);
7064
+ }
7065
+ function readCache(sourceId) {
7066
+ try {
7067
+ const file = getCacheFile(sourceId);
7068
+ if (!existsSync(file))
7069
+ return null;
7070
+ const data = JSON.parse(readFileSync(file, "utf-8"));
7071
+ return data;
7072
+ } catch {
7073
+ return null;
7074
+ }
7009
7075
  }
7010
- function closeDb() {
7011
- if (db) {
7012
- db.close();
7013
- db = null;
7076
+ function writeCache(sourceId, results) {
7077
+ try {
7078
+ mkdirSync2(CACHE_DIR, { recursive: true });
7079
+ writeFileSync(getCacheFile(sourceId), JSON.stringify({ results, cachedAt: Date.now() }), "utf-8");
7080
+ } catch {}
7081
+ }
7082
+ function clearCache(sourceId) {
7083
+ try {
7084
+ if (!existsSync(CACHE_DIR))
7085
+ return;
7086
+ const files = readdirSync(CACHE_DIR);
7087
+ for (const file of files) {
7088
+ if (!file.endsWith(".json"))
7089
+ continue;
7090
+ if (!sourceId || file.startsWith(`${sourceId}.`)) {
7091
+ try {
7092
+ unlinkSync(join2(CACHE_DIR, file));
7093
+ } catch {}
7094
+ }
7095
+ }
7096
+ } catch {}
7097
+ }
7098
+ function generateId2(name) {
7099
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7100
+ }
7101
+ function listSources() {
7102
+ const db2 = getDb();
7103
+ const rows = db2.query("SELECT * FROM sources ORDER BY created_at ASC").all();
7104
+ return rows.map((r) => ({ ...r, enabled: r.enabled === 1 }));
7105
+ }
7106
+ function getSource(id) {
7107
+ const db2 = getDb();
7108
+ const row = db2.query("SELECT * FROM sources WHERE id = ?").get(id);
7109
+ if (!row)
7110
+ return null;
7111
+ return { ...row, enabled: row.enabled === 1 };
7112
+ }
7113
+ function addSource(opts) {
7114
+ const db2 = getDb();
7115
+ const id = generateId2(opts.name);
7116
+ db2.run("INSERT INTO sources (id, name, type, url, description) VALUES (?, ?, ?, ?, ?)", [id, opts.name, opts.type, opts.url, opts.description ?? null]);
7117
+ return getSource(id);
7118
+ }
7119
+ function removeSource(id) {
7120
+ const db2 = getDb();
7121
+ db2.run("DELETE FROM sources WHERE id = ?", [id]);
7122
+ }
7123
+ function enableSource(id) {
7124
+ const db2 = getDb();
7125
+ db2.run("UPDATE sources SET enabled = 1 WHERE id = ?", [id]);
7126
+ }
7127
+ function disableSource(id) {
7128
+ const db2 = getDb();
7129
+ db2.run("UPDATE sources SET enabled = 0 WHERE id = ?", [id]);
7130
+ }
7131
+ async function searchMcpRegistry(source, query) {
7132
+ try {
7133
+ const res = await fetch(source.url);
7134
+ if (!res.ok)
7135
+ return [];
7136
+ const data = await res.json();
7137
+ const q = query.toLowerCase();
7138
+ return (data.servers || []).map((e) => e.server).filter((s) => q === "" || s.name.toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q)).map((s) => ({
7139
+ name: s.name,
7140
+ description: s.description || "",
7141
+ source: "registry",
7142
+ sourceId: source.id,
7143
+ url: s.repository?.url,
7144
+ githubRepo: s.repository?.url,
7145
+ installCmd: s.packages?.[0] ? s.packages[0].registryType === "npm" ? `npx -y ${s.packages[0].identifier}` : s.packages[0].identifier : undefined,
7146
+ npmPackage: s.packages?.find((p) => p.registryType === "npm")?.identifier
7147
+ }));
7148
+ } catch {
7149
+ return [];
7150
+ }
7151
+ }
7152
+ async function searchAwesomeList(source, query) {
7153
+ try {
7154
+ const res = await fetch(source.url);
7155
+ if (!res.ok)
7156
+ return [];
7157
+ const text = await res.text();
7158
+ const results = [];
7159
+ const linkPattern = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)(?:\s*[-\u2013]\s*([^\n]+))?/g;
7160
+ const q = query.toLowerCase();
7161
+ let match;
7162
+ while ((match = linkPattern.exec(text)) !== null) {
7163
+ const name = match[1].trim();
7164
+ const url2 = match[2].trim();
7165
+ const description = match[3]?.trim() || "";
7166
+ if ((url2.includes("github.com") || url2.includes("npmjs.com")) && name.length > 2) {
7167
+ if (q === "" || `${name} ${description}`.toLowerCase().includes(q)) {
7168
+ results.push({
7169
+ name,
7170
+ description,
7171
+ source: "awesome",
7172
+ sourceId: source.id,
7173
+ url: url2,
7174
+ githubRepo: url2.includes("github.com") ? url2 : undefined,
7175
+ installCmd: url2.includes("npmjs.com") ? `npx -y ${url2.split("/").pop()}` : undefined
7176
+ });
7177
+ }
7178
+ }
7179
+ }
7180
+ return results;
7181
+ } catch {
7182
+ return [];
7183
+ }
7184
+ }
7185
+ function scoreNpmPackage(pkg) {
7186
+ const name = pkg.name.toLowerCase();
7187
+ const keywords = (pkg.keywords || []).map((k) => k.toLowerCase());
7188
+ let score = 0;
7189
+ if (name.includes("mcp-server"))
7190
+ score += 3;
7191
+ else if (name.startsWith("mcp-") || name.endsWith("-mcp"))
7192
+ score += 2;
7193
+ else if (name.includes("mcp"))
7194
+ score += 1;
7195
+ if (keywords.includes("mcp"))
7196
+ score += 1;
7197
+ if (keywords.includes("mcp-server"))
7198
+ score += 2;
7199
+ if (keywords.some((k) => k.includes("modelcontextprotocol")))
7200
+ score += 2;
7201
+ return score;
7202
+ }
7203
+ async function searchNpm(source, query) {
7204
+ try {
7205
+ const url2 = new URL(source.url);
7206
+ url2.searchParams.set("text", `mcp-server ${query}`);
7207
+ url2.searchParams.set("size", "50");
7208
+ const res = await fetch(url2.toString());
7209
+ if (!res.ok)
7210
+ return [];
7211
+ const data = await res.json();
7212
+ const q = query.toLowerCase();
7213
+ const scored = (data.objects || []).map((o) => ({ pkg: o.package, score: scoreNpmPackage(o.package) })).filter(({ pkg, score }) => {
7214
+ if (score < 1)
7215
+ return false;
7216
+ const text = `${pkg.name} ${pkg.description || ""} ${(pkg.keywords || []).join(" ")}`.toLowerCase();
7217
+ return q === "" || text.includes(q);
7218
+ });
7219
+ scored.sort((a, b) => b.score - a.score);
7220
+ return scored.map(({ pkg }) => ({
7221
+ name: pkg.name,
7222
+ description: pkg.description || "",
7223
+ source: "npm",
7224
+ sourceId: source.id,
7225
+ url: pkg.links?.repository || pkg.links?.npm,
7226
+ npmPackage: pkg.name,
7227
+ installCmd: `npx -y ${pkg.name}`
7228
+ }));
7229
+ } catch {
7230
+ return [];
7231
+ }
7232
+ }
7233
+ async function searchGitHubTopic(source, query) {
7234
+ const token = process.env.GITHUB_TOKEN;
7235
+ try {
7236
+ const url2 = new URL(source.url);
7237
+ const q = query ? `${query} topic:mcp-server` : "topic:mcp-server";
7238
+ url2.searchParams.set("q", q);
7239
+ url2.searchParams.set("sort", "stars");
7240
+ url2.searchParams.set("per_page", "30");
7241
+ const headers = {
7242
+ Accept: "application/vnd.github.v3+json"
7243
+ };
7244
+ if (token)
7245
+ headers["Authorization"] = `Bearer ${token}`;
7246
+ const res = await fetch(url2.toString(), { headers });
7247
+ if (!res.ok)
7248
+ return [];
7249
+ const data = await res.json();
7250
+ return (data.items || []).map((repo) => ({
7251
+ name: repo.full_name,
7252
+ description: repo.description || "",
7253
+ source: "github",
7254
+ sourceId: source.id,
7255
+ url: repo.html_url,
7256
+ githubRepo: repo.html_url,
7257
+ stars: repo.stargazers_count
7258
+ }));
7259
+ } catch {
7260
+ return [];
7261
+ }
7262
+ }
7263
+ async function fetchSource(source) {
7264
+ switch (source.type) {
7265
+ case "mcp-registry":
7266
+ return searchMcpRegistry(source, "");
7267
+ case "awesome-list":
7268
+ return searchAwesomeList(source, "");
7269
+ case "npm-search":
7270
+ return searchNpm(source, "");
7271
+ case "github-topic":
7272
+ return searchGitHubTopic(source, "");
7273
+ default:
7274
+ return [];
7014
7275
  }
7015
7276
  }
7277
+ function filterResults(results, query) {
7278
+ if (!query)
7279
+ return results;
7280
+ const q = query.toLowerCase();
7281
+ return results.filter((r) => {
7282
+ const text = `${r.name} ${r.description || ""}`.toLowerCase();
7283
+ return text.includes(q);
7284
+ });
7285
+ }
7286
+ async function searchSource(source, query, noCache = false) {
7287
+ if (!noCache) {
7288
+ const cached2 = readCache(source.id);
7289
+ if (cached2 && Date.now() - cached2.cachedAt < DEFAULT_TTL_MS) {
7290
+ return filterResults(cached2.results, query);
7291
+ }
7292
+ }
7293
+ const results = await fetchSource(source);
7294
+ writeCache(source.id, results);
7295
+ return filterResults(results, query);
7296
+ }
7297
+ function deduplicate(results) {
7298
+ const seen = new Set;
7299
+ return results.filter((r) => {
7300
+ const key = r.npmPackage || r.githubRepo || r.name.toLowerCase();
7301
+ if (seen.has(key))
7302
+ return false;
7303
+ seen.add(key);
7304
+ return true;
7305
+ });
7306
+ }
7307
+ async function findServers(query, opts = {}) {
7308
+ let sources = listSources().filter((s) => s.enabled);
7309
+ if (opts.sources && opts.sources.length > 0) {
7310
+ sources = sources.filter((s) => opts.sources.includes(s.id));
7311
+ }
7312
+ const limit = opts.limit ?? 20;
7313
+ const all = await Promise.all(sources.map((s) => searchSource(s, query, opts.noCache)));
7314
+ const flat = all.flat();
7315
+ const deduped = deduplicate(flat);
7316
+ const typeOrder = {
7317
+ "mcp-registry": 0,
7318
+ "awesome-list": 1,
7319
+ "npm-search": 2,
7320
+ "github-topic": 3
7321
+ };
7322
+ const sourceTypeMap = new Map(sources.map((s) => [s.id, s.type]));
7323
+ deduped.sort((a, b) => {
7324
+ const aType = a.sourceId ? sourceTypeMap.get(a.sourceId) ?? "" : a.source;
7325
+ const bType = b.sourceId ? sourceTypeMap.get(b.sourceId) ?? "" : b.source;
7326
+ const ao = typeOrder[aType] ?? 99;
7327
+ const bo = typeOrder[bType] ?? 99;
7328
+ if (ao !== bo)
7329
+ return ao - bo;
7330
+ return (b.stars ?? 0) - (a.stars ?? 0);
7331
+ });
7332
+ return deduped.slice(0, limit * Math.max(sources.length, 1));
7333
+ }
7334
+ var CACHE_DIR, DEFAULT_TTL_MS;
7335
+ var init_sources = __esm(() => {
7336
+ init_db();
7337
+ init_config();
7338
+ CACHE_DIR = join2(MCPS_DIR, "cache");
7339
+ DEFAULT_TTL_MS = 10 * 60 * 1000;
7340
+ });
7016
7341
 
7017
7342
  // src/lib/registry.ts
7343
+ init_db();
7018
7344
  function parseRow(row) {
7019
7345
  return {
7020
7346
  id: row.id,
7021
7347
  name: row.name,
7022
7348
  description: row.description || null,
7023
7349
  command: row.command,
7024
- args: JSON.parse(row.args),
7025
- env: JSON.parse(row.env),
7350
+ args: safeJsonParse(row.args, []),
7351
+ env: safeJsonParse(row.env, {}),
7026
7352
  transport: row.transport,
7027
7353
  url: row.url || null,
7028
7354
  source: row.source,
7029
7355
  enabled: row.enabled === 1,
7030
7356
  created_at: row.created_at,
7031
- updated_at: row.updated_at
7357
+ updated_at: row.updated_at,
7358
+ last_connected_at: row.last_connected_at ?? null,
7359
+ last_error: row.last_error ?? null
7032
7360
  };
7033
7361
  }
7362
+ function safeJsonParse(value, fallback) {
7363
+ if (typeof value !== "string")
7364
+ return fallback;
7365
+ try {
7366
+ return JSON.parse(value);
7367
+ } catch {
7368
+ return fallback;
7369
+ }
7370
+ }
7034
7371
  function generateId(name) {
7035
7372
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7036
7373
  }
7374
+ function normalizeCandidate(value) {
7375
+ const trimmed = value?.trim();
7376
+ return trimmed ? trimmed : undefined;
7377
+ }
7378
+ function pickNameFromArgs(args) {
7379
+ if (!args || args.length === 0)
7380
+ return;
7381
+ const ddIndex = args.indexOf("--");
7382
+ if (ddIndex >= 0 && ddIndex < args.length - 1) {
7383
+ const after = normalizeCandidate(args[ddIndex + 1]);
7384
+ if (after)
7385
+ return after;
7386
+ }
7387
+ for (const arg of args) {
7388
+ const candidate = normalizeCandidate(arg);
7389
+ if (!candidate)
7390
+ continue;
7391
+ if (candidate.startsWith("-"))
7392
+ continue;
7393
+ return candidate;
7394
+ }
7395
+ return;
7396
+ }
7397
+ function pickId(candidates) {
7398
+ for (const candidate of candidates) {
7399
+ if (!candidate)
7400
+ continue;
7401
+ const id = generateId(candidate);
7402
+ if (id)
7403
+ return id;
7404
+ }
7405
+ return null;
7406
+ }
7037
7407
  function addServer(opts) {
7038
7408
  const db2 = getDb();
7039
- const name = opts.name || opts.args?.[0] || opts.command;
7040
- const id = generateId(name);
7409
+ const command = normalizeCandidate(opts.command);
7410
+ if (!command) {
7411
+ throw new Error("Command is required");
7412
+ }
7413
+ const argName = pickNameFromArgs(opts.args);
7414
+ const name = normalizeCandidate(opts.name) || argName || command;
7415
+ const id = pickId([normalizeCandidate(opts.name), argName, command]) || null;
7416
+ if (!id) {
7417
+ throw new Error("Unable to generate a valid server ID");
7418
+ }
7041
7419
  const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
7042
7420
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7043
- 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");
7421
+ 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");
7044
7422
  return parseRow(row);
7045
7423
  }
7046
7424
  function removeServer(id) {
@@ -7096,6 +7474,9 @@ function updateServer(id, updates) {
7096
7474
  sets.push("updated_at = datetime('now')");
7097
7475
  values.push(id);
7098
7476
  const row = db2.prepare(`UPDATE servers SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...values);
7477
+ if (!row) {
7478
+ throw new Error(`Server "${id}" not found`);
7479
+ }
7099
7480
  return parseRow(row);
7100
7481
  }
7101
7482
  function enableServer(id) {
@@ -7104,74 +7485,55 @@ function enableServer(id) {
7104
7485
  function disableServer(id) {
7105
7486
  return updateServer(id, { enabled: false });
7106
7487
  }
7488
+ function setServerEnv(id, key, value) {
7489
+ const db2 = getDb();
7490
+ const server = getServer(id);
7491
+ if (!server)
7492
+ throw new Error(`Server "${id}" not found`);
7493
+ const env = { ...server.env, [key]: value };
7494
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
7495
+ }
7496
+ function unsetServerEnv(id, key) {
7497
+ const db2 = getDb();
7498
+ const server = getServer(id);
7499
+ if (!server)
7500
+ throw new Error(`Server "${id}" not found`);
7501
+ const env = { ...server.env };
7502
+ delete env[key];
7503
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
7504
+ }
7107
7505
  function cacheTools(serverId, tools) {
7108
7506
  const db2 = getDb();
7109
- db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
7110
7507
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
7508
+ const uniqueTools = [];
7509
+ const seen = new Set;
7111
7510
  for (const tool of tools) {
7112
- insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
7113
- }
7114
- }
7115
- // src/lib/remote.ts
7116
- function parseRegistryEntry(entry) {
7117
- const s = entry.server;
7118
- return {
7119
- id: s.name,
7120
- name: s.name,
7121
- description: s.description || "",
7122
- repository: s.repository,
7123
- packages: s.packages || []
7124
- };
7125
- }
7126
- async function searchRegistry(query) {
7127
- const res = await fetch(REGISTRY_API_URL);
7128
- if (!res.ok) {
7129
- throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
7130
- }
7131
- const data = await res.json();
7132
- const entries = data.servers || [];
7133
- const q = query.toLowerCase();
7134
- return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
7135
- }
7136
- async function getRegistryServer(id) {
7137
- const res = await fetch(REGISTRY_API_URL);
7138
- if (!res.ok) {
7139
- throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
7140
- }
7141
- const data = await res.json();
7142
- const entries = data.servers || [];
7143
- const all = entries.map(parseRegistryEntry);
7144
- return all.find((s) => s.id === id) || null;
7145
- }
7146
- async function installFromRegistry(id) {
7147
- const server = await getRegistryServer(id);
7148
- if (!server) {
7149
- throw new Error(`Server "${id}" not found in registry`);
7511
+ const name = tool.name?.trim();
7512
+ if (!name || seen.has(name))
7513
+ continue;
7514
+ seen.add(name);
7515
+ uniqueTools.push({
7516
+ name,
7517
+ description: tool.description || "",
7518
+ input_schema: tool.input_schema || {}
7519
+ });
7150
7520
  }
7151
- const pkg = server.packages?.[0];
7152
- let command = "npx";
7153
- let args = [];
7154
- let transport = "stdio";
7155
- if (pkg) {
7156
- if (pkg.registryType === "npm") {
7157
- command = "npx";
7158
- args = ["-y", pkg.identifier];
7159
- } else {
7160
- command = pkg.identifier;
7161
- }
7162
- if (pkg.transport?.type) {
7163
- transport = pkg.transport.type;
7521
+ const run = db2.transaction((rows) => {
7522
+ db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
7523
+ for (const tool of rows) {
7524
+ insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
7164
7525
  }
7165
- }
7166
- return addServer({
7167
- name: server.name,
7168
- description: server.description,
7169
- command,
7170
- args,
7171
- transport,
7172
- source: "registry"
7173
7526
  });
7527
+ run(uniqueTools);
7174
7528
  }
7529
+ function getToolCounts() {
7530
+ const db2 = getDb();
7531
+ const rows = db2.prepare("SELECT server_id, COUNT(*) as count FROM tool_cache GROUP BY server_id").all();
7532
+ return new Map(rows.map((row) => [row.server_id, Number(row.count)]));
7533
+ }
7534
+ // src/lib/doctor.ts
7535
+ import { execFileSync } from "child_process";
7536
+
7175
7537
  // node_modules/zod/v4/core/core.js
7176
7538
  var NEVER = Object.freeze({
7177
7539
  status: "aborted"
@@ -15284,54 +15646,126 @@ class StreamableHTTPClientTransport {
15284
15646
  }
15285
15647
 
15286
15648
  // src/lib/proxy.ts
15649
+ init_config();
15650
+ init_db();
15287
15651
  var connections = new Map;
15652
+ var inflightConnections = new Map;
15653
+ function buildEnv(extra) {
15654
+ const merged = {};
15655
+ for (const [key, value] of Object.entries(process.env)) {
15656
+ if (typeof value === "string")
15657
+ merged[key] = value;
15658
+ }
15659
+ for (const [key, value] of Object.entries(extra || {})) {
15660
+ if (value === undefined || value === null)
15661
+ continue;
15662
+ merged[key] = String(value);
15663
+ }
15664
+ return merged;
15665
+ }
15666
+ function requireUrl(entry) {
15667
+ if (!entry.url) {
15668
+ throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
15669
+ }
15670
+ try {
15671
+ return new URL(entry.url);
15672
+ } catch {
15673
+ throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
15674
+ }
15675
+ }
15288
15676
  async function connectToServer(entry) {
15289
15677
  if (connections.has(entry.id)) {
15290
15678
  return connections.get(entry.id);
15291
15679
  }
15680
+ const inflight = inflightConnections.get(entry.id);
15681
+ if (inflight) {
15682
+ return inflight;
15683
+ }
15292
15684
  const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
15293
15685
  let transport;
15294
- if (entry.transport === "stdio") {
15295
- transport = new StdioClientTransport({
15296
- command: entry.command,
15297
- args: entry.args,
15298
- env: { ...process.env, ...entry.env }
15299
- });
15300
- } else if (entry.transport === "sse") {
15301
- transport = new SSEClientTransport(new URL(entry.url));
15302
- } else {
15303
- transport = new StreamableHTTPClientTransport(new URL(entry.url));
15304
- }
15305
- await client.connect(transport);
15306
- const result = await client.listTools();
15307
- const tools = (result.tools || []).map((t) => ({
15308
- server_id: entry.id,
15309
- name: t.name,
15310
- description: t.description || "",
15311
- input_schema: t.inputSchema || {}
15312
- }));
15313
- cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
15314
- const connected = {
15315
- entry,
15316
- tools,
15317
- disconnect: async () => {
15318
- await client.close();
15319
- connections.delete(entry.id);
15686
+ const connectPromise = (async () => {
15687
+ try {
15688
+ if (entry.transport === "stdio") {
15689
+ if (!entry.command?.trim()) {
15690
+ throw new Error(`Server "${entry.id}" is missing a command`);
15691
+ }
15692
+ transport = new StdioClientTransport({
15693
+ command: entry.command,
15694
+ args: entry.args,
15695
+ env: buildEnv(entry.env)
15696
+ });
15697
+ } else if (entry.transport === "sse") {
15698
+ transport = new SSEClientTransport(requireUrl(entry));
15699
+ } else {
15700
+ transport = new StreamableHTTPClientTransport(requireUrl(entry));
15701
+ }
15702
+ await client.connect(transport);
15703
+ const result = await client.listTools();
15704
+ const tools = (result.tools || []).map((t) => ({
15705
+ server_id: entry.id,
15706
+ name: t.name,
15707
+ description: t.description || "",
15708
+ input_schema: t.inputSchema || {}
15709
+ }));
15710
+ cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
15711
+ const connected = {
15712
+ entry,
15713
+ tools,
15714
+ disconnect: async () => {
15715
+ try {
15716
+ await client.close();
15717
+ } finally {
15718
+ connections.delete(entry.id);
15719
+ }
15720
+ }
15721
+ };
15722
+ connected._client = client;
15723
+ connections.set(entry.id, connected);
15724
+ try {
15725
+ getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
15726
+ } catch {}
15727
+ return connected;
15728
+ } catch (err) {
15729
+ try {
15730
+ getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
15731
+ } catch {}
15732
+ try {
15733
+ await client.close();
15734
+ } catch {}
15735
+ throw err;
15320
15736
  }
15321
- };
15322
- connected._client = client;
15323
- connections.set(entry.id, connected);
15324
- return connected;
15737
+ })();
15738
+ inflightConnections.set(entry.id, connectPromise);
15739
+ try {
15740
+ return await connectPromise;
15741
+ } finally {
15742
+ inflightConnections.delete(entry.id);
15743
+ }
15325
15744
  }
15326
15745
  async function disconnectServer(id) {
15327
15746
  const conn = connections.get(id);
15328
15747
  if (conn) {
15329
15748
  await conn.disconnect();
15749
+ return;
15750
+ }
15751
+ const inflight = inflightConnections.get(id);
15752
+ if (inflight) {
15753
+ try {
15754
+ const pending = await inflight;
15755
+ await pending.disconnect();
15756
+ } catch {}
15330
15757
  }
15331
15758
  }
15332
15759
  async function disconnectAll() {
15333
15760
  const ids = Array.from(connections.keys());
15334
- await Promise.all(ids.map((id) => disconnectServer(id)));
15761
+ await Promise.allSettled(ids.map((id) => disconnectServer(id)));
15762
+ const inflight = Array.from(inflightConnections.values());
15763
+ await Promise.allSettled(inflight.map(async (promise2) => {
15764
+ try {
15765
+ const conn = await promise2;
15766
+ await conn.disconnect();
15767
+ } catch {}
15768
+ }));
15335
15769
  }
15336
15770
  function listAllTools() {
15337
15771
  const tools = [];
@@ -15346,12 +15780,19 @@ function listAllTools() {
15346
15780
  return tools;
15347
15781
  }
15348
15782
  async function callTool(prefixedName, args) {
15349
- const sepIdx = prefixedName.indexOf(TOOL_PREFIX_SEPARATOR);
15783
+ const normalized = prefixedName.trim();
15784
+ if (!normalized) {
15785
+ throw new Error("Tool name is required");
15786
+ }
15787
+ const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
15350
15788
  if (sepIdx === -1) {
15351
15789
  throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
15352
15790
  }
15353
- const serverId = prefixedName.slice(0, sepIdx);
15354
- const toolName = prefixedName.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length);
15791
+ const serverId = normalized.slice(0, sepIdx).trim();
15792
+ const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
15793
+ if (!serverId || !toolName) {
15794
+ throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
15795
+ }
15355
15796
  const conn = connections.get(serverId);
15356
15797
  if (!conn) {
15357
15798
  throw new Error(`Server "${serverId}" is not connected`);
@@ -15381,23 +15822,243 @@ async function refreshTools(id) {
15381
15822
  cacheTools(id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
15382
15823
  return tools;
15383
15824
  }
15825
+
15826
+ // src/lib/doctor.ts
15827
+ async function diagnoseServer(server) {
15828
+ const checks3 = [];
15829
+ if (server.transport === "stdio") {
15830
+ try {
15831
+ execFileSync("which", [server.command], { stdio: "pipe" });
15832
+ checks3.push({ name: "command on PATH", pass: true, message: `${server.command} found` });
15833
+ } catch {
15834
+ checks3.push({ name: "command on PATH", pass: false, message: `${server.command} not found on PATH` });
15835
+ }
15836
+ }
15837
+ const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
15838
+ if (Object.keys(server.env).length === 0) {
15839
+ checks3.push({ name: "env vars", pass: true, message: "no env vars required" });
15840
+ } else if (missingEnv.length > 0) {
15841
+ checks3.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
15842
+ } else {
15843
+ checks3.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
15844
+ }
15845
+ if (server.transport !== "stdio" && server.url) {
15846
+ try {
15847
+ const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
15848
+ checks3.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
15849
+ } catch (err) {
15850
+ checks3.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
15851
+ }
15852
+ }
15853
+ if (server.enabled) {
15854
+ try {
15855
+ await Promise.race([
15856
+ connectToServer(server),
15857
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
15858
+ ]);
15859
+ await disconnectServer(server.id);
15860
+ checks3.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
15861
+ } catch (err) {
15862
+ checks3.push({ name: "connect & list tools", pass: false, message: err.message });
15863
+ }
15864
+ } else {
15865
+ checks3.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
15866
+ }
15867
+ return {
15868
+ server,
15869
+ checks: checks3,
15870
+ healthy: checks3.every((c) => c.pass)
15871
+ };
15872
+ }
15873
+ // src/lib/remote.ts
15874
+ init_config();
15875
+ function parseRegistryEntry(entry) {
15876
+ const s = entry.server;
15877
+ return {
15878
+ id: s.name,
15879
+ name: s.name,
15880
+ description: s.description || "",
15881
+ repository: s.repository,
15882
+ packages: s.packages || []
15883
+ };
15884
+ }
15885
+ async function searchRegistry(query) {
15886
+ const res = await fetch(REGISTRY_API_URL);
15887
+ if (!res.ok) {
15888
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
15889
+ }
15890
+ const data = await res.json();
15891
+ const entries = data.servers || [];
15892
+ const q = query.toLowerCase();
15893
+ return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
15894
+ }
15895
+ async function getRegistryServer(id) {
15896
+ const res = await fetch(REGISTRY_API_URL);
15897
+ if (!res.ok) {
15898
+ throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
15899
+ }
15900
+ const data = await res.json();
15901
+ const entries = data.servers || [];
15902
+ const all = entries.map(parseRegistryEntry);
15903
+ return all.find((s) => s.id === id) || null;
15904
+ }
15905
+ async function installFromRegistry(id) {
15906
+ const server = await getRegistryServer(id);
15907
+ if (!server) {
15908
+ throw new Error(`Server "${id}" not found in registry`);
15909
+ }
15910
+ const pkg = server.packages?.[0];
15911
+ let command = "npx";
15912
+ let args = [];
15913
+ let transport = "stdio";
15914
+ if (pkg) {
15915
+ if (pkg.registryType === "npm") {
15916
+ command = "npx";
15917
+ args = ["-y", pkg.identifier];
15918
+ } else {
15919
+ command = pkg.identifier;
15920
+ }
15921
+ if (pkg.transport?.type) {
15922
+ transport = pkg.transport.type;
15923
+ }
15924
+ }
15925
+ return addServer({
15926
+ name: server.name,
15927
+ description: server.description,
15928
+ command,
15929
+ args,
15930
+ transport,
15931
+ source: "registry"
15932
+ });
15933
+ }
15934
+ // src/lib/finder.ts
15935
+ init_sources();
15936
+ async function listAwesomeServers() {
15937
+ const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
15938
+ const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
15939
+ if (!source)
15940
+ return [];
15941
+ return searchSource2(source, "");
15942
+ }
15943
+
15944
+ // src/index.ts
15945
+ init_sources();
15946
+
15947
+ // src/lib/install.ts
15948
+ import { execFileSync as execFileSync2 } from "child_process";
15949
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
15950
+ import { join as join3 } from "path";
15951
+ import { homedir as homedir2 } from "os";
15952
+ function installToClaude(entry) {
15953
+ try {
15954
+ const args = [
15955
+ "mcp",
15956
+ "add",
15957
+ "--transport",
15958
+ entry.transport,
15959
+ "--scope",
15960
+ "user"
15961
+ ];
15962
+ for (const [k, v] of Object.entries(entry.env)) {
15963
+ args.push("--env", `${k}=${v}`);
15964
+ }
15965
+ args.push(entry.id, "--", entry.command, ...entry.args);
15966
+ execFileSync2("claude", args, { stdio: "pipe" });
15967
+ return { agent: "claude", success: true };
15968
+ } catch (err) {
15969
+ return { agent: "claude", success: false, error: err.message };
15970
+ }
15971
+ }
15972
+ function installToCodex(entry) {
15973
+ try {
15974
+ const configDir = join3(homedir2(), ".codex");
15975
+ const configPath = join3(configDir, "config.toml");
15976
+ if (!existsSync2(configDir)) {
15977
+ mkdirSync3(configDir, { recursive: true });
15978
+ }
15979
+ const block = `
15980
+ [mcp_servers.${entry.id}]
15981
+ ` + `command = ${JSON.stringify(entry.command)}
15982
+ ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
15983
+ `;
15984
+ const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
15985
+ if (existing.includes(`[mcp_servers.${entry.id}]`)) {
15986
+ return { agent: "codex", success: true };
15987
+ }
15988
+ writeFileSync2(configPath, existing + block, "utf-8");
15989
+ return { agent: "codex", success: true };
15990
+ } catch (err) {
15991
+ return { agent: "codex", success: false, error: err.message };
15992
+ }
15993
+ }
15994
+ function installToGemini(entry) {
15995
+ try {
15996
+ const configDir = join3(homedir2(), ".gemini");
15997
+ const configPath = join3(configDir, "settings.json");
15998
+ if (!existsSync2(configDir)) {
15999
+ mkdirSync3(configDir, { recursive: true });
16000
+ }
16001
+ let settings = {};
16002
+ if (existsSync2(configPath)) {
16003
+ settings = JSON.parse(readFileSync2(configPath, "utf-8"));
16004
+ }
16005
+ if (!settings.mcpServers)
16006
+ settings.mcpServers = {};
16007
+ settings.mcpServers[entry.id] = {
16008
+ command: entry.command,
16009
+ args: entry.args,
16010
+ ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
16011
+ };
16012
+ writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
16013
+ return { agent: "gemini", success: true };
16014
+ } catch (err) {
16015
+ return { agent: "gemini", success: false, error: err.message };
16016
+ }
16017
+ }
16018
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
16019
+ return targets.map((target) => {
16020
+ if (target === "claude")
16021
+ return installToClaude(entry);
16022
+ if (target === "codex")
16023
+ return installToCodex(entry);
16024
+ if (target === "gemini")
16025
+ return installToGemini(entry);
16026
+ return { agent: target, success: false, error: "Unknown target" };
16027
+ });
16028
+ }
16029
+
16030
+ // src/index.ts
16031
+ init_db();
15384
16032
  export {
15385
16033
  updateServer,
16034
+ unsetServerEnv,
16035
+ setServerEnv,
15386
16036
  searchRegistry,
16037
+ removeSource,
15387
16038
  removeServer,
15388
16039
  refreshTools,
16040
+ listSources,
15389
16041
  listServers,
16042
+ listAwesomeServers,
15390
16043
  listAllTools,
16044
+ installToAgents,
15391
16045
  installFromRegistry,
16046
+ getToolCounts,
16047
+ getSource,
15392
16048
  getServer,
15393
16049
  getRegistryServer,
15394
16050
  getDb,
16051
+ findServers,
16052
+ enableSource,
15395
16053
  enableServer,
15396
16054
  disconnectServer,
15397
16055
  disconnectAll,
16056
+ disableSource,
15398
16057
  disableServer,
16058
+ diagnoseServer,
15399
16059
  connectToServer,
15400
16060
  closeDb,
15401
16061
  callTool,
16062
+ addSource,
15402
16063
  addServer
15403
16064
  };