@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/LICENSE +190 -0
- package/README.md +129 -0
- package/bin/index.js +1782 -244
- package/bin/mcp.js +903 -104
- package/dashboard/dist/assets/{index-KmgzJKQR.js → index-BHsa5YXH.js} +40 -35
- package/dashboard/dist/assets/index-C7n__Rq8.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +817 -156
- package/dist/lib/doctor.d.ts +12 -0
- package/dist/lib/finder.d.ts +8 -0
- package/dist/lib/install.d.ts +8 -0
- package/dist/lib/registry.d.ts +3 -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 +2 -2
- package/dashboard/dist/assets/index-Bl3d9_Ks.css +0 -1
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 [];
|
|
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:
|
|
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,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
|
-
|
|
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}`);
|
|
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
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
15354
|
-
const toolName =
|
|
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
|
};
|