@hasna/mcps 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1 -1
- package/dashboard/dist/assets/index-Bl3d9_Ks.css +0 -1
package/bin/mcp.js
CHANGED
|
@@ -25,6 +25,7 @@ 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
|
|
|
30
31
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
@@ -6286,7 +6287,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
6286
6287
|
}
|
|
6287
6288
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6288
6289
|
function getTime(strictTimeZone) {
|
|
6289
|
-
return function
|
|
6290
|
+
return function time(str) {
|
|
6290
6291
|
const matches = TIME.exec(str);
|
|
6291
6292
|
if (!matches)
|
|
6292
6293
|
return false;
|
|
@@ -6499,6 +6500,383 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
6499
6500
|
exports.default = formatsPlugin;
|
|
6500
6501
|
});
|
|
6501
6502
|
|
|
6503
|
+
// src/lib/config.ts
|
|
6504
|
+
import { join } from "path";
|
|
6505
|
+
import { homedir } from "os";
|
|
6506
|
+
var MCPS_DIR, DB_PATH, REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers", TOOL_PREFIX_SEPARATOR = "__";
|
|
6507
|
+
var init_config = __esm(() => {
|
|
6508
|
+
MCPS_DIR = join(homedir(), ".mcps");
|
|
6509
|
+
DB_PATH = join(MCPS_DIR, "registry.db");
|
|
6510
|
+
});
|
|
6511
|
+
|
|
6512
|
+
// src/lib/db.ts
|
|
6513
|
+
import { Database } from "bun:sqlite";
|
|
6514
|
+
import { mkdirSync } from "fs";
|
|
6515
|
+
function getDb() {
|
|
6516
|
+
if (db)
|
|
6517
|
+
return db;
|
|
6518
|
+
mkdirSync(MCPS_DIR, { recursive: true });
|
|
6519
|
+
db = new Database(DB_PATH, { create: true });
|
|
6520
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
6521
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
6522
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
6523
|
+
db.exec(`
|
|
6524
|
+
CREATE TABLE IF NOT EXISTS servers (
|
|
6525
|
+
id TEXT PRIMARY KEY,
|
|
6526
|
+
name TEXT NOT NULL,
|
|
6527
|
+
description TEXT,
|
|
6528
|
+
command TEXT NOT NULL,
|
|
6529
|
+
args TEXT NOT NULL DEFAULT '[]',
|
|
6530
|
+
env TEXT NOT NULL DEFAULT '{}',
|
|
6531
|
+
transport TEXT NOT NULL DEFAULT 'stdio',
|
|
6532
|
+
url TEXT,
|
|
6533
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
6534
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
6535
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
6536
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
6537
|
+
)
|
|
6538
|
+
`);
|
|
6539
|
+
db.exec(`
|
|
6540
|
+
CREATE TABLE IF NOT EXISTS tool_cache (
|
|
6541
|
+
server_id TEXT NOT NULL,
|
|
6542
|
+
name TEXT NOT NULL,
|
|
6543
|
+
description TEXT NOT NULL DEFAULT '',
|
|
6544
|
+
input_schema TEXT NOT NULL DEFAULT '{}',
|
|
6545
|
+
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
6546
|
+
PRIMARY KEY (server_id, name),
|
|
6547
|
+
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
6548
|
+
)
|
|
6549
|
+
`);
|
|
6550
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
|
|
6551
|
+
try {
|
|
6552
|
+
db.exec("ALTER TABLE servers ADD COLUMN last_connected_at TEXT");
|
|
6553
|
+
} catch {}
|
|
6554
|
+
try {
|
|
6555
|
+
db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
|
|
6556
|
+
} catch {}
|
|
6557
|
+
db.exec(`
|
|
6558
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
6559
|
+
id TEXT PRIMARY KEY,
|
|
6560
|
+
name TEXT NOT NULL,
|
|
6561
|
+
type TEXT NOT NULL,
|
|
6562
|
+
url TEXT NOT NULL,
|
|
6563
|
+
description TEXT,
|
|
6564
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
6565
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
6566
|
+
)
|
|
6567
|
+
`);
|
|
6568
|
+
const count = db.query("SELECT COUNT(*) as c FROM sources").get().c;
|
|
6569
|
+
if (count === 0) {
|
|
6570
|
+
db.exec(`
|
|
6571
|
+
INSERT OR IGNORE INTO sources (id, name, type, url, description) VALUES
|
|
6572
|
+
('official-registry', 'Official MCP Registry', 'mcp-registry', 'https://registry.modelcontextprotocol.io/v0/servers', 'The official Model Context Protocol server registry'),
|
|
6573
|
+
('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'),
|
|
6574
|
+
('npm-mcp', 'npm MCP Packages', 'npm-search', 'https://registry.npmjs.org/-/v1/search', 'Search npm packages for MCP servers'),
|
|
6575
|
+
('github-mcp-topic', 'GitHub MCP Topic', 'github-topic', 'https://api.github.com/search/repositories', 'GitHub repositories tagged with mcp-server topic')
|
|
6576
|
+
`);
|
|
6577
|
+
}
|
|
6578
|
+
return db;
|
|
6579
|
+
}
|
|
6580
|
+
var db = null;
|
|
6581
|
+
var init_db = __esm(() => {
|
|
6582
|
+
init_config();
|
|
6583
|
+
});
|
|
6584
|
+
|
|
6585
|
+
// src/lib/sources.ts
|
|
6586
|
+
var exports_sources = {};
|
|
6587
|
+
__export(exports_sources, {
|
|
6588
|
+
searchSource: () => searchSource,
|
|
6589
|
+
removeSource: () => removeSource,
|
|
6590
|
+
listSources: () => listSources,
|
|
6591
|
+
getSource: () => getSource,
|
|
6592
|
+
findServers: () => findServers,
|
|
6593
|
+
enableSource: () => enableSource,
|
|
6594
|
+
disableSource: () => disableSource,
|
|
6595
|
+
clearCache: () => clearCache,
|
|
6596
|
+
addSource: () => addSource
|
|
6597
|
+
});
|
|
6598
|
+
import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
|
|
6599
|
+
import { join as join2 } from "path";
|
|
6600
|
+
function getCacheFile(sourceId) {
|
|
6601
|
+
return join2(CACHE_DIR, `${sourceId}.json`);
|
|
6602
|
+
}
|
|
6603
|
+
function readCache(sourceId) {
|
|
6604
|
+
try {
|
|
6605
|
+
const file = getCacheFile(sourceId);
|
|
6606
|
+
if (!existsSync(file))
|
|
6607
|
+
return null;
|
|
6608
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
6609
|
+
return data;
|
|
6610
|
+
} catch {
|
|
6611
|
+
return null;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
function writeCache(sourceId, results) {
|
|
6615
|
+
try {
|
|
6616
|
+
mkdirSync2(CACHE_DIR, { recursive: true });
|
|
6617
|
+
writeFileSync(getCacheFile(sourceId), JSON.stringify({ results, cachedAt: Date.now() }), "utf-8");
|
|
6618
|
+
} catch {}
|
|
6619
|
+
}
|
|
6620
|
+
function clearCache(sourceId) {
|
|
6621
|
+
try {
|
|
6622
|
+
if (!existsSync(CACHE_DIR))
|
|
6623
|
+
return;
|
|
6624
|
+
const files = readdirSync(CACHE_DIR);
|
|
6625
|
+
for (const file of files) {
|
|
6626
|
+
if (!file.endsWith(".json"))
|
|
6627
|
+
continue;
|
|
6628
|
+
if (!sourceId || file.startsWith(`${sourceId}.`)) {
|
|
6629
|
+
try {
|
|
6630
|
+
unlinkSync(join2(CACHE_DIR, file));
|
|
6631
|
+
} catch {}
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
} catch {}
|
|
6635
|
+
}
|
|
6636
|
+
function generateId2(name) {
|
|
6637
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
6638
|
+
}
|
|
6639
|
+
function listSources() {
|
|
6640
|
+
const db2 = getDb();
|
|
6641
|
+
const rows = db2.query("SELECT * FROM sources ORDER BY created_at ASC").all();
|
|
6642
|
+
return rows.map((r) => ({ ...r, enabled: r.enabled === 1 }));
|
|
6643
|
+
}
|
|
6644
|
+
function getSource(id) {
|
|
6645
|
+
const db2 = getDb();
|
|
6646
|
+
const row = db2.query("SELECT * FROM sources WHERE id = ?").get(id);
|
|
6647
|
+
if (!row)
|
|
6648
|
+
return null;
|
|
6649
|
+
return { ...row, enabled: row.enabled === 1 };
|
|
6650
|
+
}
|
|
6651
|
+
function addSource(opts) {
|
|
6652
|
+
const db2 = getDb();
|
|
6653
|
+
const id = generateId2(opts.name);
|
|
6654
|
+
db2.run("INSERT INTO sources (id, name, type, url, description) VALUES (?, ?, ?, ?, ?)", [id, opts.name, opts.type, opts.url, opts.description ?? null]);
|
|
6655
|
+
return getSource(id);
|
|
6656
|
+
}
|
|
6657
|
+
function removeSource(id) {
|
|
6658
|
+
const db2 = getDb();
|
|
6659
|
+
db2.run("DELETE FROM sources WHERE id = ?", [id]);
|
|
6660
|
+
}
|
|
6661
|
+
function enableSource(id) {
|
|
6662
|
+
const db2 = getDb();
|
|
6663
|
+
db2.run("UPDATE sources SET enabled = 1 WHERE id = ?", [id]);
|
|
6664
|
+
}
|
|
6665
|
+
function disableSource(id) {
|
|
6666
|
+
const db2 = getDb();
|
|
6667
|
+
db2.run("UPDATE sources SET enabled = 0 WHERE id = ?", [id]);
|
|
6668
|
+
}
|
|
6669
|
+
async function searchMcpRegistry(source, query) {
|
|
6670
|
+
try {
|
|
6671
|
+
const res = await fetch(source.url);
|
|
6672
|
+
if (!res.ok)
|
|
6673
|
+
return [];
|
|
6674
|
+
const data = await res.json();
|
|
6675
|
+
const q = query.toLowerCase();
|
|
6676
|
+
return (data.servers || []).map((e) => e.server).filter((s) => q === "" || s.name.toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q)).map((s) => ({
|
|
6677
|
+
name: s.name,
|
|
6678
|
+
description: s.description || "",
|
|
6679
|
+
source: "registry",
|
|
6680
|
+
sourceId: source.id,
|
|
6681
|
+
url: s.repository?.url,
|
|
6682
|
+
githubRepo: s.repository?.url,
|
|
6683
|
+
installCmd: s.packages?.[0] ? s.packages[0].registryType === "npm" ? `npx -y ${s.packages[0].identifier}` : s.packages[0].identifier : undefined,
|
|
6684
|
+
npmPackage: s.packages?.find((p) => p.registryType === "npm")?.identifier
|
|
6685
|
+
}));
|
|
6686
|
+
} catch {
|
|
6687
|
+
return [];
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
async function searchAwesomeList(source, query) {
|
|
6691
|
+
try {
|
|
6692
|
+
const res = await fetch(source.url);
|
|
6693
|
+
if (!res.ok)
|
|
6694
|
+
return [];
|
|
6695
|
+
const text = await res.text();
|
|
6696
|
+
const results = [];
|
|
6697
|
+
const linkPattern = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)(?:\s*[-\u2013]\s*([^\n]+))?/g;
|
|
6698
|
+
const q = query.toLowerCase();
|
|
6699
|
+
let match;
|
|
6700
|
+
while ((match = linkPattern.exec(text)) !== null) {
|
|
6701
|
+
const name = match[1].trim();
|
|
6702
|
+
const url2 = match[2].trim();
|
|
6703
|
+
const description = match[3]?.trim() || "";
|
|
6704
|
+
if ((url2.includes("github.com") || url2.includes("npmjs.com")) && name.length > 2) {
|
|
6705
|
+
if (q === "" || `${name} ${description}`.toLowerCase().includes(q)) {
|
|
6706
|
+
results.push({
|
|
6707
|
+
name,
|
|
6708
|
+
description,
|
|
6709
|
+
source: "awesome",
|
|
6710
|
+
sourceId: source.id,
|
|
6711
|
+
url: url2,
|
|
6712
|
+
githubRepo: url2.includes("github.com") ? url2 : undefined,
|
|
6713
|
+
installCmd: url2.includes("npmjs.com") ? `npx -y ${url2.split("/").pop()}` : undefined
|
|
6714
|
+
});
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
return results;
|
|
6719
|
+
} catch {
|
|
6720
|
+
return [];
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
function scoreNpmPackage(pkg) {
|
|
6724
|
+
const name = pkg.name.toLowerCase();
|
|
6725
|
+
const keywords = (pkg.keywords || []).map((k) => k.toLowerCase());
|
|
6726
|
+
let score = 0;
|
|
6727
|
+
if (name.includes("mcp-server"))
|
|
6728
|
+
score += 3;
|
|
6729
|
+
else if (name.startsWith("mcp-") || name.endsWith("-mcp"))
|
|
6730
|
+
score += 2;
|
|
6731
|
+
else if (name.includes("mcp"))
|
|
6732
|
+
score += 1;
|
|
6733
|
+
if (keywords.includes("mcp"))
|
|
6734
|
+
score += 1;
|
|
6735
|
+
if (keywords.includes("mcp-server"))
|
|
6736
|
+
score += 2;
|
|
6737
|
+
if (keywords.some((k) => k.includes("modelcontextprotocol")))
|
|
6738
|
+
score += 2;
|
|
6739
|
+
return score;
|
|
6740
|
+
}
|
|
6741
|
+
async function searchNpm(source, query) {
|
|
6742
|
+
try {
|
|
6743
|
+
const url2 = new URL(source.url);
|
|
6744
|
+
url2.searchParams.set("text", `mcp-server ${query}`);
|
|
6745
|
+
url2.searchParams.set("size", "50");
|
|
6746
|
+
const res = await fetch(url2.toString());
|
|
6747
|
+
if (!res.ok)
|
|
6748
|
+
return [];
|
|
6749
|
+
const data = await res.json();
|
|
6750
|
+
const q = query.toLowerCase();
|
|
6751
|
+
const scored = (data.objects || []).map((o) => ({ pkg: o.package, score: scoreNpmPackage(o.package) })).filter(({ pkg, score }) => {
|
|
6752
|
+
if (score < 1)
|
|
6753
|
+
return false;
|
|
6754
|
+
const text = `${pkg.name} ${pkg.description || ""} ${(pkg.keywords || []).join(" ")}`.toLowerCase();
|
|
6755
|
+
return q === "" || text.includes(q);
|
|
6756
|
+
});
|
|
6757
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6758
|
+
return scored.map(({ pkg }) => ({
|
|
6759
|
+
name: pkg.name,
|
|
6760
|
+
description: pkg.description || "",
|
|
6761
|
+
source: "npm",
|
|
6762
|
+
sourceId: source.id,
|
|
6763
|
+
url: pkg.links?.repository || pkg.links?.npm,
|
|
6764
|
+
npmPackage: pkg.name,
|
|
6765
|
+
installCmd: `npx -y ${pkg.name}`
|
|
6766
|
+
}));
|
|
6767
|
+
} catch {
|
|
6768
|
+
return [];
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
async function searchGitHubTopic(source, query) {
|
|
6772
|
+
const token = process.env.GITHUB_TOKEN;
|
|
6773
|
+
try {
|
|
6774
|
+
const url2 = new URL(source.url);
|
|
6775
|
+
const q = query ? `${query} topic:mcp-server` : "topic:mcp-server";
|
|
6776
|
+
url2.searchParams.set("q", q);
|
|
6777
|
+
url2.searchParams.set("sort", "stars");
|
|
6778
|
+
url2.searchParams.set("per_page", "30");
|
|
6779
|
+
const headers = {
|
|
6780
|
+
Accept: "application/vnd.github.v3+json"
|
|
6781
|
+
};
|
|
6782
|
+
if (token)
|
|
6783
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
6784
|
+
const res = await fetch(url2.toString(), { headers });
|
|
6785
|
+
if (!res.ok)
|
|
6786
|
+
return [];
|
|
6787
|
+
const data = await res.json();
|
|
6788
|
+
return (data.items || []).map((repo) => ({
|
|
6789
|
+
name: repo.full_name,
|
|
6790
|
+
description: repo.description || "",
|
|
6791
|
+
source: "github",
|
|
6792
|
+
sourceId: source.id,
|
|
6793
|
+
url: repo.html_url,
|
|
6794
|
+
githubRepo: repo.html_url,
|
|
6795
|
+
stars: repo.stargazers_count
|
|
6796
|
+
}));
|
|
6797
|
+
} catch {
|
|
6798
|
+
return [];
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
async function fetchSource(source) {
|
|
6802
|
+
switch (source.type) {
|
|
6803
|
+
case "mcp-registry":
|
|
6804
|
+
return searchMcpRegistry(source, "");
|
|
6805
|
+
case "awesome-list":
|
|
6806
|
+
return searchAwesomeList(source, "");
|
|
6807
|
+
case "npm-search":
|
|
6808
|
+
return searchNpm(source, "");
|
|
6809
|
+
case "github-topic":
|
|
6810
|
+
return searchGitHubTopic(source, "");
|
|
6811
|
+
default:
|
|
6812
|
+
return [];
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
function filterResults(results, query) {
|
|
6816
|
+
if (!query)
|
|
6817
|
+
return results;
|
|
6818
|
+
const q = query.toLowerCase();
|
|
6819
|
+
return results.filter((r) => {
|
|
6820
|
+
const text = `${r.name} ${r.description || ""}`.toLowerCase();
|
|
6821
|
+
return text.includes(q);
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6824
|
+
async function searchSource(source, query, noCache = false) {
|
|
6825
|
+
if (!noCache) {
|
|
6826
|
+
const cached2 = readCache(source.id);
|
|
6827
|
+
if (cached2 && Date.now() - cached2.cachedAt < DEFAULT_TTL_MS) {
|
|
6828
|
+
return filterResults(cached2.results, query);
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
const results = await fetchSource(source);
|
|
6832
|
+
writeCache(source.id, results);
|
|
6833
|
+
return filterResults(results, query);
|
|
6834
|
+
}
|
|
6835
|
+
function deduplicate(results) {
|
|
6836
|
+
const seen = new Set;
|
|
6837
|
+
return results.filter((r) => {
|
|
6838
|
+
const key = r.npmPackage || r.githubRepo || r.name.toLowerCase();
|
|
6839
|
+
if (seen.has(key))
|
|
6840
|
+
return false;
|
|
6841
|
+
seen.add(key);
|
|
6842
|
+
return true;
|
|
6843
|
+
});
|
|
6844
|
+
}
|
|
6845
|
+
async function findServers(query, opts = {}) {
|
|
6846
|
+
let sources = listSources().filter((s) => s.enabled);
|
|
6847
|
+
if (opts.sources && opts.sources.length > 0) {
|
|
6848
|
+
sources = sources.filter((s) => opts.sources.includes(s.id));
|
|
6849
|
+
}
|
|
6850
|
+
const limit = opts.limit ?? 20;
|
|
6851
|
+
const all = await Promise.all(sources.map((s) => searchSource(s, query, opts.noCache)));
|
|
6852
|
+
const flat = all.flat();
|
|
6853
|
+
const deduped = deduplicate(flat);
|
|
6854
|
+
const typeOrder = {
|
|
6855
|
+
"mcp-registry": 0,
|
|
6856
|
+
"awesome-list": 1,
|
|
6857
|
+
"npm-search": 2,
|
|
6858
|
+
"github-topic": 3
|
|
6859
|
+
};
|
|
6860
|
+
const sourceTypeMap = new Map(sources.map((s) => [s.id, s.type]));
|
|
6861
|
+
deduped.sort((a, b) => {
|
|
6862
|
+
const aType = a.sourceId ? sourceTypeMap.get(a.sourceId) ?? "" : a.source;
|
|
6863
|
+
const bType = b.sourceId ? sourceTypeMap.get(b.sourceId) ?? "" : b.source;
|
|
6864
|
+
const ao = typeOrder[aType] ?? 99;
|
|
6865
|
+
const bo = typeOrder[bType] ?? 99;
|
|
6866
|
+
if (ao !== bo)
|
|
6867
|
+
return ao - bo;
|
|
6868
|
+
return (b.stars ?? 0) - (a.stars ?? 0);
|
|
6869
|
+
});
|
|
6870
|
+
return deduped.slice(0, limit * Math.max(sources.length, 1));
|
|
6871
|
+
}
|
|
6872
|
+
var CACHE_DIR, DEFAULT_TTL_MS;
|
|
6873
|
+
var init_sources = __esm(() => {
|
|
6874
|
+
init_db();
|
|
6875
|
+
init_config();
|
|
6876
|
+
CACHE_DIR = join2(MCPS_DIR, "cache");
|
|
6877
|
+
DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
6878
|
+
});
|
|
6879
|
+
|
|
6502
6880
|
// node_modules/isexe/windows.js
|
|
6503
6881
|
var require_windows = __commonJS((exports, module) => {
|
|
6504
6882
|
module.exports = isexe;
|
|
@@ -20023,86 +20401,91 @@ class StdioServerTransport {
|
|
|
20023
20401
|
}
|
|
20024
20402
|
}
|
|
20025
20403
|
|
|
20026
|
-
// src/
|
|
20027
|
-
import {
|
|
20028
|
-
import {
|
|
20029
|
-
|
|
20030
|
-
// src/lib/config.ts
|
|
20031
|
-
import { join } from "path";
|
|
20032
|
-
import { homedir } from "os";
|
|
20033
|
-
var MCPS_DIR = join(homedir(), ".mcps");
|
|
20034
|
-
var DB_PATH = join(MCPS_DIR, "registry.db");
|
|
20035
|
-
var REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
20036
|
-
var TOOL_PREFIX_SEPARATOR = "__";
|
|
20037
|
-
|
|
20038
|
-
// src/lib/db.ts
|
|
20039
|
-
var db = null;
|
|
20040
|
-
function getDb() {
|
|
20041
|
-
if (db)
|
|
20042
|
-
return db;
|
|
20043
|
-
mkdirSync(MCPS_DIR, { recursive: true });
|
|
20044
|
-
db = new Database(DB_PATH, { create: true });
|
|
20045
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
20046
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
20047
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
20048
|
-
db.exec(`
|
|
20049
|
-
CREATE TABLE IF NOT EXISTS servers (
|
|
20050
|
-
id TEXT PRIMARY KEY,
|
|
20051
|
-
name TEXT NOT NULL,
|
|
20052
|
-
description TEXT,
|
|
20053
|
-
command TEXT NOT NULL,
|
|
20054
|
-
args TEXT NOT NULL DEFAULT '[]',
|
|
20055
|
-
env TEXT NOT NULL DEFAULT '{}',
|
|
20056
|
-
transport TEXT NOT NULL DEFAULT 'stdio',
|
|
20057
|
-
url TEXT,
|
|
20058
|
-
source TEXT NOT NULL DEFAULT 'local',
|
|
20059
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
20060
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
20061
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
20062
|
-
)
|
|
20063
|
-
`);
|
|
20064
|
-
db.exec(`
|
|
20065
|
-
CREATE TABLE IF NOT EXISTS tool_cache (
|
|
20066
|
-
server_id TEXT NOT NULL,
|
|
20067
|
-
name TEXT NOT NULL,
|
|
20068
|
-
description TEXT NOT NULL DEFAULT '',
|
|
20069
|
-
input_schema TEXT NOT NULL DEFAULT '{}',
|
|
20070
|
-
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
20071
|
-
PRIMARY KEY (server_id, name),
|
|
20072
|
-
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
20073
|
-
)
|
|
20074
|
-
`);
|
|
20075
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
|
|
20076
|
-
return db;
|
|
20077
|
-
}
|
|
20404
|
+
// src/mcp/index.ts
|
|
20405
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
20406
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
20407
|
+
import { fileURLToPath } from "url";
|
|
20078
20408
|
|
|
20079
20409
|
// src/lib/registry.ts
|
|
20410
|
+
init_db();
|
|
20080
20411
|
function parseRow(row) {
|
|
20081
20412
|
return {
|
|
20082
20413
|
id: row.id,
|
|
20083
20414
|
name: row.name,
|
|
20084
20415
|
description: row.description || null,
|
|
20085
20416
|
command: row.command,
|
|
20086
|
-
args:
|
|
20087
|
-
env:
|
|
20417
|
+
args: safeJsonParse(row.args, []),
|
|
20418
|
+
env: safeJsonParse(row.env, {}),
|
|
20088
20419
|
transport: row.transport,
|
|
20089
20420
|
url: row.url || null,
|
|
20090
20421
|
source: row.source,
|
|
20091
20422
|
enabled: row.enabled === 1,
|
|
20092
20423
|
created_at: row.created_at,
|
|
20093
|
-
updated_at: row.updated_at
|
|
20424
|
+
updated_at: row.updated_at,
|
|
20425
|
+
last_connected_at: row.last_connected_at ?? null,
|
|
20426
|
+
last_error: row.last_error ?? null
|
|
20094
20427
|
};
|
|
20095
20428
|
}
|
|
20429
|
+
function safeJsonParse(value, fallback) {
|
|
20430
|
+
if (typeof value !== "string")
|
|
20431
|
+
return fallback;
|
|
20432
|
+
try {
|
|
20433
|
+
return JSON.parse(value);
|
|
20434
|
+
} catch {
|
|
20435
|
+
return fallback;
|
|
20436
|
+
}
|
|
20437
|
+
}
|
|
20096
20438
|
function generateId(name) {
|
|
20097
20439
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
20098
20440
|
}
|
|
20441
|
+
function normalizeCandidate(value) {
|
|
20442
|
+
const trimmed = value?.trim();
|
|
20443
|
+
return trimmed ? trimmed : undefined;
|
|
20444
|
+
}
|
|
20445
|
+
function pickNameFromArgs(args) {
|
|
20446
|
+
if (!args || args.length === 0)
|
|
20447
|
+
return;
|
|
20448
|
+
const ddIndex = args.indexOf("--");
|
|
20449
|
+
if (ddIndex >= 0 && ddIndex < args.length - 1) {
|
|
20450
|
+
const after = normalizeCandidate(args[ddIndex + 1]);
|
|
20451
|
+
if (after)
|
|
20452
|
+
return after;
|
|
20453
|
+
}
|
|
20454
|
+
for (const arg of args) {
|
|
20455
|
+
const candidate = normalizeCandidate(arg);
|
|
20456
|
+
if (!candidate)
|
|
20457
|
+
continue;
|
|
20458
|
+
if (candidate.startsWith("-"))
|
|
20459
|
+
continue;
|
|
20460
|
+
return candidate;
|
|
20461
|
+
}
|
|
20462
|
+
return;
|
|
20463
|
+
}
|
|
20464
|
+
function pickId(candidates) {
|
|
20465
|
+
for (const candidate of candidates) {
|
|
20466
|
+
if (!candidate)
|
|
20467
|
+
continue;
|
|
20468
|
+
const id = generateId(candidate);
|
|
20469
|
+
if (id)
|
|
20470
|
+
return id;
|
|
20471
|
+
}
|
|
20472
|
+
return null;
|
|
20473
|
+
}
|
|
20099
20474
|
function addServer(opts) {
|
|
20100
20475
|
const db2 = getDb();
|
|
20101
|
-
const
|
|
20102
|
-
|
|
20476
|
+
const command = normalizeCandidate(opts.command);
|
|
20477
|
+
if (!command) {
|
|
20478
|
+
throw new Error("Command is required");
|
|
20479
|
+
}
|
|
20480
|
+
const argName = pickNameFromArgs(opts.args);
|
|
20481
|
+
const name = normalizeCandidate(opts.name) || argName || command;
|
|
20482
|
+
const id = pickId([normalizeCandidate(opts.name), argName, command]) || null;
|
|
20483
|
+
if (!id) {
|
|
20484
|
+
throw new Error("Unable to generate a valid server ID");
|
|
20485
|
+
}
|
|
20103
20486
|
const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
|
|
20104
20487
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
20105
|
-
RETURNING *`).get(id, name, opts.description || null,
|
|
20488
|
+
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");
|
|
20106
20489
|
return parseRow(row);
|
|
20107
20490
|
}
|
|
20108
20491
|
function removeServer(id) {
|
|
@@ -20158,6 +20541,9 @@ function updateServer(id, updates) {
|
|
|
20158
20541
|
sets.push("updated_at = datetime('now')");
|
|
20159
20542
|
values.push(id);
|
|
20160
20543
|
const row = db2.prepare(`UPDATE servers SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...values);
|
|
20544
|
+
if (!row) {
|
|
20545
|
+
throw new Error(`Server "${id}" not found`);
|
|
20546
|
+
}
|
|
20161
20547
|
return parseRow(row);
|
|
20162
20548
|
}
|
|
20163
20549
|
function enableServer(id) {
|
|
@@ -20168,14 +20554,31 @@ function disableServer(id) {
|
|
|
20168
20554
|
}
|
|
20169
20555
|
function cacheTools(serverId, tools) {
|
|
20170
20556
|
const db2 = getDb();
|
|
20171
|
-
db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
|
|
20172
20557
|
const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
|
|
20558
|
+
const uniqueTools = [];
|
|
20559
|
+
const seen = new Set;
|
|
20173
20560
|
for (const tool of tools) {
|
|
20174
|
-
|
|
20561
|
+
const name = tool.name?.trim();
|
|
20562
|
+
if (!name || seen.has(name))
|
|
20563
|
+
continue;
|
|
20564
|
+
seen.add(name);
|
|
20565
|
+
uniqueTools.push({
|
|
20566
|
+
name,
|
|
20567
|
+
description: tool.description || "",
|
|
20568
|
+
input_schema: tool.input_schema || {}
|
|
20569
|
+
});
|
|
20175
20570
|
}
|
|
20571
|
+
const run = db2.transaction((rows) => {
|
|
20572
|
+
db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
|
|
20573
|
+
for (const tool of rows) {
|
|
20574
|
+
insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
|
|
20575
|
+
}
|
|
20576
|
+
});
|
|
20577
|
+
run(uniqueTools);
|
|
20176
20578
|
}
|
|
20177
20579
|
|
|
20178
20580
|
// src/lib/remote.ts
|
|
20581
|
+
init_config();
|
|
20179
20582
|
function parseRegistryEntry(entry) {
|
|
20180
20583
|
const s = entry.server;
|
|
20181
20584
|
return {
|
|
@@ -20236,6 +20639,102 @@ async function installFromRegistry(id) {
|
|
|
20236
20639
|
});
|
|
20237
20640
|
}
|
|
20238
20641
|
|
|
20642
|
+
// src/lib/finder.ts
|
|
20643
|
+
init_sources();
|
|
20644
|
+
async function listAwesomeServers() {
|
|
20645
|
+
const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
|
|
20646
|
+
const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
|
|
20647
|
+
if (!source)
|
|
20648
|
+
return [];
|
|
20649
|
+
return searchSource2(source, "");
|
|
20650
|
+
}
|
|
20651
|
+
|
|
20652
|
+
// src/mcp/index.ts
|
|
20653
|
+
init_sources();
|
|
20654
|
+
|
|
20655
|
+
// src/lib/install.ts
|
|
20656
|
+
import { execFileSync } from "child_process";
|
|
20657
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
20658
|
+
import { join as join3 } from "path";
|
|
20659
|
+
import { homedir as homedir2 } from "os";
|
|
20660
|
+
function installToClaude(entry) {
|
|
20661
|
+
try {
|
|
20662
|
+
const args = [
|
|
20663
|
+
"mcp",
|
|
20664
|
+
"add",
|
|
20665
|
+
"--transport",
|
|
20666
|
+
entry.transport,
|
|
20667
|
+
"--scope",
|
|
20668
|
+
"user"
|
|
20669
|
+
];
|
|
20670
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
20671
|
+
args.push("--env", `${k}=${v}`);
|
|
20672
|
+
}
|
|
20673
|
+
args.push(entry.id, "--", entry.command, ...entry.args);
|
|
20674
|
+
execFileSync("claude", args, { stdio: "pipe" });
|
|
20675
|
+
return { agent: "claude", success: true };
|
|
20676
|
+
} catch (err) {
|
|
20677
|
+
return { agent: "claude", success: false, error: err.message };
|
|
20678
|
+
}
|
|
20679
|
+
}
|
|
20680
|
+
function installToCodex(entry) {
|
|
20681
|
+
try {
|
|
20682
|
+
const configDir = join3(homedir2(), ".codex");
|
|
20683
|
+
const configPath = join3(configDir, "config.toml");
|
|
20684
|
+
if (!existsSync2(configDir)) {
|
|
20685
|
+
mkdirSync3(configDir, { recursive: true });
|
|
20686
|
+
}
|
|
20687
|
+
const block = `
|
|
20688
|
+
[mcp_servers.${entry.id}]
|
|
20689
|
+
` + `command = ${JSON.stringify(entry.command)}
|
|
20690
|
+
` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
|
|
20691
|
+
`;
|
|
20692
|
+
const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
|
|
20693
|
+
if (existing.includes(`[mcp_servers.${entry.id}]`)) {
|
|
20694
|
+
return { agent: "codex", success: true };
|
|
20695
|
+
}
|
|
20696
|
+
writeFileSync2(configPath, existing + block, "utf-8");
|
|
20697
|
+
return { agent: "codex", success: true };
|
|
20698
|
+
} catch (err) {
|
|
20699
|
+
return { agent: "codex", success: false, error: err.message };
|
|
20700
|
+
}
|
|
20701
|
+
}
|
|
20702
|
+
function installToGemini(entry) {
|
|
20703
|
+
try {
|
|
20704
|
+
const configDir = join3(homedir2(), ".gemini");
|
|
20705
|
+
const configPath = join3(configDir, "settings.json");
|
|
20706
|
+
if (!existsSync2(configDir)) {
|
|
20707
|
+
mkdirSync3(configDir, { recursive: true });
|
|
20708
|
+
}
|
|
20709
|
+
let settings = {};
|
|
20710
|
+
if (existsSync2(configPath)) {
|
|
20711
|
+
settings = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
20712
|
+
}
|
|
20713
|
+
if (!settings.mcpServers)
|
|
20714
|
+
settings.mcpServers = {};
|
|
20715
|
+
settings.mcpServers[entry.id] = {
|
|
20716
|
+
command: entry.command,
|
|
20717
|
+
args: entry.args,
|
|
20718
|
+
...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
|
|
20719
|
+
};
|
|
20720
|
+
writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
20721
|
+
return { agent: "gemini", success: true };
|
|
20722
|
+
} catch (err) {
|
|
20723
|
+
return { agent: "gemini", success: false, error: err.message };
|
|
20724
|
+
}
|
|
20725
|
+
}
|
|
20726
|
+
function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
|
|
20727
|
+
return targets.map((target) => {
|
|
20728
|
+
if (target === "claude")
|
|
20729
|
+
return installToClaude(entry);
|
|
20730
|
+
if (target === "codex")
|
|
20731
|
+
return installToCodex(entry);
|
|
20732
|
+
if (target === "gemini")
|
|
20733
|
+
return installToGemini(entry);
|
|
20734
|
+
return { agent: target, success: false, error: "Unknown target" };
|
|
20735
|
+
});
|
|
20736
|
+
}
|
|
20737
|
+
|
|
20239
20738
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/client.js
|
|
20240
20739
|
class ExperimentalClientTasks {
|
|
20241
20740
|
constructor(_client) {
|
|
@@ -22613,44 +23112,127 @@ class StreamableHTTPClientTransport {
|
|
|
22613
23112
|
}
|
|
22614
23113
|
|
|
22615
23114
|
// src/lib/proxy.ts
|
|
23115
|
+
init_config();
|
|
23116
|
+
init_db();
|
|
22616
23117
|
var connections = new Map;
|
|
23118
|
+
var inflightConnections = new Map;
|
|
23119
|
+
var CONNECT_CONCURRENCY = 4;
|
|
23120
|
+
function buildEnv(extra) {
|
|
23121
|
+
const merged = {};
|
|
23122
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
23123
|
+
if (typeof value === "string")
|
|
23124
|
+
merged[key] = value;
|
|
23125
|
+
}
|
|
23126
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
23127
|
+
if (value === undefined || value === null)
|
|
23128
|
+
continue;
|
|
23129
|
+
merged[key] = String(value);
|
|
23130
|
+
}
|
|
23131
|
+
return merged;
|
|
23132
|
+
}
|
|
23133
|
+
function requireUrl(entry) {
|
|
23134
|
+
if (!entry.url) {
|
|
23135
|
+
throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
|
|
23136
|
+
}
|
|
23137
|
+
try {
|
|
23138
|
+
return new URL(entry.url);
|
|
23139
|
+
} catch {
|
|
23140
|
+
throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
|
|
23141
|
+
}
|
|
23142
|
+
}
|
|
22617
23143
|
async function connectToServer(entry) {
|
|
22618
23144
|
if (connections.has(entry.id)) {
|
|
22619
23145
|
return connections.get(entry.id);
|
|
22620
23146
|
}
|
|
23147
|
+
const inflight = inflightConnections.get(entry.id);
|
|
23148
|
+
if (inflight) {
|
|
23149
|
+
return inflight;
|
|
23150
|
+
}
|
|
22621
23151
|
const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
|
|
22622
23152
|
let transport;
|
|
22623
|
-
|
|
22624
|
-
|
|
22625
|
-
|
|
22626
|
-
|
|
22627
|
-
|
|
22628
|
-
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
23153
|
+
const connectPromise = (async () => {
|
|
23154
|
+
try {
|
|
23155
|
+
if (entry.transport === "stdio") {
|
|
23156
|
+
if (!entry.command?.trim()) {
|
|
23157
|
+
throw new Error(`Server "${entry.id}" is missing a command`);
|
|
23158
|
+
}
|
|
23159
|
+
transport = new StdioClientTransport({
|
|
23160
|
+
command: entry.command,
|
|
23161
|
+
args: entry.args,
|
|
23162
|
+
env: buildEnv(entry.env)
|
|
23163
|
+
});
|
|
23164
|
+
} else if (entry.transport === "sse") {
|
|
23165
|
+
transport = new SSEClientTransport(requireUrl(entry));
|
|
23166
|
+
} else {
|
|
23167
|
+
transport = new StreamableHTTPClientTransport(requireUrl(entry));
|
|
23168
|
+
}
|
|
23169
|
+
await client.connect(transport);
|
|
23170
|
+
const result = await client.listTools();
|
|
23171
|
+
const tools = (result.tools || []).map((t) => ({
|
|
23172
|
+
server_id: entry.id,
|
|
23173
|
+
name: t.name,
|
|
23174
|
+
description: t.description || "",
|
|
23175
|
+
input_schema: t.inputSchema || {}
|
|
23176
|
+
}));
|
|
23177
|
+
cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
|
|
23178
|
+
const connected = {
|
|
23179
|
+
entry,
|
|
23180
|
+
tools,
|
|
23181
|
+
disconnect: async () => {
|
|
23182
|
+
try {
|
|
23183
|
+
await client.close();
|
|
23184
|
+
} finally {
|
|
23185
|
+
connections.delete(entry.id);
|
|
23186
|
+
}
|
|
23187
|
+
}
|
|
23188
|
+
};
|
|
23189
|
+
connected._client = client;
|
|
23190
|
+
connections.set(entry.id, connected);
|
|
23191
|
+
try {
|
|
23192
|
+
getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
|
|
23193
|
+
} catch {}
|
|
23194
|
+
return connected;
|
|
23195
|
+
} catch (err) {
|
|
23196
|
+
try {
|
|
23197
|
+
getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
|
|
23198
|
+
} catch {}
|
|
23199
|
+
try {
|
|
23200
|
+
await client.close();
|
|
23201
|
+
} catch {}
|
|
23202
|
+
throw err;
|
|
22649
23203
|
}
|
|
22650
|
-
};
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
23204
|
+
})();
|
|
23205
|
+
inflightConnections.set(entry.id, connectPromise);
|
|
23206
|
+
try {
|
|
23207
|
+
return await connectPromise;
|
|
23208
|
+
} finally {
|
|
23209
|
+
inflightConnections.delete(entry.id);
|
|
23210
|
+
}
|
|
23211
|
+
}
|
|
23212
|
+
async function disconnectServer(id) {
|
|
23213
|
+
const conn = connections.get(id);
|
|
23214
|
+
if (conn) {
|
|
23215
|
+
await conn.disconnect();
|
|
23216
|
+
return;
|
|
23217
|
+
}
|
|
23218
|
+
const inflight = inflightConnections.get(id);
|
|
23219
|
+
if (inflight) {
|
|
23220
|
+
try {
|
|
23221
|
+
const pending = await inflight;
|
|
23222
|
+
await pending.disconnect();
|
|
23223
|
+
} catch {}
|
|
23224
|
+
}
|
|
23225
|
+
}
|
|
23226
|
+
async function disconnectAll() {
|
|
23227
|
+
const ids = Array.from(connections.keys());
|
|
23228
|
+
await Promise.allSettled(ids.map((id) => disconnectServer(id)));
|
|
23229
|
+
const inflight = Array.from(inflightConnections.values());
|
|
23230
|
+
await Promise.allSettled(inflight.map(async (promise2) => {
|
|
23231
|
+
try {
|
|
23232
|
+
const conn = await promise2;
|
|
23233
|
+
await conn.disconnect();
|
|
23234
|
+
} catch {}
|
|
23235
|
+
}));
|
|
22654
23236
|
}
|
|
22655
23237
|
function listAllTools() {
|
|
22656
23238
|
const tools = [];
|
|
@@ -22665,12 +23247,19 @@ function listAllTools() {
|
|
|
22665
23247
|
return tools;
|
|
22666
23248
|
}
|
|
22667
23249
|
async function callTool(prefixedName, args) {
|
|
22668
|
-
const
|
|
23250
|
+
const normalized = prefixedName.trim();
|
|
23251
|
+
if (!normalized) {
|
|
23252
|
+
throw new Error("Tool name is required");
|
|
23253
|
+
}
|
|
23254
|
+
const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
22669
23255
|
if (sepIdx === -1) {
|
|
22670
23256
|
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
22671
23257
|
}
|
|
22672
|
-
const serverId =
|
|
22673
|
-
const toolName =
|
|
23258
|
+
const serverId = normalized.slice(0, sepIdx).trim();
|
|
23259
|
+
const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
|
|
23260
|
+
if (!serverId || !toolName) {
|
|
23261
|
+
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
23262
|
+
}
|
|
22674
23263
|
const conn = connections.get(serverId);
|
|
22675
23264
|
if (!conn) {
|
|
22676
23265
|
throw new Error(`Server "${serverId}" is not connected`);
|
|
@@ -22686,26 +23275,97 @@ async function callTool(prefixedName, args) {
|
|
|
22686
23275
|
async function connectAllEnabled() {
|
|
22687
23276
|
const servers = listServers().filter((s) => s.enabled);
|
|
22688
23277
|
const results = [];
|
|
22689
|
-
|
|
23278
|
+
let index = 0;
|
|
23279
|
+
const workerCount = Math.min(CONNECT_CONCURRENCY, servers.length);
|
|
23280
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
23281
|
+
while (true) {
|
|
23282
|
+
const current = index++;
|
|
23283
|
+
if (current >= servers.length)
|
|
23284
|
+
return;
|
|
23285
|
+
const server = servers[current];
|
|
23286
|
+
try {
|
|
23287
|
+
const conn = await connectToServer(server);
|
|
23288
|
+
results.push(conn);
|
|
23289
|
+
} catch (err) {
|
|
23290
|
+
console.error(`Failed to connect to ${server.name}: ${err.message}`);
|
|
23291
|
+
}
|
|
23292
|
+
}
|
|
23293
|
+
});
|
|
23294
|
+
await Promise.all(workers);
|
|
23295
|
+
return results;
|
|
23296
|
+
}
|
|
23297
|
+
|
|
23298
|
+
// src/lib/doctor.ts
|
|
23299
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
23300
|
+
async function diagnoseServer(server) {
|
|
23301
|
+
const checks4 = [];
|
|
23302
|
+
if (server.transport === "stdio") {
|
|
22690
23303
|
try {
|
|
22691
|
-
|
|
22692
|
-
|
|
23304
|
+
execFileSync2("which", [server.command], { stdio: "pipe" });
|
|
23305
|
+
checks4.push({ name: "command on PATH", pass: true, message: `${server.command} found` });
|
|
23306
|
+
} catch {
|
|
23307
|
+
checks4.push({ name: "command on PATH", pass: false, message: `${server.command} not found on PATH` });
|
|
23308
|
+
}
|
|
23309
|
+
}
|
|
23310
|
+
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
23311
|
+
if (Object.keys(server.env).length === 0) {
|
|
23312
|
+
checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
|
|
23313
|
+
} else if (missingEnv.length > 0) {
|
|
23314
|
+
checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
|
|
23315
|
+
} else {
|
|
23316
|
+
checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
|
|
23317
|
+
}
|
|
23318
|
+
if (server.transport !== "stdio" && server.url) {
|
|
23319
|
+
try {
|
|
23320
|
+
const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
|
|
23321
|
+
checks4.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
|
|
22693
23322
|
} catch (err) {
|
|
22694
|
-
|
|
23323
|
+
checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
|
|
22695
23324
|
}
|
|
22696
23325
|
}
|
|
22697
|
-
|
|
23326
|
+
if (server.enabled) {
|
|
23327
|
+
try {
|
|
23328
|
+
await Promise.race([
|
|
23329
|
+
connectToServer(server),
|
|
23330
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
|
|
23331
|
+
]);
|
|
23332
|
+
await disconnectServer(server.id);
|
|
23333
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
|
|
23334
|
+
} catch (err) {
|
|
23335
|
+
checks4.push({ name: "connect & list tools", pass: false, message: err.message });
|
|
23336
|
+
}
|
|
23337
|
+
} else {
|
|
23338
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
|
|
23339
|
+
}
|
|
23340
|
+
return {
|
|
23341
|
+
server,
|
|
23342
|
+
checks: checks4,
|
|
23343
|
+
healthy: checks4.every((c) => c.pass)
|
|
23344
|
+
};
|
|
22698
23345
|
}
|
|
22699
23346
|
|
|
22700
23347
|
// src/mcp/index.ts
|
|
23348
|
+
init_config();
|
|
23349
|
+
function redactServerEnv(server) {
|
|
23350
|
+
return { ...server, env: {} };
|
|
23351
|
+
}
|
|
23352
|
+
var VERSION = (() => {
|
|
23353
|
+
try {
|
|
23354
|
+
const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
23355
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
23356
|
+
return pkg.version || "0.0.1";
|
|
23357
|
+
} catch {
|
|
23358
|
+
return "0.0.1";
|
|
23359
|
+
}
|
|
23360
|
+
})();
|
|
22701
23361
|
var server = new McpServer({
|
|
22702
23362
|
name: "mcps",
|
|
22703
|
-
version:
|
|
23363
|
+
version: VERSION
|
|
22704
23364
|
});
|
|
22705
23365
|
server.tool("list_servers", "List all registered MCP servers", {}, async () => {
|
|
22706
23366
|
const servers = listServers();
|
|
22707
23367
|
return {
|
|
22708
|
-
content: [{ type: "text", text: JSON.stringify(servers, null, 2) }]
|
|
23368
|
+
content: [{ type: "text", text: JSON.stringify(servers.map(redactServerEnv), null, 2) }]
|
|
22709
23369
|
};
|
|
22710
23370
|
});
|
|
22711
23371
|
server.tool("search_registry", "Search the official MCP registry for servers", { query: exports_external.string().describe("Search query") }, async ({ query }) => {
|
|
@@ -22756,12 +23416,26 @@ server.tool("remove_server", "Remove a registered MCP server", { id: exports_ext
|
|
|
22756
23416
|
};
|
|
22757
23417
|
});
|
|
22758
23418
|
server.tool("enable_server", "Enable a registered MCP server", { id: exports_external.string().describe("Server ID to enable") }, async ({ id }) => {
|
|
23419
|
+
const existing = getServer(id);
|
|
23420
|
+
if (!existing) {
|
|
23421
|
+
return {
|
|
23422
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
23423
|
+
isError: true
|
|
23424
|
+
};
|
|
23425
|
+
}
|
|
22759
23426
|
const entry = enableServer(id);
|
|
22760
23427
|
return {
|
|
22761
23428
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
22762
23429
|
};
|
|
22763
23430
|
});
|
|
22764
23431
|
server.tool("disable_server", "Disable a registered MCP server", { id: exports_external.string().describe("Server ID to disable") }, async ({ id }) => {
|
|
23432
|
+
const existing = getServer(id);
|
|
23433
|
+
if (!existing) {
|
|
23434
|
+
return {
|
|
23435
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
23436
|
+
isError: true
|
|
23437
|
+
};
|
|
23438
|
+
}
|
|
22765
23439
|
const entry = disableServer(id);
|
|
22766
23440
|
return {
|
|
22767
23441
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
@@ -22776,12 +23450,108 @@ server.tool("get_server_info", "Get detailed information about a registered MCP
|
|
|
22776
23450
|
};
|
|
22777
23451
|
}
|
|
22778
23452
|
return {
|
|
22779
|
-
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
23453
|
+
content: [{ type: "text", text: JSON.stringify(redactServerEnv(entry), null, 2) }]
|
|
23454
|
+
};
|
|
23455
|
+
});
|
|
23456
|
+
server.tool("find_mcp_servers", "Search for MCP servers across configured sources (official registry, npm, GitHub topics, awesome lists). Use list_sources to see available source IDs.", {
|
|
23457
|
+
query: exports_external.string().describe("Search query (e.g. 'filesystem', 'postgres', 'browser')"),
|
|
23458
|
+
sources: exports_external.array(exports_external.string()).optional().describe("Source IDs to search (default: all enabled). Use list_sources to get IDs."),
|
|
23459
|
+
limit: exports_external.number().optional().describe("Max results per source (default: 20)")
|
|
23460
|
+
}, async ({ query, sources, limit }) => {
|
|
23461
|
+
const results = await findServers(query, { sources, limit });
|
|
23462
|
+
return {
|
|
23463
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
23464
|
+
};
|
|
23465
|
+
});
|
|
23466
|
+
server.tool("list_sources", "List all configured search sources for finding MCP servers", {}, async () => {
|
|
23467
|
+
const sources = listSources();
|
|
23468
|
+
return {
|
|
23469
|
+
content: [{ type: "text", text: JSON.stringify(sources, null, 2) }]
|
|
23470
|
+
};
|
|
23471
|
+
});
|
|
23472
|
+
server.tool("add_source", "Add a new search source for finding MCP servers", {
|
|
23473
|
+
name: exports_external.string().describe("Source name"),
|
|
23474
|
+
type: exports_external.enum(["mcp-registry", "awesome-list", "npm-search", "github-topic"]).describe("Source type"),
|
|
23475
|
+
url: exports_external.string().describe("Source URL endpoint"),
|
|
23476
|
+
description: exports_external.string().optional().describe("Description")
|
|
23477
|
+
}, async ({ name, type, url: url2, description }) => {
|
|
23478
|
+
const source = addSource({ name, type, url: url2, description });
|
|
23479
|
+
return {
|
|
23480
|
+
content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
|
|
23481
|
+
};
|
|
23482
|
+
});
|
|
23483
|
+
server.tool("remove_source", "Remove a search source by ID", { id: exports_external.string().describe("Source ID to remove") }, async ({ id }) => {
|
|
23484
|
+
const existing = getSource(id);
|
|
23485
|
+
if (!existing) {
|
|
23486
|
+
return {
|
|
23487
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
23488
|
+
isError: true
|
|
23489
|
+
};
|
|
23490
|
+
}
|
|
23491
|
+
removeSource(id);
|
|
23492
|
+
return {
|
|
23493
|
+
content: [{ type: "text", text: `Removed source: ${existing.name} [${id}]` }]
|
|
23494
|
+
};
|
|
23495
|
+
});
|
|
23496
|
+
server.tool("enable_source_finder", "Enable a search source", { id: exports_external.string().describe("Source ID to enable") }, async ({ id }) => {
|
|
23497
|
+
const existing = getSource(id);
|
|
23498
|
+
if (!existing) {
|
|
23499
|
+
return {
|
|
23500
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
23501
|
+
isError: true
|
|
23502
|
+
};
|
|
23503
|
+
}
|
|
23504
|
+
enableSource(id);
|
|
23505
|
+
return {
|
|
23506
|
+
content: [{ type: "text", text: `Enabled source: ${existing.name}` }]
|
|
23507
|
+
};
|
|
23508
|
+
});
|
|
23509
|
+
server.tool("disable_source_finder", "Disable a search source", { id: exports_external.string().describe("Source ID to disable") }, async ({ id }) => {
|
|
23510
|
+
const existing = getSource(id);
|
|
23511
|
+
if (!existing) {
|
|
23512
|
+
return {
|
|
23513
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
23514
|
+
isError: true
|
|
23515
|
+
};
|
|
23516
|
+
}
|
|
23517
|
+
disableSource(id);
|
|
23518
|
+
return {
|
|
23519
|
+
content: [{ type: "text", text: `Disabled source: ${existing.name}` }]
|
|
23520
|
+
};
|
|
23521
|
+
});
|
|
23522
|
+
server.tool("install_to_agents", "Install a registered MCP server into Claude Code, Codex, and/or Gemini", {
|
|
23523
|
+
id: exports_external.string().describe("Server ID to install (from list_servers)"),
|
|
23524
|
+
targets: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
|
|
23525
|
+
}, async ({ id, targets }) => {
|
|
23526
|
+
const entry = getServer(id);
|
|
23527
|
+
if (!entry) {
|
|
23528
|
+
return {
|
|
23529
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
23530
|
+
isError: true
|
|
23531
|
+
};
|
|
23532
|
+
}
|
|
23533
|
+
const agentTargets = targets ?? ["claude", "codex", "gemini"];
|
|
23534
|
+
const results = installToAgents(entry, agentTargets);
|
|
23535
|
+
return {
|
|
23536
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
23537
|
+
};
|
|
23538
|
+
});
|
|
23539
|
+
server.tool("list_awesome_servers", "List all MCP servers from the curated punkpeye/awesome-mcp-servers GitHub list", {}, async () => {
|
|
23540
|
+
const results = await listAwesomeServers();
|
|
23541
|
+
return {
|
|
23542
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
22780
23543
|
};
|
|
22781
23544
|
});
|
|
22782
23545
|
server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
|
|
22783
|
-
|
|
22784
|
-
|
|
23546
|
+
let tools = [];
|
|
23547
|
+
try {
|
|
23548
|
+
await connectAllEnabled();
|
|
23549
|
+
tools = listAllTools();
|
|
23550
|
+
} finally {
|
|
23551
|
+
await disconnectAll().catch(() => {
|
|
23552
|
+
return;
|
|
23553
|
+
});
|
|
23554
|
+
}
|
|
22785
23555
|
return {
|
|
22786
23556
|
content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
|
|
22787
23557
|
};
|
|
@@ -22791,6 +23561,28 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
|
|
|
22791
23561
|
arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
|
|
22792
23562
|
}, async ({ tool_name, arguments: args }) => {
|
|
22793
23563
|
try {
|
|
23564
|
+
const sepIdx = tool_name.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
23565
|
+
if (sepIdx === -1) {
|
|
23566
|
+
return {
|
|
23567
|
+
content: [{ type: "text", text: `Error: Invalid tool name "${tool_name}"` }],
|
|
23568
|
+
isError: true
|
|
23569
|
+
};
|
|
23570
|
+
}
|
|
23571
|
+
const serverId = tool_name.slice(0, sepIdx);
|
|
23572
|
+
const entry = getServer(serverId);
|
|
23573
|
+
if (!entry) {
|
|
23574
|
+
return {
|
|
23575
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" not found.` }],
|
|
23576
|
+
isError: true
|
|
23577
|
+
};
|
|
23578
|
+
}
|
|
23579
|
+
if (!entry.enabled) {
|
|
23580
|
+
return {
|
|
23581
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" is disabled.` }],
|
|
23582
|
+
isError: true
|
|
23583
|
+
};
|
|
23584
|
+
}
|
|
23585
|
+
await connectToServer(entry);
|
|
22794
23586
|
const result = await callTool(tool_name, args || {});
|
|
22795
23587
|
return { content: result.content };
|
|
22796
23588
|
} catch (err) {
|
|
@@ -22800,6 +23592,13 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
|
|
|
22800
23592
|
};
|
|
22801
23593
|
}
|
|
22802
23594
|
});
|
|
23595
|
+
server.tool("diagnose_server", "Run health checks on a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
|
|
23596
|
+
const entry = getServer(id);
|
|
23597
|
+
if (!entry)
|
|
23598
|
+
return { content: [{ type: "text", text: `Server "${id}" not found.` }], isError: true };
|
|
23599
|
+
const report = await diagnoseServer(entry);
|
|
23600
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
23601
|
+
});
|
|
22803
23602
|
async function startMcpServer() {
|
|
22804
23603
|
const transport = new StdioServerTransport;
|
|
22805
23604
|
await server.connect(transport);
|