@hasna/mcps 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +2269 -232
- package/bin/mcp.js +976 -104
- package/dashboard/dist/assets/index-Df11SKJo.css +1 -0
- package/dashboard/dist/assets/index-a3768emB.js +264 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +852 -154
- package/dist/lib/doctor.d.ts +14 -0
- package/dist/lib/finder.d.ts +8 -0
- package/dist/lib/install.d.ts +8 -0
- package/dist/lib/registry.d.ts +4 -0
- package/dist/lib/sources.d.ts +18 -0
- package/dist/server/serve.d.ts +1 -0
- package/dist/types.d.ts +38 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-Bl3d9_Ks.css +0 -1
- package/dashboard/dist/assets/index-KmgzJKQR.js +0 -224
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
|
|
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/
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
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
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
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 [];
|
|
7014
7150
|
}
|
|
7015
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 [];
|
|
7275
|
+
}
|
|
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:
|
|
7025
|
-
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
|
|
7040
|
-
|
|
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,
|
|
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,79 @@ 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
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
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}`);
|
|
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
|
+
});
|
|
7130
7520
|
}
|
|
7131
|
-
const
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
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));
|
|
7525
|
+
}
|
|
7526
|
+
});
|
|
7527
|
+
run(uniqueTools);
|
|
7135
7528
|
}
|
|
7136
|
-
|
|
7137
|
-
const
|
|
7138
|
-
|
|
7139
|
-
|
|
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;
|
|
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)]));
|
|
7145
7533
|
}
|
|
7146
|
-
|
|
7147
|
-
const server =
|
|
7148
|
-
if (!server)
|
|
7149
|
-
throw new Error(`Server "${id}" not found
|
|
7150
|
-
}
|
|
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;
|
|
7164
|
-
}
|
|
7165
|
-
}
|
|
7534
|
+
function cloneServer(id, newName) {
|
|
7535
|
+
const server = getServer(id);
|
|
7536
|
+
if (!server)
|
|
7537
|
+
throw new Error(`Server "${id}" not found`);
|
|
7166
7538
|
return addServer({
|
|
7167
|
-
name:
|
|
7168
|
-
description: server.description,
|
|
7169
|
-
command,
|
|
7170
|
-
args,
|
|
7171
|
-
|
|
7172
|
-
|
|
7539
|
+
name: newName,
|
|
7540
|
+
description: server.description ?? undefined,
|
|
7541
|
+
command: server.command,
|
|
7542
|
+
args: server.args,
|
|
7543
|
+
env: server.env,
|
|
7544
|
+
transport: server.transport,
|
|
7545
|
+
url: server.url ?? undefined,
|
|
7546
|
+
source: server.source
|
|
7173
7547
|
});
|
|
7174
7548
|
}
|
|
7549
|
+
function getCachedTools(serverId) {
|
|
7550
|
+
const db2 = getDb();
|
|
7551
|
+
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
|
|
7552
|
+
return rows.map((r) => ({
|
|
7553
|
+
name: r.name,
|
|
7554
|
+
description: r.description,
|
|
7555
|
+
input_schema: safeJsonParse(r.input_schema, {})
|
|
7556
|
+
}));
|
|
7557
|
+
}
|
|
7558
|
+
// src/lib/doctor.ts
|
|
7559
|
+
import { execFileSync } from "child_process";
|
|
7560
|
+
|
|
7175
7561
|
// node_modules/zod/v4/core/core.js
|
|
7176
7562
|
var NEVER = Object.freeze({
|
|
7177
7563
|
status: "aborted"
|
|
@@ -15284,54 +15670,126 @@ class StreamableHTTPClientTransport {
|
|
|
15284
15670
|
}
|
|
15285
15671
|
|
|
15286
15672
|
// src/lib/proxy.ts
|
|
15673
|
+
init_config();
|
|
15674
|
+
init_db();
|
|
15287
15675
|
var connections = new Map;
|
|
15676
|
+
var inflightConnections = new Map;
|
|
15677
|
+
function buildEnv(extra) {
|
|
15678
|
+
const merged = {};
|
|
15679
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
15680
|
+
if (typeof value === "string")
|
|
15681
|
+
merged[key] = value;
|
|
15682
|
+
}
|
|
15683
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
15684
|
+
if (value === undefined || value === null)
|
|
15685
|
+
continue;
|
|
15686
|
+
merged[key] = String(value);
|
|
15687
|
+
}
|
|
15688
|
+
return merged;
|
|
15689
|
+
}
|
|
15690
|
+
function requireUrl(entry) {
|
|
15691
|
+
if (!entry.url) {
|
|
15692
|
+
throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
|
|
15693
|
+
}
|
|
15694
|
+
try {
|
|
15695
|
+
return new URL(entry.url);
|
|
15696
|
+
} catch {
|
|
15697
|
+
throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
|
|
15698
|
+
}
|
|
15699
|
+
}
|
|
15288
15700
|
async function connectToServer(entry) {
|
|
15289
15701
|
if (connections.has(entry.id)) {
|
|
15290
15702
|
return connections.get(entry.id);
|
|
15291
15703
|
}
|
|
15704
|
+
const inflight = inflightConnections.get(entry.id);
|
|
15705
|
+
if (inflight) {
|
|
15706
|
+
return inflight;
|
|
15707
|
+
}
|
|
15292
15708
|
const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
|
|
15293
15709
|
let transport;
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15710
|
+
const connectPromise = (async () => {
|
|
15711
|
+
try {
|
|
15712
|
+
if (entry.transport === "stdio") {
|
|
15713
|
+
if (!entry.command?.trim()) {
|
|
15714
|
+
throw new Error(`Server "${entry.id}" is missing a command`);
|
|
15715
|
+
}
|
|
15716
|
+
transport = new StdioClientTransport({
|
|
15717
|
+
command: entry.command,
|
|
15718
|
+
args: entry.args,
|
|
15719
|
+
env: buildEnv(entry.env)
|
|
15720
|
+
});
|
|
15721
|
+
} else if (entry.transport === "sse") {
|
|
15722
|
+
transport = new SSEClientTransport(requireUrl(entry));
|
|
15723
|
+
} else {
|
|
15724
|
+
transport = new StreamableHTTPClientTransport(requireUrl(entry));
|
|
15725
|
+
}
|
|
15726
|
+
await client.connect(transport);
|
|
15727
|
+
const result = await client.listTools();
|
|
15728
|
+
const tools = (result.tools || []).map((t) => ({
|
|
15729
|
+
server_id: entry.id,
|
|
15730
|
+
name: t.name,
|
|
15731
|
+
description: t.description || "",
|
|
15732
|
+
input_schema: t.inputSchema || {}
|
|
15733
|
+
}));
|
|
15734
|
+
cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
|
|
15735
|
+
const connected = {
|
|
15736
|
+
entry,
|
|
15737
|
+
tools,
|
|
15738
|
+
disconnect: async () => {
|
|
15739
|
+
try {
|
|
15740
|
+
await client.close();
|
|
15741
|
+
} finally {
|
|
15742
|
+
connections.delete(entry.id);
|
|
15743
|
+
}
|
|
15744
|
+
}
|
|
15745
|
+
};
|
|
15746
|
+
connected._client = client;
|
|
15747
|
+
connections.set(entry.id, connected);
|
|
15748
|
+
try {
|
|
15749
|
+
getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
|
|
15750
|
+
} catch {}
|
|
15751
|
+
return connected;
|
|
15752
|
+
} catch (err) {
|
|
15753
|
+
try {
|
|
15754
|
+
getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
|
|
15755
|
+
} catch {}
|
|
15756
|
+
try {
|
|
15757
|
+
await client.close();
|
|
15758
|
+
} catch {}
|
|
15759
|
+
throw err;
|
|
15320
15760
|
}
|
|
15321
|
-
};
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15761
|
+
})();
|
|
15762
|
+
inflightConnections.set(entry.id, connectPromise);
|
|
15763
|
+
try {
|
|
15764
|
+
return await connectPromise;
|
|
15765
|
+
} finally {
|
|
15766
|
+
inflightConnections.delete(entry.id);
|
|
15767
|
+
}
|
|
15325
15768
|
}
|
|
15326
15769
|
async function disconnectServer(id) {
|
|
15327
15770
|
const conn = connections.get(id);
|
|
15328
15771
|
if (conn) {
|
|
15329
15772
|
await conn.disconnect();
|
|
15773
|
+
return;
|
|
15774
|
+
}
|
|
15775
|
+
const inflight = inflightConnections.get(id);
|
|
15776
|
+
if (inflight) {
|
|
15777
|
+
try {
|
|
15778
|
+
const pending = await inflight;
|
|
15779
|
+
await pending.disconnect();
|
|
15780
|
+
} catch {}
|
|
15330
15781
|
}
|
|
15331
15782
|
}
|
|
15332
15783
|
async function disconnectAll() {
|
|
15333
15784
|
const ids = Array.from(connections.keys());
|
|
15334
|
-
await Promise.
|
|
15785
|
+
await Promise.allSettled(ids.map((id) => disconnectServer(id)));
|
|
15786
|
+
const inflight = Array.from(inflightConnections.values());
|
|
15787
|
+
await Promise.allSettled(inflight.map(async (promise2) => {
|
|
15788
|
+
try {
|
|
15789
|
+
const conn = await promise2;
|
|
15790
|
+
await conn.disconnect();
|
|
15791
|
+
} catch {}
|
|
15792
|
+
}));
|
|
15335
15793
|
}
|
|
15336
15794
|
function listAllTools() {
|
|
15337
15795
|
const tools = [];
|
|
@@ -15346,12 +15804,19 @@ function listAllTools() {
|
|
|
15346
15804
|
return tools;
|
|
15347
15805
|
}
|
|
15348
15806
|
async function callTool(prefixedName, args) {
|
|
15349
|
-
const
|
|
15807
|
+
const normalized = prefixedName.trim();
|
|
15808
|
+
if (!normalized) {
|
|
15809
|
+
throw new Error("Tool name is required");
|
|
15810
|
+
}
|
|
15811
|
+
const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
15350
15812
|
if (sepIdx === -1) {
|
|
15351
15813
|
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
15352
15814
|
}
|
|
15353
|
-
const serverId =
|
|
15354
|
-
const toolName =
|
|
15815
|
+
const serverId = normalized.slice(0, sepIdx).trim();
|
|
15816
|
+
const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
|
|
15817
|
+
if (!serverId || !toolName) {
|
|
15818
|
+
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
15819
|
+
}
|
|
15355
15820
|
const conn = connections.get(serverId);
|
|
15356
15821
|
if (!conn) {
|
|
15357
15822
|
throw new Error(`Server "${serverId}" is not connected`);
|
|
@@ -15381,23 +15846,256 @@ async function refreshTools(id) {
|
|
|
15381
15846
|
cacheTools(id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
|
|
15382
15847
|
return tools;
|
|
15383
15848
|
}
|
|
15849
|
+
|
|
15850
|
+
// src/lib/doctor.ts
|
|
15851
|
+
async function diagnoseServer(server) {
|
|
15852
|
+
const checks3 = [];
|
|
15853
|
+
if (server.transport === "stdio") {
|
|
15854
|
+
try {
|
|
15855
|
+
const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
|
|
15856
|
+
let version2 = "";
|
|
15857
|
+
try {
|
|
15858
|
+
version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
|
|
15859
|
+
`)[0];
|
|
15860
|
+
} catch {}
|
|
15861
|
+
checks3.push({ name: "command on PATH", pass: true, message: `${path}${version2 ? ` (${version2})` : ""}` });
|
|
15862
|
+
} catch {
|
|
15863
|
+
checks3.push({
|
|
15864
|
+
name: "command on PATH",
|
|
15865
|
+
pass: false,
|
|
15866
|
+
message: `${server.command} not found on PATH`,
|
|
15867
|
+
fixable: true,
|
|
15868
|
+
fixHint: server.args[0] || server.command
|
|
15869
|
+
});
|
|
15870
|
+
}
|
|
15871
|
+
}
|
|
15872
|
+
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
15873
|
+
if (Object.keys(server.env).length === 0) {
|
|
15874
|
+
checks3.push({ name: "env vars", pass: true, message: "no env vars required" });
|
|
15875
|
+
} else if (missingEnv.length > 0) {
|
|
15876
|
+
checks3.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
|
|
15877
|
+
} else {
|
|
15878
|
+
checks3.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
|
|
15879
|
+
}
|
|
15880
|
+
if (server.transport !== "stdio" && server.url) {
|
|
15881
|
+
try {
|
|
15882
|
+
const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
|
|
15883
|
+
checks3.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
|
|
15884
|
+
} catch (err) {
|
|
15885
|
+
checks3.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
|
|
15886
|
+
}
|
|
15887
|
+
}
|
|
15888
|
+
if (server.enabled) {
|
|
15889
|
+
try {
|
|
15890
|
+
await Promise.race([
|
|
15891
|
+
connectToServer(server),
|
|
15892
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
|
|
15893
|
+
]);
|
|
15894
|
+
await disconnectServer(server.id);
|
|
15895
|
+
checks3.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
|
|
15896
|
+
} catch (err) {
|
|
15897
|
+
checks3.push({ name: "connect & list tools", pass: false, message: err.message });
|
|
15898
|
+
}
|
|
15899
|
+
} else {
|
|
15900
|
+
checks3.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
|
|
15901
|
+
}
|
|
15902
|
+
return {
|
|
15903
|
+
server,
|
|
15904
|
+
checks: checks3,
|
|
15905
|
+
healthy: checks3.every((c) => c.pass)
|
|
15906
|
+
};
|
|
15907
|
+
}
|
|
15908
|
+
// src/lib/remote.ts
|
|
15909
|
+
init_config();
|
|
15910
|
+
function parseRegistryEntry(entry) {
|
|
15911
|
+
const s = entry.server;
|
|
15912
|
+
return {
|
|
15913
|
+
id: s.name,
|
|
15914
|
+
name: s.name,
|
|
15915
|
+
description: s.description || "",
|
|
15916
|
+
repository: s.repository,
|
|
15917
|
+
packages: s.packages || []
|
|
15918
|
+
};
|
|
15919
|
+
}
|
|
15920
|
+
async function searchRegistry(query) {
|
|
15921
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
15922
|
+
if (!res.ok) {
|
|
15923
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
15924
|
+
}
|
|
15925
|
+
const data = await res.json();
|
|
15926
|
+
const entries = data.servers || [];
|
|
15927
|
+
const q = query.toLowerCase();
|
|
15928
|
+
return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
|
15929
|
+
}
|
|
15930
|
+
async function getRegistryServer(id) {
|
|
15931
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
15932
|
+
if (!res.ok) {
|
|
15933
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
15934
|
+
}
|
|
15935
|
+
const data = await res.json();
|
|
15936
|
+
const entries = data.servers || [];
|
|
15937
|
+
const all = entries.map(parseRegistryEntry);
|
|
15938
|
+
return all.find((s) => s.id === id) || null;
|
|
15939
|
+
}
|
|
15940
|
+
async function installFromRegistry(id) {
|
|
15941
|
+
const server = await getRegistryServer(id);
|
|
15942
|
+
if (!server) {
|
|
15943
|
+
throw new Error(`Server "${id}" not found in registry`);
|
|
15944
|
+
}
|
|
15945
|
+
const pkg = server.packages?.[0];
|
|
15946
|
+
let command = "npx";
|
|
15947
|
+
let args = [];
|
|
15948
|
+
let transport = "stdio";
|
|
15949
|
+
if (pkg) {
|
|
15950
|
+
if (pkg.registryType === "npm") {
|
|
15951
|
+
command = "npx";
|
|
15952
|
+
args = ["-y", pkg.identifier];
|
|
15953
|
+
} else {
|
|
15954
|
+
command = pkg.identifier;
|
|
15955
|
+
}
|
|
15956
|
+
if (pkg.transport?.type) {
|
|
15957
|
+
transport = pkg.transport.type;
|
|
15958
|
+
}
|
|
15959
|
+
}
|
|
15960
|
+
return addServer({
|
|
15961
|
+
name: server.name,
|
|
15962
|
+
description: server.description,
|
|
15963
|
+
command,
|
|
15964
|
+
args,
|
|
15965
|
+
transport,
|
|
15966
|
+
source: "registry"
|
|
15967
|
+
});
|
|
15968
|
+
}
|
|
15969
|
+
// src/lib/finder.ts
|
|
15970
|
+
init_sources();
|
|
15971
|
+
async function listAwesomeServers() {
|
|
15972
|
+
const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
|
|
15973
|
+
const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
|
|
15974
|
+
if (!source)
|
|
15975
|
+
return [];
|
|
15976
|
+
return searchSource2(source, "");
|
|
15977
|
+
}
|
|
15978
|
+
|
|
15979
|
+
// src/index.ts
|
|
15980
|
+
init_sources();
|
|
15981
|
+
|
|
15982
|
+
// src/lib/install.ts
|
|
15983
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
15984
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
15985
|
+
import { join as join3 } from "path";
|
|
15986
|
+
import { homedir as homedir2 } from "os";
|
|
15987
|
+
function installToClaude(entry) {
|
|
15988
|
+
try {
|
|
15989
|
+
const args = [
|
|
15990
|
+
"mcp",
|
|
15991
|
+
"add",
|
|
15992
|
+
"--transport",
|
|
15993
|
+
entry.transport,
|
|
15994
|
+
"--scope",
|
|
15995
|
+
"user"
|
|
15996
|
+
];
|
|
15997
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
15998
|
+
args.push("--env", `${k}=${v}`);
|
|
15999
|
+
}
|
|
16000
|
+
args.push(entry.id, "--", entry.command, ...entry.args);
|
|
16001
|
+
execFileSync2("claude", args, { stdio: "pipe" });
|
|
16002
|
+
return { agent: "claude", success: true };
|
|
16003
|
+
} catch (err) {
|
|
16004
|
+
return { agent: "claude", success: false, error: err.message };
|
|
16005
|
+
}
|
|
16006
|
+
}
|
|
16007
|
+
function installToCodex(entry) {
|
|
16008
|
+
try {
|
|
16009
|
+
const configDir = join3(homedir2(), ".codex");
|
|
16010
|
+
const configPath = join3(configDir, "config.toml");
|
|
16011
|
+
if (!existsSync2(configDir)) {
|
|
16012
|
+
mkdirSync3(configDir, { recursive: true });
|
|
16013
|
+
}
|
|
16014
|
+
const block = `
|
|
16015
|
+
[mcp_servers.${entry.id}]
|
|
16016
|
+
` + `command = ${JSON.stringify(entry.command)}
|
|
16017
|
+
` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
|
|
16018
|
+
`;
|
|
16019
|
+
const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
|
|
16020
|
+
if (existing.includes(`[mcp_servers.${entry.id}]`)) {
|
|
16021
|
+
return { agent: "codex", success: true };
|
|
16022
|
+
}
|
|
16023
|
+
writeFileSync2(configPath, existing + block, "utf-8");
|
|
16024
|
+
return { agent: "codex", success: true };
|
|
16025
|
+
} catch (err) {
|
|
16026
|
+
return { agent: "codex", success: false, error: err.message };
|
|
16027
|
+
}
|
|
16028
|
+
}
|
|
16029
|
+
function installToGemini(entry) {
|
|
16030
|
+
try {
|
|
16031
|
+
const configDir = join3(homedir2(), ".gemini");
|
|
16032
|
+
const configPath = join3(configDir, "settings.json");
|
|
16033
|
+
if (!existsSync2(configDir)) {
|
|
16034
|
+
mkdirSync3(configDir, { recursive: true });
|
|
16035
|
+
}
|
|
16036
|
+
let settings = {};
|
|
16037
|
+
if (existsSync2(configPath)) {
|
|
16038
|
+
settings = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
16039
|
+
}
|
|
16040
|
+
if (!settings.mcpServers)
|
|
16041
|
+
settings.mcpServers = {};
|
|
16042
|
+
settings.mcpServers[entry.id] = {
|
|
16043
|
+
command: entry.command,
|
|
16044
|
+
args: entry.args,
|
|
16045
|
+
...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
|
|
16046
|
+
};
|
|
16047
|
+
writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
16048
|
+
return { agent: "gemini", success: true };
|
|
16049
|
+
} catch (err) {
|
|
16050
|
+
return { agent: "gemini", success: false, error: err.message };
|
|
16051
|
+
}
|
|
16052
|
+
}
|
|
16053
|
+
function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
|
|
16054
|
+
return targets.map((target) => {
|
|
16055
|
+
if (target === "claude")
|
|
16056
|
+
return installToClaude(entry);
|
|
16057
|
+
if (target === "codex")
|
|
16058
|
+
return installToCodex(entry);
|
|
16059
|
+
if (target === "gemini")
|
|
16060
|
+
return installToGemini(entry);
|
|
16061
|
+
return { agent: target, success: false, error: "Unknown target" };
|
|
16062
|
+
});
|
|
16063
|
+
}
|
|
16064
|
+
|
|
16065
|
+
// src/index.ts
|
|
16066
|
+
init_db();
|
|
15384
16067
|
export {
|
|
15385
16068
|
updateServer,
|
|
16069
|
+
unsetServerEnv,
|
|
16070
|
+
setServerEnv,
|
|
15386
16071
|
searchRegistry,
|
|
16072
|
+
removeSource,
|
|
15387
16073
|
removeServer,
|
|
15388
16074
|
refreshTools,
|
|
16075
|
+
listSources,
|
|
15389
16076
|
listServers,
|
|
16077
|
+
listAwesomeServers,
|
|
15390
16078
|
listAllTools,
|
|
16079
|
+
installToAgents,
|
|
15391
16080
|
installFromRegistry,
|
|
16081
|
+
getToolCounts,
|
|
16082
|
+
getSource,
|
|
15392
16083
|
getServer,
|
|
15393
16084
|
getRegistryServer,
|
|
15394
16085
|
getDb,
|
|
16086
|
+
getCachedTools,
|
|
16087
|
+
findServers,
|
|
16088
|
+
enableSource,
|
|
15395
16089
|
enableServer,
|
|
15396
16090
|
disconnectServer,
|
|
15397
16091
|
disconnectAll,
|
|
16092
|
+
disableSource,
|
|
15398
16093
|
disableServer,
|
|
16094
|
+
diagnoseServer,
|
|
15399
16095
|
connectToServer,
|
|
15400
16096
|
closeDb,
|
|
16097
|
+
cloneServer,
|
|
15401
16098
|
callTool,
|
|
16099
|
+
addSource,
|
|
15402
16100
|
addServer
|
|
15403
16101
|
};
|