@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/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var __export = (target, all) => {
|
|
|
26
26
|
set: (newValue) => all[name] = () => newValue
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
30
|
var __require = import.meta.require;
|
|
30
31
|
|
|
31
32
|
// node_modules/commander/lib/error.js
|
|
@@ -1867,6 +1868,94 @@ var require_commander = __commonJS((exports) => {
|
|
|
1867
1868
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
1868
1869
|
});
|
|
1869
1870
|
|
|
1871
|
+
// src/lib/config.ts
|
|
1872
|
+
import { join } from "path";
|
|
1873
|
+
import { homedir } from "os";
|
|
1874
|
+
var MCPS_DIR, DB_PATH, REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers", TOOL_PREFIX_SEPARATOR = "__";
|
|
1875
|
+
var init_config = __esm(() => {
|
|
1876
|
+
MCPS_DIR = join(homedir(), ".mcps");
|
|
1877
|
+
DB_PATH = join(MCPS_DIR, "registry.db");
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// src/lib/db.ts
|
|
1881
|
+
import { Database } from "bun:sqlite";
|
|
1882
|
+
import { mkdirSync } from "fs";
|
|
1883
|
+
function getDb() {
|
|
1884
|
+
if (db)
|
|
1885
|
+
return db;
|
|
1886
|
+
mkdirSync(MCPS_DIR, { recursive: true });
|
|
1887
|
+
db = new Database(DB_PATH, { create: true });
|
|
1888
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
1889
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
1890
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
1891
|
+
db.exec(`
|
|
1892
|
+
CREATE TABLE IF NOT EXISTS servers (
|
|
1893
|
+
id TEXT PRIMARY KEY,
|
|
1894
|
+
name TEXT NOT NULL,
|
|
1895
|
+
description TEXT,
|
|
1896
|
+
command TEXT NOT NULL,
|
|
1897
|
+
args TEXT NOT NULL DEFAULT '[]',
|
|
1898
|
+
env TEXT NOT NULL DEFAULT '{}',
|
|
1899
|
+
transport TEXT NOT NULL DEFAULT 'stdio',
|
|
1900
|
+
url TEXT,
|
|
1901
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
1902
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1903
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1904
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1905
|
+
)
|
|
1906
|
+
`);
|
|
1907
|
+
db.exec(`
|
|
1908
|
+
CREATE TABLE IF NOT EXISTS tool_cache (
|
|
1909
|
+
server_id TEXT NOT NULL,
|
|
1910
|
+
name TEXT NOT NULL,
|
|
1911
|
+
description TEXT NOT NULL DEFAULT '',
|
|
1912
|
+
input_schema TEXT NOT NULL DEFAULT '{}',
|
|
1913
|
+
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1914
|
+
PRIMARY KEY (server_id, name),
|
|
1915
|
+
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
1916
|
+
)
|
|
1917
|
+
`);
|
|
1918
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
|
|
1919
|
+
try {
|
|
1920
|
+
db.exec("ALTER TABLE servers ADD COLUMN last_connected_at TEXT");
|
|
1921
|
+
} catch {}
|
|
1922
|
+
try {
|
|
1923
|
+
db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
|
|
1924
|
+
} catch {}
|
|
1925
|
+
db.exec(`
|
|
1926
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
1927
|
+
id TEXT PRIMARY KEY,
|
|
1928
|
+
name TEXT NOT NULL,
|
|
1929
|
+
type TEXT NOT NULL,
|
|
1930
|
+
url TEXT NOT NULL,
|
|
1931
|
+
description TEXT,
|
|
1932
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1933
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1934
|
+
)
|
|
1935
|
+
`);
|
|
1936
|
+
const count = db.query("SELECT COUNT(*) as c FROM sources").get().c;
|
|
1937
|
+
if (count === 0) {
|
|
1938
|
+
db.exec(`
|
|
1939
|
+
INSERT OR IGNORE INTO sources (id, name, type, url, description) VALUES
|
|
1940
|
+
('official-registry', 'Official MCP Registry', 'mcp-registry', 'https://registry.modelcontextprotocol.io/v0/servers', 'The official Model Context Protocol server registry'),
|
|
1941
|
+
('awesome-mcp-servers', 'Awesome MCP Servers', 'awesome-list', 'https://raw.githubusercontent.com/punkpeye/awesome-mcp-servers/main/README.md', 'Curated list of MCP servers by punkpeye'),
|
|
1942
|
+
('npm-mcp', 'npm MCP Packages', 'npm-search', 'https://registry.npmjs.org/-/v1/search', 'Search npm packages for MCP servers'),
|
|
1943
|
+
('github-mcp-topic', 'GitHub MCP Topic', 'github-topic', 'https://api.github.com/search/repositories', 'GitHub repositories tagged with mcp-server topic')
|
|
1944
|
+
`);
|
|
1945
|
+
}
|
|
1946
|
+
return db;
|
|
1947
|
+
}
|
|
1948
|
+
function closeDb() {
|
|
1949
|
+
if (db) {
|
|
1950
|
+
db.close();
|
|
1951
|
+
db = null;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
var db = null;
|
|
1955
|
+
var init_db = __esm(() => {
|
|
1956
|
+
init_config();
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1870
1959
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
1871
1960
|
var require_code = __commonJS((exports) => {
|
|
1872
1961
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -8126,7 +8215,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
8126
8215
|
}
|
|
8127
8216
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
8128
8217
|
function getTime(strictTimeZone) {
|
|
8129
|
-
return function
|
|
8218
|
+
return function time(str) {
|
|
8130
8219
|
const matches = TIME.exec(str);
|
|
8131
8220
|
if (!matches)
|
|
8132
8221
|
return false;
|
|
@@ -8795,6 +8884,361 @@ var require_cross_spawn = __commonJS((exports, module) => {
|
|
|
8795
8884
|
module.exports._enoent = enoent;
|
|
8796
8885
|
});
|
|
8797
8886
|
|
|
8887
|
+
// src/lib/sources.ts
|
|
8888
|
+
var exports_sources = {};
|
|
8889
|
+
__export(exports_sources, {
|
|
8890
|
+
searchSource: () => searchSource,
|
|
8891
|
+
removeSource: () => removeSource,
|
|
8892
|
+
listSources: () => listSources,
|
|
8893
|
+
getSource: () => getSource,
|
|
8894
|
+
findServers: () => findServers,
|
|
8895
|
+
enableSource: () => enableSource,
|
|
8896
|
+
disableSource: () => disableSource,
|
|
8897
|
+
clearCache: () => clearCache,
|
|
8898
|
+
addSource: () => addSource
|
|
8899
|
+
});
|
|
8900
|
+
import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
|
|
8901
|
+
import { join as join2 } from "path";
|
|
8902
|
+
function getCacheFile(sourceId) {
|
|
8903
|
+
return join2(CACHE_DIR, `${sourceId}.json`);
|
|
8904
|
+
}
|
|
8905
|
+
function readCache(sourceId) {
|
|
8906
|
+
try {
|
|
8907
|
+
const file = getCacheFile(sourceId);
|
|
8908
|
+
if (!existsSync(file))
|
|
8909
|
+
return null;
|
|
8910
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
8911
|
+
return data;
|
|
8912
|
+
} catch {
|
|
8913
|
+
return null;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
function writeCache(sourceId, results) {
|
|
8917
|
+
try {
|
|
8918
|
+
mkdirSync2(CACHE_DIR, { recursive: true });
|
|
8919
|
+
writeFileSync(getCacheFile(sourceId), JSON.stringify({ results, cachedAt: Date.now() }), "utf-8");
|
|
8920
|
+
} catch {}
|
|
8921
|
+
}
|
|
8922
|
+
function clearCache(sourceId) {
|
|
8923
|
+
try {
|
|
8924
|
+
if (!existsSync(CACHE_DIR))
|
|
8925
|
+
return;
|
|
8926
|
+
const files = readdirSync(CACHE_DIR);
|
|
8927
|
+
for (const file of files) {
|
|
8928
|
+
if (!file.endsWith(".json"))
|
|
8929
|
+
continue;
|
|
8930
|
+
if (!sourceId || file.startsWith(`${sourceId}.`)) {
|
|
8931
|
+
try {
|
|
8932
|
+
unlinkSync(join2(CACHE_DIR, file));
|
|
8933
|
+
} catch {}
|
|
8934
|
+
}
|
|
8935
|
+
}
|
|
8936
|
+
} catch {}
|
|
8937
|
+
}
|
|
8938
|
+
function generateId2(name) {
|
|
8939
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8940
|
+
}
|
|
8941
|
+
function listSources() {
|
|
8942
|
+
const db2 = getDb();
|
|
8943
|
+
const rows = db2.query("SELECT * FROM sources ORDER BY created_at ASC").all();
|
|
8944
|
+
return rows.map((r) => ({ ...r, enabled: r.enabled === 1 }));
|
|
8945
|
+
}
|
|
8946
|
+
function getSource(id) {
|
|
8947
|
+
const db2 = getDb();
|
|
8948
|
+
const row = db2.query("SELECT * FROM sources WHERE id = ?").get(id);
|
|
8949
|
+
if (!row)
|
|
8950
|
+
return null;
|
|
8951
|
+
return { ...row, enabled: row.enabled === 1 };
|
|
8952
|
+
}
|
|
8953
|
+
function addSource(opts) {
|
|
8954
|
+
const db2 = getDb();
|
|
8955
|
+
const id = generateId2(opts.name);
|
|
8956
|
+
db2.run("INSERT INTO sources (id, name, type, url, description) VALUES (?, ?, ?, ?, ?)", [id, opts.name, opts.type, opts.url, opts.description ?? null]);
|
|
8957
|
+
return getSource(id);
|
|
8958
|
+
}
|
|
8959
|
+
function removeSource(id) {
|
|
8960
|
+
const db2 = getDb();
|
|
8961
|
+
db2.run("DELETE FROM sources WHERE id = ?", [id]);
|
|
8962
|
+
}
|
|
8963
|
+
function enableSource(id) {
|
|
8964
|
+
const db2 = getDb();
|
|
8965
|
+
db2.run("UPDATE sources SET enabled = 1 WHERE id = ?", [id]);
|
|
8966
|
+
}
|
|
8967
|
+
function disableSource(id) {
|
|
8968
|
+
const db2 = getDb();
|
|
8969
|
+
db2.run("UPDATE sources SET enabled = 0 WHERE id = ?", [id]);
|
|
8970
|
+
}
|
|
8971
|
+
async function searchMcpRegistry(source, query) {
|
|
8972
|
+
try {
|
|
8973
|
+
const res = await fetch(source.url);
|
|
8974
|
+
if (!res.ok)
|
|
8975
|
+
return [];
|
|
8976
|
+
const data = await res.json();
|
|
8977
|
+
const q = query.toLowerCase();
|
|
8978
|
+
return (data.servers || []).map((e) => e.server).filter((s) => q === "" || s.name.toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q)).map((s) => ({
|
|
8979
|
+
name: s.name,
|
|
8980
|
+
description: s.description || "",
|
|
8981
|
+
source: "registry",
|
|
8982
|
+
sourceId: source.id,
|
|
8983
|
+
url: s.repository?.url,
|
|
8984
|
+
githubRepo: s.repository?.url,
|
|
8985
|
+
installCmd: s.packages?.[0] ? s.packages[0].registryType === "npm" ? `npx -y ${s.packages[0].identifier}` : s.packages[0].identifier : undefined,
|
|
8986
|
+
npmPackage: s.packages?.find((p) => p.registryType === "npm")?.identifier
|
|
8987
|
+
}));
|
|
8988
|
+
} catch {
|
|
8989
|
+
return [];
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
async function searchAwesomeList(source, query) {
|
|
8993
|
+
try {
|
|
8994
|
+
const res = await fetch(source.url);
|
|
8995
|
+
if (!res.ok)
|
|
8996
|
+
return [];
|
|
8997
|
+
const text = await res.text();
|
|
8998
|
+
const results = [];
|
|
8999
|
+
const linkPattern = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)(?:\s*[-\u2013]\s*([^\n]+))?/g;
|
|
9000
|
+
const q = query.toLowerCase();
|
|
9001
|
+
let match;
|
|
9002
|
+
while ((match = linkPattern.exec(text)) !== null) {
|
|
9003
|
+
const name = match[1].trim();
|
|
9004
|
+
const url2 = match[2].trim();
|
|
9005
|
+
const description = match[3]?.trim() || "";
|
|
9006
|
+
if ((url2.includes("github.com") || url2.includes("npmjs.com")) && name.length > 2) {
|
|
9007
|
+
if (q === "" || `${name} ${description}`.toLowerCase().includes(q)) {
|
|
9008
|
+
results.push({
|
|
9009
|
+
name,
|
|
9010
|
+
description,
|
|
9011
|
+
source: "awesome",
|
|
9012
|
+
sourceId: source.id,
|
|
9013
|
+
url: url2,
|
|
9014
|
+
githubRepo: url2.includes("github.com") ? url2 : undefined,
|
|
9015
|
+
installCmd: url2.includes("npmjs.com") ? `npx -y ${url2.split("/").pop()}` : undefined
|
|
9016
|
+
});
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
return results;
|
|
9021
|
+
} catch {
|
|
9022
|
+
return [];
|
|
9023
|
+
}
|
|
9024
|
+
}
|
|
9025
|
+
function scoreNpmPackage(pkg) {
|
|
9026
|
+
const name = pkg.name.toLowerCase();
|
|
9027
|
+
const keywords = (pkg.keywords || []).map((k) => k.toLowerCase());
|
|
9028
|
+
let score = 0;
|
|
9029
|
+
if (name.includes("mcp-server"))
|
|
9030
|
+
score += 3;
|
|
9031
|
+
else if (name.startsWith("mcp-") || name.endsWith("-mcp"))
|
|
9032
|
+
score += 2;
|
|
9033
|
+
else if (name.includes("mcp"))
|
|
9034
|
+
score += 1;
|
|
9035
|
+
if (keywords.includes("mcp"))
|
|
9036
|
+
score += 1;
|
|
9037
|
+
if (keywords.includes("mcp-server"))
|
|
9038
|
+
score += 2;
|
|
9039
|
+
if (keywords.some((k) => k.includes("modelcontextprotocol")))
|
|
9040
|
+
score += 2;
|
|
9041
|
+
return score;
|
|
9042
|
+
}
|
|
9043
|
+
async function searchNpm(source, query) {
|
|
9044
|
+
try {
|
|
9045
|
+
const url2 = new URL(source.url);
|
|
9046
|
+
url2.searchParams.set("text", `mcp-server ${query}`);
|
|
9047
|
+
url2.searchParams.set("size", "50");
|
|
9048
|
+
const res = await fetch(url2.toString());
|
|
9049
|
+
if (!res.ok)
|
|
9050
|
+
return [];
|
|
9051
|
+
const data = await res.json();
|
|
9052
|
+
const q = query.toLowerCase();
|
|
9053
|
+
const scored = (data.objects || []).map((o) => ({ pkg: o.package, score: scoreNpmPackage(o.package) })).filter(({ pkg, score }) => {
|
|
9054
|
+
if (score < 1)
|
|
9055
|
+
return false;
|
|
9056
|
+
const text = `${pkg.name} ${pkg.description || ""} ${(pkg.keywords || []).join(" ")}`.toLowerCase();
|
|
9057
|
+
return q === "" || text.includes(q);
|
|
9058
|
+
});
|
|
9059
|
+
scored.sort((a, b) => b.score - a.score);
|
|
9060
|
+
return scored.map(({ pkg }) => ({
|
|
9061
|
+
name: pkg.name,
|
|
9062
|
+
description: pkg.description || "",
|
|
9063
|
+
source: "npm",
|
|
9064
|
+
sourceId: source.id,
|
|
9065
|
+
url: pkg.links?.repository || pkg.links?.npm,
|
|
9066
|
+
npmPackage: pkg.name,
|
|
9067
|
+
installCmd: `npx -y ${pkg.name}`
|
|
9068
|
+
}));
|
|
9069
|
+
} catch {
|
|
9070
|
+
return [];
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
9073
|
+
async function searchGitHubTopic(source, query) {
|
|
9074
|
+
const token = process.env.GITHUB_TOKEN;
|
|
9075
|
+
try {
|
|
9076
|
+
const url2 = new URL(source.url);
|
|
9077
|
+
const q = query ? `${query} topic:mcp-server` : "topic:mcp-server";
|
|
9078
|
+
url2.searchParams.set("q", q);
|
|
9079
|
+
url2.searchParams.set("sort", "stars");
|
|
9080
|
+
url2.searchParams.set("per_page", "30");
|
|
9081
|
+
const headers = {
|
|
9082
|
+
Accept: "application/vnd.github.v3+json"
|
|
9083
|
+
};
|
|
9084
|
+
if (token)
|
|
9085
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
9086
|
+
const res = await fetch(url2.toString(), { headers });
|
|
9087
|
+
if (!res.ok)
|
|
9088
|
+
return [];
|
|
9089
|
+
const data = await res.json();
|
|
9090
|
+
return (data.items || []).map((repo) => ({
|
|
9091
|
+
name: repo.full_name,
|
|
9092
|
+
description: repo.description || "",
|
|
9093
|
+
source: "github",
|
|
9094
|
+
sourceId: source.id,
|
|
9095
|
+
url: repo.html_url,
|
|
9096
|
+
githubRepo: repo.html_url,
|
|
9097
|
+
stars: repo.stargazers_count
|
|
9098
|
+
}));
|
|
9099
|
+
} catch {
|
|
9100
|
+
return [];
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
async function fetchSource(source) {
|
|
9104
|
+
switch (source.type) {
|
|
9105
|
+
case "mcp-registry":
|
|
9106
|
+
return searchMcpRegistry(source, "");
|
|
9107
|
+
case "awesome-list":
|
|
9108
|
+
return searchAwesomeList(source, "");
|
|
9109
|
+
case "npm-search":
|
|
9110
|
+
return searchNpm(source, "");
|
|
9111
|
+
case "github-topic":
|
|
9112
|
+
return searchGitHubTopic(source, "");
|
|
9113
|
+
default:
|
|
9114
|
+
return [];
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
function filterResults(results, query) {
|
|
9118
|
+
if (!query)
|
|
9119
|
+
return results;
|
|
9120
|
+
const q = query.toLowerCase();
|
|
9121
|
+
return results.filter((r) => {
|
|
9122
|
+
const text = `${r.name} ${r.description || ""}`.toLowerCase();
|
|
9123
|
+
return text.includes(q);
|
|
9124
|
+
});
|
|
9125
|
+
}
|
|
9126
|
+
async function searchSource(source, query, noCache = false) {
|
|
9127
|
+
if (!noCache) {
|
|
9128
|
+
const cached2 = readCache(source.id);
|
|
9129
|
+
if (cached2 && Date.now() - cached2.cachedAt < DEFAULT_TTL_MS) {
|
|
9130
|
+
return filterResults(cached2.results, query);
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
const results = await fetchSource(source);
|
|
9134
|
+
writeCache(source.id, results);
|
|
9135
|
+
return filterResults(results, query);
|
|
9136
|
+
}
|
|
9137
|
+
function deduplicate(results) {
|
|
9138
|
+
const seen = new Set;
|
|
9139
|
+
return results.filter((r) => {
|
|
9140
|
+
const key = r.npmPackage || r.githubRepo || r.name.toLowerCase();
|
|
9141
|
+
if (seen.has(key))
|
|
9142
|
+
return false;
|
|
9143
|
+
seen.add(key);
|
|
9144
|
+
return true;
|
|
9145
|
+
});
|
|
9146
|
+
}
|
|
9147
|
+
async function findServers(query, opts = {}) {
|
|
9148
|
+
let sources = listSources().filter((s) => s.enabled);
|
|
9149
|
+
if (opts.sources && opts.sources.length > 0) {
|
|
9150
|
+
sources = sources.filter((s) => opts.sources.includes(s.id));
|
|
9151
|
+
}
|
|
9152
|
+
const limit = opts.limit ?? 20;
|
|
9153
|
+
const all = await Promise.all(sources.map((s) => searchSource(s, query, opts.noCache)));
|
|
9154
|
+
const flat = all.flat();
|
|
9155
|
+
const deduped = deduplicate(flat);
|
|
9156
|
+
const typeOrder = {
|
|
9157
|
+
"mcp-registry": 0,
|
|
9158
|
+
"awesome-list": 1,
|
|
9159
|
+
"npm-search": 2,
|
|
9160
|
+
"github-topic": 3
|
|
9161
|
+
};
|
|
9162
|
+
const sourceTypeMap = new Map(sources.map((s) => [s.id, s.type]));
|
|
9163
|
+
deduped.sort((a, b) => {
|
|
9164
|
+
const aType = a.sourceId ? sourceTypeMap.get(a.sourceId) ?? "" : a.source;
|
|
9165
|
+
const bType = b.sourceId ? sourceTypeMap.get(b.sourceId) ?? "" : b.source;
|
|
9166
|
+
const ao = typeOrder[aType] ?? 99;
|
|
9167
|
+
const bo = typeOrder[bType] ?? 99;
|
|
9168
|
+
if (ao !== bo)
|
|
9169
|
+
return ao - bo;
|
|
9170
|
+
return (b.stars ?? 0) - (a.stars ?? 0);
|
|
9171
|
+
});
|
|
9172
|
+
return deduped.slice(0, limit * Math.max(sources.length, 1));
|
|
9173
|
+
}
|
|
9174
|
+
var CACHE_DIR, DEFAULT_TTL_MS;
|
|
9175
|
+
var init_sources = __esm(() => {
|
|
9176
|
+
init_db();
|
|
9177
|
+
init_config();
|
|
9178
|
+
CACHE_DIR = join2(MCPS_DIR, "cache");
|
|
9179
|
+
DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
9180
|
+
});
|
|
9181
|
+
|
|
9182
|
+
// package.json
|
|
9183
|
+
var require_package = __commonJS((exports, module) => {
|
|
9184
|
+
module.exports = {
|
|
9185
|
+
name: "@hasna/mcps",
|
|
9186
|
+
version: "0.0.3",
|
|
9187
|
+
description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
|
|
9188
|
+
type: "module",
|
|
9189
|
+
main: "dist/index.js",
|
|
9190
|
+
types: "dist/index.d.ts",
|
|
9191
|
+
exports: {
|
|
9192
|
+
".": {
|
|
9193
|
+
import: "./dist/index.js",
|
|
9194
|
+
types: "./dist/index.d.ts"
|
|
9195
|
+
}
|
|
9196
|
+
},
|
|
9197
|
+
bin: {
|
|
9198
|
+
mcps: "bin/index.js",
|
|
9199
|
+
"mcps-mcp": "bin/mcp.js"
|
|
9200
|
+
},
|
|
9201
|
+
files: [
|
|
9202
|
+
"dist/",
|
|
9203
|
+
"bin/",
|
|
9204
|
+
"dashboard/dist/",
|
|
9205
|
+
"LICENSE",
|
|
9206
|
+
"README.md"
|
|
9207
|
+
],
|
|
9208
|
+
scripts: {
|
|
9209
|
+
build: "bun run build:dashboard && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
9210
|
+
"build:dashboard": "cd dashboard && bun install && bun run build",
|
|
9211
|
+
dev: "bun run src/cli/index.tsx",
|
|
9212
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
9213
|
+
"dev:dashboard": "cd dashboard && bun run dev",
|
|
9214
|
+
serve: "bun run src/cli/index.tsx serve",
|
|
9215
|
+
test: "bun test",
|
|
9216
|
+
typecheck: "tsc --noEmit",
|
|
9217
|
+
prepublishOnly: "bun run build"
|
|
9218
|
+
},
|
|
9219
|
+
dependencies: {
|
|
9220
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
9221
|
+
chalk: "^5.3.0",
|
|
9222
|
+
commander: "^12.1.0",
|
|
9223
|
+
ink: "^5.0.1",
|
|
9224
|
+
"ink-select-input": "^6.0.0",
|
|
9225
|
+
"ink-spinner": "^5.0.0",
|
|
9226
|
+
"ink-text-input": "^6.0.0",
|
|
9227
|
+
react: "^18.2.0",
|
|
9228
|
+
zod: "^3.23.0"
|
|
9229
|
+
},
|
|
9230
|
+
devDependencies: {
|
|
9231
|
+
"@types/bun": "latest",
|
|
9232
|
+
"@types/react": "^18.2.0",
|
|
9233
|
+
typescript: "^5"
|
|
9234
|
+
},
|
|
9235
|
+
engines: {
|
|
9236
|
+
bun: ">=1.0.0"
|
|
9237
|
+
},
|
|
9238
|
+
license: "Apache-2.0"
|
|
9239
|
+
};
|
|
9240
|
+
});
|
|
9241
|
+
|
|
8798
9242
|
// node_modules/cli-spinners/spinners.json
|
|
8799
9243
|
var require_spinners = __commonJS((exports, module) => {
|
|
8800
9244
|
module.exports = {
|
|
@@ -10455,93 +10899,90 @@ var {
|
|
|
10455
10899
|
import React10 from "react";
|
|
10456
10900
|
import { render } from "ink";
|
|
10457
10901
|
import chalk2 from "chalk";
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
import {
|
|
10461
|
-
import { mkdirSync } from "fs";
|
|
10462
|
-
|
|
10463
|
-
// src/lib/config.ts
|
|
10464
|
-
import { join } from "path";
|
|
10465
|
-
import { homedir } from "os";
|
|
10466
|
-
var MCPS_DIR = join(homedir(), ".mcps");
|
|
10467
|
-
var DB_PATH = join(MCPS_DIR, "registry.db");
|
|
10468
|
-
var REGISTRY_API_URL = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
10469
|
-
var TOOL_PREFIX_SEPARATOR = "__";
|
|
10470
|
-
|
|
10471
|
-
// src/lib/db.ts
|
|
10472
|
-
var db = null;
|
|
10473
|
-
function getDb() {
|
|
10474
|
-
if (db)
|
|
10475
|
-
return db;
|
|
10476
|
-
mkdirSync(MCPS_DIR, { recursive: true });
|
|
10477
|
-
db = new Database(DB_PATH, { create: true });
|
|
10478
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
10479
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
10480
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
10481
|
-
db.exec(`
|
|
10482
|
-
CREATE TABLE IF NOT EXISTS servers (
|
|
10483
|
-
id TEXT PRIMARY KEY,
|
|
10484
|
-
name TEXT NOT NULL,
|
|
10485
|
-
description TEXT,
|
|
10486
|
-
command TEXT NOT NULL,
|
|
10487
|
-
args TEXT NOT NULL DEFAULT '[]',
|
|
10488
|
-
env TEXT NOT NULL DEFAULT '{}',
|
|
10489
|
-
transport TEXT NOT NULL DEFAULT 'stdio',
|
|
10490
|
-
url TEXT,
|
|
10491
|
-
source TEXT NOT NULL DEFAULT 'local',
|
|
10492
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
10493
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10494
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10495
|
-
)
|
|
10496
|
-
`);
|
|
10497
|
-
db.exec(`
|
|
10498
|
-
CREATE TABLE IF NOT EXISTS tool_cache (
|
|
10499
|
-
server_id TEXT NOT NULL,
|
|
10500
|
-
name TEXT NOT NULL,
|
|
10501
|
-
description TEXT NOT NULL DEFAULT '',
|
|
10502
|
-
input_schema TEXT NOT NULL DEFAULT '{}',
|
|
10503
|
-
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10504
|
-
PRIMARY KEY (server_id, name),
|
|
10505
|
-
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
10506
|
-
)
|
|
10507
|
-
`);
|
|
10508
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_tool_cache_server ON tool_cache(server_id)");
|
|
10509
|
-
return db;
|
|
10510
|
-
}
|
|
10511
|
-
function closeDb() {
|
|
10512
|
-
if (db) {
|
|
10513
|
-
db.close();
|
|
10514
|
-
db = null;
|
|
10515
|
-
}
|
|
10516
|
-
}
|
|
10902
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
10903
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
10904
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10517
10905
|
|
|
10518
10906
|
// src/lib/registry.ts
|
|
10907
|
+
init_db();
|
|
10519
10908
|
function parseRow(row) {
|
|
10520
10909
|
return {
|
|
10521
10910
|
id: row.id,
|
|
10522
10911
|
name: row.name,
|
|
10523
10912
|
description: row.description || null,
|
|
10524
10913
|
command: row.command,
|
|
10525
|
-
args:
|
|
10526
|
-
env:
|
|
10914
|
+
args: safeJsonParse(row.args, []),
|
|
10915
|
+
env: safeJsonParse(row.env, {}),
|
|
10527
10916
|
transport: row.transport,
|
|
10528
10917
|
url: row.url || null,
|
|
10529
10918
|
source: row.source,
|
|
10530
10919
|
enabled: row.enabled === 1,
|
|
10531
10920
|
created_at: row.created_at,
|
|
10532
|
-
updated_at: row.updated_at
|
|
10921
|
+
updated_at: row.updated_at,
|
|
10922
|
+
last_connected_at: row.last_connected_at ?? null,
|
|
10923
|
+
last_error: row.last_error ?? null
|
|
10533
10924
|
};
|
|
10534
10925
|
}
|
|
10926
|
+
function safeJsonParse(value, fallback) {
|
|
10927
|
+
if (typeof value !== "string")
|
|
10928
|
+
return fallback;
|
|
10929
|
+
try {
|
|
10930
|
+
return JSON.parse(value);
|
|
10931
|
+
} catch {
|
|
10932
|
+
return fallback;
|
|
10933
|
+
}
|
|
10934
|
+
}
|
|
10535
10935
|
function generateId(name) {
|
|
10536
10936
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10537
10937
|
}
|
|
10938
|
+
function normalizeCandidate(value) {
|
|
10939
|
+
const trimmed = value?.trim();
|
|
10940
|
+
return trimmed ? trimmed : undefined;
|
|
10941
|
+
}
|
|
10942
|
+
function pickNameFromArgs(args) {
|
|
10943
|
+
if (!args || args.length === 0)
|
|
10944
|
+
return;
|
|
10945
|
+
const ddIndex = args.indexOf("--");
|
|
10946
|
+
if (ddIndex >= 0 && ddIndex < args.length - 1) {
|
|
10947
|
+
const after = normalizeCandidate(args[ddIndex + 1]);
|
|
10948
|
+
if (after)
|
|
10949
|
+
return after;
|
|
10950
|
+
}
|
|
10951
|
+
for (const arg of args) {
|
|
10952
|
+
const candidate = normalizeCandidate(arg);
|
|
10953
|
+
if (!candidate)
|
|
10954
|
+
continue;
|
|
10955
|
+
if (candidate.startsWith("-"))
|
|
10956
|
+
continue;
|
|
10957
|
+
return candidate;
|
|
10958
|
+
}
|
|
10959
|
+
return;
|
|
10960
|
+
}
|
|
10961
|
+
function pickId(candidates) {
|
|
10962
|
+
for (const candidate of candidates) {
|
|
10963
|
+
if (!candidate)
|
|
10964
|
+
continue;
|
|
10965
|
+
const id = generateId(candidate);
|
|
10966
|
+
if (id)
|
|
10967
|
+
return id;
|
|
10968
|
+
}
|
|
10969
|
+
return null;
|
|
10970
|
+
}
|
|
10538
10971
|
function addServer(opts) {
|
|
10539
10972
|
const db2 = getDb();
|
|
10540
|
-
const
|
|
10541
|
-
|
|
10973
|
+
const command = normalizeCandidate(opts.command);
|
|
10974
|
+
if (!command) {
|
|
10975
|
+
throw new Error("Command is required");
|
|
10976
|
+
}
|
|
10977
|
+
const argName = pickNameFromArgs(opts.args);
|
|
10978
|
+
const name = normalizeCandidate(opts.name) || argName || command;
|
|
10979
|
+
const id = pickId([normalizeCandidate(opts.name), argName, command]) || null;
|
|
10980
|
+
if (!id) {
|
|
10981
|
+
throw new Error("Unable to generate a valid server ID");
|
|
10982
|
+
}
|
|
10542
10983
|
const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
|
|
10543
10984
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10544
|
-
RETURNING *`).get(id, name, opts.description || null,
|
|
10985
|
+
RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
|
|
10545
10986
|
return parseRow(row);
|
|
10546
10987
|
}
|
|
10547
10988
|
function removeServer(id) {
|
|
@@ -10597,6 +11038,9 @@ function updateServer(id, updates) {
|
|
|
10597
11038
|
sets.push("updated_at = datetime('now')");
|
|
10598
11039
|
values.push(id);
|
|
10599
11040
|
const row = db2.prepare(`UPDATE servers SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...values);
|
|
11041
|
+
if (!row) {
|
|
11042
|
+
throw new Error(`Server "${id}" not found`);
|
|
11043
|
+
}
|
|
10600
11044
|
return parseRow(row);
|
|
10601
11045
|
}
|
|
10602
11046
|
function enableServer(id) {
|
|
@@ -10605,84 +11049,64 @@ function enableServer(id) {
|
|
|
10605
11049
|
function disableServer(id) {
|
|
10606
11050
|
return updateServer(id, { enabled: false });
|
|
10607
11051
|
}
|
|
11052
|
+
function setServerEnv(id, key, value) {
|
|
11053
|
+
const db2 = getDb();
|
|
11054
|
+
const server = getServer(id);
|
|
11055
|
+
if (!server)
|
|
11056
|
+
throw new Error(`Server "${id}" not found`);
|
|
11057
|
+
const env = { ...server.env, [key]: value };
|
|
11058
|
+
db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
|
|
11059
|
+
}
|
|
11060
|
+
function unsetServerEnv(id, key) {
|
|
11061
|
+
const db2 = getDb();
|
|
11062
|
+
const server = getServer(id);
|
|
11063
|
+
if (!server)
|
|
11064
|
+
throw new Error(`Server "${id}" not found`);
|
|
11065
|
+
const env = { ...server.env };
|
|
11066
|
+
delete env[key];
|
|
11067
|
+
db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
|
|
11068
|
+
}
|
|
10608
11069
|
function cacheTools(serverId, tools) {
|
|
10609
11070
|
const db2 = getDb();
|
|
10610
|
-
db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
|
|
10611
11071
|
const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
|
|
11072
|
+
const uniqueTools = [];
|
|
11073
|
+
const seen = new Set;
|
|
10612
11074
|
for (const tool of tools) {
|
|
10613
|
-
|
|
11075
|
+
const name = tool.name?.trim();
|
|
11076
|
+
if (!name || seen.has(name))
|
|
11077
|
+
continue;
|
|
11078
|
+
seen.add(name);
|
|
11079
|
+
uniqueTools.push({
|
|
11080
|
+
name,
|
|
11081
|
+
description: tool.description || "",
|
|
11082
|
+
input_schema: tool.input_schema || {}
|
|
11083
|
+
});
|
|
10614
11084
|
}
|
|
11085
|
+
const run = db2.transaction((rows) => {
|
|
11086
|
+
db2.prepare("DELETE FROM tool_cache WHERE server_id = ?").run(serverId);
|
|
11087
|
+
for (const tool of rows) {
|
|
11088
|
+
insert.run(serverId, tool.name, tool.description, JSON.stringify(tool.input_schema));
|
|
11089
|
+
}
|
|
11090
|
+
});
|
|
11091
|
+
run(uniqueTools);
|
|
11092
|
+
}
|
|
11093
|
+
function getToolCounts() {
|
|
11094
|
+
const db2 = getDb();
|
|
11095
|
+
const rows = db2.prepare("SELECT server_id, COUNT(*) as count FROM tool_cache GROUP BY server_id").all();
|
|
11096
|
+
return new Map(rows.map((row) => [row.server_id, Number(row.count)]));
|
|
10615
11097
|
}
|
|
10616
11098
|
function getCachedTools(serverId) {
|
|
10617
11099
|
const db2 = getDb();
|
|
10618
|
-
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ?").all(serverId);
|
|
11100
|
+
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
|
|
10619
11101
|
return rows.map((r) => ({
|
|
10620
11102
|
name: r.name,
|
|
10621
11103
|
description: r.description,
|
|
10622
|
-
input_schema:
|
|
11104
|
+
input_schema: safeJsonParse(r.input_schema, {})
|
|
10623
11105
|
}));
|
|
10624
11106
|
}
|
|
10625
11107
|
|
|
10626
|
-
// src/lib/
|
|
10627
|
-
|
|
10628
|
-
const s = entry.server;
|
|
10629
|
-
return {
|
|
10630
|
-
id: s.name,
|
|
10631
|
-
name: s.name,
|
|
10632
|
-
description: s.description || "",
|
|
10633
|
-
repository: s.repository,
|
|
10634
|
-
packages: s.packages || []
|
|
10635
|
-
};
|
|
10636
|
-
}
|
|
10637
|
-
async function searchRegistry(query) {
|
|
10638
|
-
const res = await fetch(REGISTRY_API_URL);
|
|
10639
|
-
if (!res.ok) {
|
|
10640
|
-
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
10641
|
-
}
|
|
10642
|
-
const data = await res.json();
|
|
10643
|
-
const entries = data.servers || [];
|
|
10644
|
-
const q = query.toLowerCase();
|
|
10645
|
-
return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
|
10646
|
-
}
|
|
10647
|
-
async function getRegistryServer(id) {
|
|
10648
|
-
const res = await fetch(REGISTRY_API_URL);
|
|
10649
|
-
if (!res.ok) {
|
|
10650
|
-
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
10651
|
-
}
|
|
10652
|
-
const data = await res.json();
|
|
10653
|
-
const entries = data.servers || [];
|
|
10654
|
-
const all = entries.map(parseRegistryEntry);
|
|
10655
|
-
return all.find((s) => s.id === id) || null;
|
|
10656
|
-
}
|
|
10657
|
-
async function installFromRegistry(id) {
|
|
10658
|
-
const server = await getRegistryServer(id);
|
|
10659
|
-
if (!server) {
|
|
10660
|
-
throw new Error(`Server "${id}" not found in registry`);
|
|
10661
|
-
}
|
|
10662
|
-
const pkg = server.packages?.[0];
|
|
10663
|
-
let command = "npx";
|
|
10664
|
-
let args = [];
|
|
10665
|
-
let transport = "stdio";
|
|
10666
|
-
if (pkg) {
|
|
10667
|
-
if (pkg.registryType === "npm") {
|
|
10668
|
-
command = "npx";
|
|
10669
|
-
args = ["-y", pkg.identifier];
|
|
10670
|
-
} else {
|
|
10671
|
-
command = pkg.identifier;
|
|
10672
|
-
}
|
|
10673
|
-
if (pkg.transport?.type) {
|
|
10674
|
-
transport = pkg.transport.type;
|
|
10675
|
-
}
|
|
10676
|
-
}
|
|
10677
|
-
return addServer({
|
|
10678
|
-
name: server.name,
|
|
10679
|
-
description: server.description,
|
|
10680
|
-
command,
|
|
10681
|
-
args,
|
|
10682
|
-
transport,
|
|
10683
|
-
source: "registry"
|
|
10684
|
-
});
|
|
10685
|
-
}
|
|
11108
|
+
// src/lib/doctor.ts
|
|
11109
|
+
import { execFileSync } from "child_process";
|
|
10686
11110
|
|
|
10687
11111
|
// node_modules/zod/v3/external.js
|
|
10688
11112
|
var exports_external = {};
|
|
@@ -24909,54 +25333,127 @@ class StreamableHTTPClientTransport {
|
|
|
24909
25333
|
}
|
|
24910
25334
|
|
|
24911
25335
|
// src/lib/proxy.ts
|
|
25336
|
+
init_config();
|
|
25337
|
+
init_db();
|
|
24912
25338
|
var connections = new Map;
|
|
25339
|
+
var inflightConnections = new Map;
|
|
25340
|
+
var CONNECT_CONCURRENCY = 4;
|
|
25341
|
+
function buildEnv(extra) {
|
|
25342
|
+
const merged = {};
|
|
25343
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
25344
|
+
if (typeof value === "string")
|
|
25345
|
+
merged[key] = value;
|
|
25346
|
+
}
|
|
25347
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
25348
|
+
if (value === undefined || value === null)
|
|
25349
|
+
continue;
|
|
25350
|
+
merged[key] = String(value);
|
|
25351
|
+
}
|
|
25352
|
+
return merged;
|
|
25353
|
+
}
|
|
25354
|
+
function requireUrl(entry) {
|
|
25355
|
+
if (!entry.url) {
|
|
25356
|
+
throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
|
|
25357
|
+
}
|
|
25358
|
+
try {
|
|
25359
|
+
return new URL(entry.url);
|
|
25360
|
+
} catch {
|
|
25361
|
+
throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
|
|
25362
|
+
}
|
|
25363
|
+
}
|
|
24913
25364
|
async function connectToServer(entry) {
|
|
24914
25365
|
if (connections.has(entry.id)) {
|
|
24915
25366
|
return connections.get(entry.id);
|
|
24916
25367
|
}
|
|
25368
|
+
const inflight = inflightConnections.get(entry.id);
|
|
25369
|
+
if (inflight) {
|
|
25370
|
+
return inflight;
|
|
25371
|
+
}
|
|
24917
25372
|
const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
|
|
24918
25373
|
let transport;
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
|
|
24923
|
-
|
|
24924
|
-
|
|
24925
|
-
|
|
24926
|
-
|
|
24927
|
-
|
|
24928
|
-
|
|
24929
|
-
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
|
|
24936
|
-
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
24940
|
-
|
|
24941
|
-
|
|
24942
|
-
|
|
24943
|
-
|
|
24944
|
-
|
|
25374
|
+
const connectPromise = (async () => {
|
|
25375
|
+
try {
|
|
25376
|
+
if (entry.transport === "stdio") {
|
|
25377
|
+
if (!entry.command?.trim()) {
|
|
25378
|
+
throw new Error(`Server "${entry.id}" is missing a command`);
|
|
25379
|
+
}
|
|
25380
|
+
transport = new StdioClientTransport({
|
|
25381
|
+
command: entry.command,
|
|
25382
|
+
args: entry.args,
|
|
25383
|
+
env: buildEnv(entry.env)
|
|
25384
|
+
});
|
|
25385
|
+
} else if (entry.transport === "sse") {
|
|
25386
|
+
transport = new SSEClientTransport(requireUrl(entry));
|
|
25387
|
+
} else {
|
|
25388
|
+
transport = new StreamableHTTPClientTransport(requireUrl(entry));
|
|
25389
|
+
}
|
|
25390
|
+
await client.connect(transport);
|
|
25391
|
+
const result = await client.listTools();
|
|
25392
|
+
const tools = (result.tools || []).map((t) => ({
|
|
25393
|
+
server_id: entry.id,
|
|
25394
|
+
name: t.name,
|
|
25395
|
+
description: t.description || "",
|
|
25396
|
+
input_schema: t.inputSchema || {}
|
|
25397
|
+
}));
|
|
25398
|
+
cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
|
|
25399
|
+
const connected = {
|
|
25400
|
+
entry,
|
|
25401
|
+
tools,
|
|
25402
|
+
disconnect: async () => {
|
|
25403
|
+
try {
|
|
25404
|
+
await client.close();
|
|
25405
|
+
} finally {
|
|
25406
|
+
connections.delete(entry.id);
|
|
25407
|
+
}
|
|
25408
|
+
}
|
|
25409
|
+
};
|
|
25410
|
+
connected._client = client;
|
|
25411
|
+
connections.set(entry.id, connected);
|
|
25412
|
+
try {
|
|
25413
|
+
getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
|
|
25414
|
+
} catch {}
|
|
25415
|
+
return connected;
|
|
25416
|
+
} catch (err) {
|
|
25417
|
+
try {
|
|
25418
|
+
getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
|
|
25419
|
+
} catch {}
|
|
25420
|
+
try {
|
|
25421
|
+
await client.close();
|
|
25422
|
+
} catch {}
|
|
25423
|
+
throw err;
|
|
24945
25424
|
}
|
|
24946
|
-
};
|
|
24947
|
-
|
|
24948
|
-
|
|
24949
|
-
|
|
25425
|
+
})();
|
|
25426
|
+
inflightConnections.set(entry.id, connectPromise);
|
|
25427
|
+
try {
|
|
25428
|
+
return await connectPromise;
|
|
25429
|
+
} finally {
|
|
25430
|
+
inflightConnections.delete(entry.id);
|
|
25431
|
+
}
|
|
24950
25432
|
}
|
|
24951
25433
|
async function disconnectServer(id) {
|
|
24952
25434
|
const conn = connections.get(id);
|
|
24953
25435
|
if (conn) {
|
|
24954
25436
|
await conn.disconnect();
|
|
25437
|
+
return;
|
|
25438
|
+
}
|
|
25439
|
+
const inflight = inflightConnections.get(id);
|
|
25440
|
+
if (inflight) {
|
|
25441
|
+
try {
|
|
25442
|
+
const pending = await inflight;
|
|
25443
|
+
await pending.disconnect();
|
|
25444
|
+
} catch {}
|
|
24955
25445
|
}
|
|
24956
25446
|
}
|
|
24957
25447
|
async function disconnectAll() {
|
|
24958
25448
|
const ids = Array.from(connections.keys());
|
|
24959
|
-
await Promise.
|
|
25449
|
+
await Promise.allSettled(ids.map((id) => disconnectServer(id)));
|
|
25450
|
+
const inflight = Array.from(inflightConnections.values());
|
|
25451
|
+
await Promise.allSettled(inflight.map(async (promise2) => {
|
|
25452
|
+
try {
|
|
25453
|
+
const conn = await promise2;
|
|
25454
|
+
await conn.disconnect();
|
|
25455
|
+
} catch {}
|
|
25456
|
+
}));
|
|
24960
25457
|
}
|
|
24961
25458
|
function listAllTools() {
|
|
24962
25459
|
const tools = [];
|
|
@@ -24971,12 +25468,19 @@ function listAllTools() {
|
|
|
24971
25468
|
return tools;
|
|
24972
25469
|
}
|
|
24973
25470
|
async function callTool(prefixedName, args) {
|
|
24974
|
-
const
|
|
25471
|
+
const normalized = prefixedName.trim();
|
|
25472
|
+
if (!normalized) {
|
|
25473
|
+
throw new Error("Tool name is required");
|
|
25474
|
+
}
|
|
25475
|
+
const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
24975
25476
|
if (sepIdx === -1) {
|
|
24976
25477
|
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
24977
25478
|
}
|
|
24978
|
-
const serverId =
|
|
24979
|
-
const toolName =
|
|
25479
|
+
const serverId = normalized.slice(0, sepIdx).trim();
|
|
25480
|
+
const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
|
|
25481
|
+
if (!serverId || !toolName) {
|
|
25482
|
+
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
25483
|
+
}
|
|
24980
25484
|
const conn = connections.get(serverId);
|
|
24981
25485
|
if (!conn) {
|
|
24982
25486
|
throw new Error(`Server "${serverId}" is not connected`);
|
|
@@ -24992,16 +25496,235 @@ async function callTool(prefixedName, args) {
|
|
|
24992
25496
|
async function connectAllEnabled() {
|
|
24993
25497
|
const servers = listServers().filter((s) => s.enabled);
|
|
24994
25498
|
const results = [];
|
|
24995
|
-
|
|
25499
|
+
let index = 0;
|
|
25500
|
+
const workerCount = Math.min(CONNECT_CONCURRENCY, servers.length);
|
|
25501
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
25502
|
+
while (true) {
|
|
25503
|
+
const current = index++;
|
|
25504
|
+
if (current >= servers.length)
|
|
25505
|
+
return;
|
|
25506
|
+
const server = servers[current];
|
|
25507
|
+
try {
|
|
25508
|
+
const conn = await connectToServer(server);
|
|
25509
|
+
results.push(conn);
|
|
25510
|
+
} catch (err) {
|
|
25511
|
+
console.error(`Failed to connect to ${server.name}: ${err.message}`);
|
|
25512
|
+
}
|
|
25513
|
+
}
|
|
25514
|
+
});
|
|
25515
|
+
await Promise.all(workers);
|
|
25516
|
+
return results;
|
|
25517
|
+
}
|
|
25518
|
+
|
|
25519
|
+
// src/lib/doctor.ts
|
|
25520
|
+
async function diagnoseServer(server) {
|
|
25521
|
+
const checks4 = [];
|
|
25522
|
+
if (server.transport === "stdio") {
|
|
25523
|
+
try {
|
|
25524
|
+
execFileSync("which", [server.command], { stdio: "pipe" });
|
|
25525
|
+
checks4.push({ name: "command on PATH", pass: true, message: `${server.command} found` });
|
|
25526
|
+
} catch {
|
|
25527
|
+
checks4.push({ name: "command on PATH", pass: false, message: `${server.command} not found on PATH` });
|
|
25528
|
+
}
|
|
25529
|
+
}
|
|
25530
|
+
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
25531
|
+
if (Object.keys(server.env).length === 0) {
|
|
25532
|
+
checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
|
|
25533
|
+
} else if (missingEnv.length > 0) {
|
|
25534
|
+
checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
|
|
25535
|
+
} else {
|
|
25536
|
+
checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
|
|
25537
|
+
}
|
|
25538
|
+
if (server.transport !== "stdio" && server.url) {
|
|
24996
25539
|
try {
|
|
24997
|
-
const
|
|
24998
|
-
|
|
25540
|
+
const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
|
|
25541
|
+
checks4.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
|
|
24999
25542
|
} catch (err) {
|
|
25000
|
-
|
|
25543
|
+
checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
|
|
25001
25544
|
}
|
|
25002
25545
|
}
|
|
25003
|
-
|
|
25546
|
+
if (server.enabled) {
|
|
25547
|
+
try {
|
|
25548
|
+
await Promise.race([
|
|
25549
|
+
connectToServer(server),
|
|
25550
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
|
|
25551
|
+
]);
|
|
25552
|
+
await disconnectServer(server.id);
|
|
25553
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
|
|
25554
|
+
} catch (err) {
|
|
25555
|
+
checks4.push({ name: "connect & list tools", pass: false, message: err.message });
|
|
25556
|
+
}
|
|
25557
|
+
} else {
|
|
25558
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
|
|
25559
|
+
}
|
|
25560
|
+
return {
|
|
25561
|
+
server,
|
|
25562
|
+
checks: checks4,
|
|
25563
|
+
healthy: checks4.every((c) => c.pass)
|
|
25564
|
+
};
|
|
25565
|
+
}
|
|
25566
|
+
|
|
25567
|
+
// src/lib/remote.ts
|
|
25568
|
+
init_config();
|
|
25569
|
+
function parseRegistryEntry(entry) {
|
|
25570
|
+
const s = entry.server;
|
|
25571
|
+
return {
|
|
25572
|
+
id: s.name,
|
|
25573
|
+
name: s.name,
|
|
25574
|
+
description: s.description || "",
|
|
25575
|
+
repository: s.repository,
|
|
25576
|
+
packages: s.packages || []
|
|
25577
|
+
};
|
|
25578
|
+
}
|
|
25579
|
+
async function searchRegistry(query) {
|
|
25580
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
25581
|
+
if (!res.ok) {
|
|
25582
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
25583
|
+
}
|
|
25584
|
+
const data = await res.json();
|
|
25585
|
+
const entries = data.servers || [];
|
|
25586
|
+
const q = query.toLowerCase();
|
|
25587
|
+
return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
|
25588
|
+
}
|
|
25589
|
+
async function getRegistryServer(id) {
|
|
25590
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
25591
|
+
if (!res.ok) {
|
|
25592
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
25593
|
+
}
|
|
25594
|
+
const data = await res.json();
|
|
25595
|
+
const entries = data.servers || [];
|
|
25596
|
+
const all = entries.map(parseRegistryEntry);
|
|
25597
|
+
return all.find((s) => s.id === id) || null;
|
|
25598
|
+
}
|
|
25599
|
+
async function installFromRegistry(id) {
|
|
25600
|
+
const server = await getRegistryServer(id);
|
|
25601
|
+
if (!server) {
|
|
25602
|
+
throw new Error(`Server "${id}" not found in registry`);
|
|
25603
|
+
}
|
|
25604
|
+
const pkg = server.packages?.[0];
|
|
25605
|
+
let command = "npx";
|
|
25606
|
+
let args = [];
|
|
25607
|
+
let transport = "stdio";
|
|
25608
|
+
if (pkg) {
|
|
25609
|
+
if (pkg.registryType === "npm") {
|
|
25610
|
+
command = "npx";
|
|
25611
|
+
args = ["-y", pkg.identifier];
|
|
25612
|
+
} else {
|
|
25613
|
+
command = pkg.identifier;
|
|
25614
|
+
}
|
|
25615
|
+
if (pkg.transport?.type) {
|
|
25616
|
+
transport = pkg.transport.type;
|
|
25617
|
+
}
|
|
25618
|
+
}
|
|
25619
|
+
return addServer({
|
|
25620
|
+
name: server.name,
|
|
25621
|
+
description: server.description,
|
|
25622
|
+
command,
|
|
25623
|
+
args,
|
|
25624
|
+
transport,
|
|
25625
|
+
source: "registry"
|
|
25626
|
+
});
|
|
25627
|
+
}
|
|
25628
|
+
|
|
25629
|
+
// src/lib/finder.ts
|
|
25630
|
+
init_sources();
|
|
25631
|
+
async function listAwesomeServers() {
|
|
25632
|
+
const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
|
|
25633
|
+
const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
|
|
25634
|
+
if (!source)
|
|
25635
|
+
return [];
|
|
25636
|
+
return searchSource2(source, "");
|
|
25637
|
+
}
|
|
25638
|
+
|
|
25639
|
+
// src/cli/index.tsx
|
|
25640
|
+
init_sources();
|
|
25641
|
+
|
|
25642
|
+
// src/lib/install.ts
|
|
25643
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
25644
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
25645
|
+
import { join as join3 } from "path";
|
|
25646
|
+
import { homedir as homedir2 } from "os";
|
|
25647
|
+
function installToClaude(entry) {
|
|
25648
|
+
try {
|
|
25649
|
+
const args = [
|
|
25650
|
+
"mcp",
|
|
25651
|
+
"add",
|
|
25652
|
+
"--transport",
|
|
25653
|
+
entry.transport,
|
|
25654
|
+
"--scope",
|
|
25655
|
+
"user"
|
|
25656
|
+
];
|
|
25657
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
25658
|
+
args.push("--env", `${k}=${v}`);
|
|
25659
|
+
}
|
|
25660
|
+
args.push(entry.id, "--", entry.command, ...entry.args);
|
|
25661
|
+
execFileSync2("claude", args, { stdio: "pipe" });
|
|
25662
|
+
return { agent: "claude", success: true };
|
|
25663
|
+
} catch (err) {
|
|
25664
|
+
return { agent: "claude", success: false, error: err.message };
|
|
25665
|
+
}
|
|
25666
|
+
}
|
|
25667
|
+
function installToCodex(entry) {
|
|
25668
|
+
try {
|
|
25669
|
+
const configDir = join3(homedir2(), ".codex");
|
|
25670
|
+
const configPath = join3(configDir, "config.toml");
|
|
25671
|
+
if (!existsSync2(configDir)) {
|
|
25672
|
+
mkdirSync3(configDir, { recursive: true });
|
|
25673
|
+
}
|
|
25674
|
+
const block = `
|
|
25675
|
+
[mcp_servers.${entry.id}]
|
|
25676
|
+
` + `command = ${JSON.stringify(entry.command)}
|
|
25677
|
+
` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
|
|
25678
|
+
`;
|
|
25679
|
+
const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
|
|
25680
|
+
if (existing.includes(`[mcp_servers.${entry.id}]`)) {
|
|
25681
|
+
return { agent: "codex", success: true };
|
|
25682
|
+
}
|
|
25683
|
+
writeFileSync2(configPath, existing + block, "utf-8");
|
|
25684
|
+
return { agent: "codex", success: true };
|
|
25685
|
+
} catch (err) {
|
|
25686
|
+
return { agent: "codex", success: false, error: err.message };
|
|
25687
|
+
}
|
|
25004
25688
|
}
|
|
25689
|
+
function installToGemini(entry) {
|
|
25690
|
+
try {
|
|
25691
|
+
const configDir = join3(homedir2(), ".gemini");
|
|
25692
|
+
const configPath = join3(configDir, "settings.json");
|
|
25693
|
+
if (!existsSync2(configDir)) {
|
|
25694
|
+
mkdirSync3(configDir, { recursive: true });
|
|
25695
|
+
}
|
|
25696
|
+
let settings = {};
|
|
25697
|
+
if (existsSync2(configPath)) {
|
|
25698
|
+
settings = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
25699
|
+
}
|
|
25700
|
+
if (!settings.mcpServers)
|
|
25701
|
+
settings.mcpServers = {};
|
|
25702
|
+
settings.mcpServers[entry.id] = {
|
|
25703
|
+
command: entry.command,
|
|
25704
|
+
args: entry.args,
|
|
25705
|
+
...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
|
|
25706
|
+
};
|
|
25707
|
+
writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
25708
|
+
return { agent: "gemini", success: true };
|
|
25709
|
+
} catch (err) {
|
|
25710
|
+
return { agent: "gemini", success: false, error: err.message };
|
|
25711
|
+
}
|
|
25712
|
+
}
|
|
25713
|
+
function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
|
|
25714
|
+
return targets.map((target) => {
|
|
25715
|
+
if (target === "claude")
|
|
25716
|
+
return installToClaude(entry);
|
|
25717
|
+
if (target === "codex")
|
|
25718
|
+
return installToCodex(entry);
|
|
25719
|
+
if (target === "gemini")
|
|
25720
|
+
return installToGemini(entry);
|
|
25721
|
+
return { agent: target, success: false, error: "Unknown target" };
|
|
25722
|
+
});
|
|
25723
|
+
}
|
|
25724
|
+
|
|
25725
|
+
// src/cli/index.tsx
|
|
25726
|
+
init_db();
|
|
25727
|
+
import * as readline from "readline";
|
|
25005
25728
|
|
|
25006
25729
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
|
|
25007
25730
|
class ExperimentalServerTasks {
|
|
@@ -26224,14 +26947,31 @@ class StdioServerTransport {
|
|
|
26224
26947
|
}
|
|
26225
26948
|
|
|
26226
26949
|
// src/mcp/index.ts
|
|
26950
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
26951
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
26952
|
+
import { fileURLToPath } from "url";
|
|
26953
|
+
init_sources();
|
|
26954
|
+
init_config();
|
|
26955
|
+
function redactServerEnv(server) {
|
|
26956
|
+
return { ...server, env: {} };
|
|
26957
|
+
}
|
|
26958
|
+
var VERSION = (() => {
|
|
26959
|
+
try {
|
|
26960
|
+
const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
26961
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
26962
|
+
return pkg.version || "0.0.1";
|
|
26963
|
+
} catch {
|
|
26964
|
+
return "0.0.1";
|
|
26965
|
+
}
|
|
26966
|
+
})();
|
|
26227
26967
|
var server = new McpServer({
|
|
26228
26968
|
name: "mcps",
|
|
26229
|
-
version:
|
|
26969
|
+
version: VERSION
|
|
26230
26970
|
});
|
|
26231
26971
|
server.tool("list_servers", "List all registered MCP servers", {}, async () => {
|
|
26232
26972
|
const servers = listServers();
|
|
26233
26973
|
return {
|
|
26234
|
-
content: [{ type: "text", text: JSON.stringify(servers, null, 2) }]
|
|
26974
|
+
content: [{ type: "text", text: JSON.stringify(servers.map(redactServerEnv), null, 2) }]
|
|
26235
26975
|
};
|
|
26236
26976
|
});
|
|
26237
26977
|
server.tool("search_registry", "Search the official MCP registry for servers", { query: exports_external.string().describe("Search query") }, async ({ query }) => {
|
|
@@ -26282,12 +27022,26 @@ server.tool("remove_server", "Remove a registered MCP server", { id: exports_ext
|
|
|
26282
27022
|
};
|
|
26283
27023
|
});
|
|
26284
27024
|
server.tool("enable_server", "Enable a registered MCP server", { id: exports_external.string().describe("Server ID to enable") }, async ({ id }) => {
|
|
27025
|
+
const existing = getServer(id);
|
|
27026
|
+
if (!existing) {
|
|
27027
|
+
return {
|
|
27028
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27029
|
+
isError: true
|
|
27030
|
+
};
|
|
27031
|
+
}
|
|
26285
27032
|
const entry = enableServer(id);
|
|
26286
27033
|
return {
|
|
26287
27034
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
26288
27035
|
};
|
|
26289
27036
|
});
|
|
26290
27037
|
server.tool("disable_server", "Disable a registered MCP server", { id: exports_external.string().describe("Server ID to disable") }, async ({ id }) => {
|
|
27038
|
+
const existing = getServer(id);
|
|
27039
|
+
if (!existing) {
|
|
27040
|
+
return {
|
|
27041
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27042
|
+
isError: true
|
|
27043
|
+
};
|
|
27044
|
+
}
|
|
26291
27045
|
const entry = disableServer(id);
|
|
26292
27046
|
return {
|
|
26293
27047
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
@@ -26302,36 +27056,161 @@ server.tool("get_server_info", "Get detailed information about a registered MCP
|
|
|
26302
27056
|
};
|
|
26303
27057
|
}
|
|
26304
27058
|
return {
|
|
26305
|
-
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
27059
|
+
content: [{ type: "text", text: JSON.stringify(redactServerEnv(entry), null, 2) }]
|
|
26306
27060
|
};
|
|
26307
27061
|
});
|
|
26308
|
-
server.tool("
|
|
26309
|
-
|
|
26310
|
-
|
|
27062
|
+
server.tool("find_mcp_servers", "Search for MCP servers across configured sources (official registry, npm, GitHub topics, awesome lists). Use list_sources to see available source IDs.", {
|
|
27063
|
+
query: exports_external.string().describe("Search query (e.g. 'filesystem', 'postgres', 'browser')"),
|
|
27064
|
+
sources: exports_external.array(exports_external.string()).optional().describe("Source IDs to search (default: all enabled). Use list_sources to get IDs."),
|
|
27065
|
+
limit: exports_external.number().optional().describe("Max results per source (default: 20)")
|
|
27066
|
+
}, async ({ query, sources, limit }) => {
|
|
27067
|
+
const results = await findServers(query, { sources, limit });
|
|
26311
27068
|
return {
|
|
26312
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
27069
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
26313
27070
|
};
|
|
26314
27071
|
});
|
|
26315
|
-
server.tool("
|
|
26316
|
-
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
27072
|
+
server.tool("list_sources", "List all configured search sources for finding MCP servers", {}, async () => {
|
|
27073
|
+
const sources = listSources();
|
|
27074
|
+
return {
|
|
27075
|
+
content: [{ type: "text", text: JSON.stringify(sources, null, 2) }]
|
|
27076
|
+
};
|
|
27077
|
+
});
|
|
27078
|
+
server.tool("add_source", "Add a new search source for finding MCP servers", {
|
|
27079
|
+
name: exports_external.string().describe("Source name"),
|
|
27080
|
+
type: exports_external.enum(["mcp-registry", "awesome-list", "npm-search", "github-topic"]).describe("Source type"),
|
|
27081
|
+
url: exports_external.string().describe("Source URL endpoint"),
|
|
27082
|
+
description: exports_external.string().optional().describe("Description")
|
|
27083
|
+
}, async ({ name, type, url: url2, description }) => {
|
|
27084
|
+
const source = addSource({ name, type, url: url2, description });
|
|
27085
|
+
return {
|
|
27086
|
+
content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
|
|
27087
|
+
};
|
|
27088
|
+
});
|
|
27089
|
+
server.tool("remove_source", "Remove a search source by ID", { id: exports_external.string().describe("Source ID to remove") }, async ({ id }) => {
|
|
27090
|
+
const existing = getSource(id);
|
|
27091
|
+
if (!existing) {
|
|
26323
27092
|
return {
|
|
26324
|
-
content: [{ type: "text", text: `
|
|
27093
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
26325
27094
|
isError: true
|
|
26326
27095
|
};
|
|
26327
27096
|
}
|
|
27097
|
+
removeSource(id);
|
|
27098
|
+
return {
|
|
27099
|
+
content: [{ type: "text", text: `Removed source: ${existing.name} [${id}]` }]
|
|
27100
|
+
};
|
|
26328
27101
|
});
|
|
26329
|
-
|
|
26330
|
-
const
|
|
26331
|
-
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
27102
|
+
server.tool("enable_source_finder", "Enable a search source", { id: exports_external.string().describe("Source ID to enable") }, async ({ id }) => {
|
|
27103
|
+
const existing = getSource(id);
|
|
27104
|
+
if (!existing) {
|
|
27105
|
+
return {
|
|
27106
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
27107
|
+
isError: true
|
|
27108
|
+
};
|
|
27109
|
+
}
|
|
27110
|
+
enableSource(id);
|
|
27111
|
+
return {
|
|
27112
|
+
content: [{ type: "text", text: `Enabled source: ${existing.name}` }]
|
|
27113
|
+
};
|
|
27114
|
+
});
|
|
27115
|
+
server.tool("disable_source_finder", "Disable a search source", { id: exports_external.string().describe("Source ID to disable") }, async ({ id }) => {
|
|
27116
|
+
const existing = getSource(id);
|
|
27117
|
+
if (!existing) {
|
|
27118
|
+
return {
|
|
27119
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
27120
|
+
isError: true
|
|
27121
|
+
};
|
|
27122
|
+
}
|
|
27123
|
+
disableSource(id);
|
|
27124
|
+
return {
|
|
27125
|
+
content: [{ type: "text", text: `Disabled source: ${existing.name}` }]
|
|
27126
|
+
};
|
|
27127
|
+
});
|
|
27128
|
+
server.tool("install_to_agents", "Install a registered MCP server into Claude Code, Codex, and/or Gemini", {
|
|
27129
|
+
id: exports_external.string().describe("Server ID to install (from list_servers)"),
|
|
27130
|
+
targets: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
|
|
27131
|
+
}, async ({ id, targets }) => {
|
|
27132
|
+
const entry = getServer(id);
|
|
27133
|
+
if (!entry) {
|
|
27134
|
+
return {
|
|
27135
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27136
|
+
isError: true
|
|
27137
|
+
};
|
|
27138
|
+
}
|
|
27139
|
+
const agentTargets = targets ?? ["claude", "codex", "gemini"];
|
|
27140
|
+
const results = installToAgents(entry, agentTargets);
|
|
27141
|
+
return {
|
|
27142
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
27143
|
+
};
|
|
27144
|
+
});
|
|
27145
|
+
server.tool("list_awesome_servers", "List all MCP servers from the curated punkpeye/awesome-mcp-servers GitHub list", {}, async () => {
|
|
27146
|
+
const results = await listAwesomeServers();
|
|
27147
|
+
return {
|
|
27148
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
27149
|
+
};
|
|
27150
|
+
});
|
|
27151
|
+
server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
|
|
27152
|
+
let tools = [];
|
|
27153
|
+
try {
|
|
27154
|
+
await connectAllEnabled();
|
|
27155
|
+
tools = listAllTools();
|
|
27156
|
+
} finally {
|
|
27157
|
+
await disconnectAll().catch(() => {
|
|
27158
|
+
return;
|
|
27159
|
+
});
|
|
27160
|
+
}
|
|
27161
|
+
return {
|
|
27162
|
+
content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
|
|
27163
|
+
};
|
|
27164
|
+
});
|
|
27165
|
+
server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`, {
|
|
27166
|
+
tool_name: exports_external.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
|
|
27167
|
+
arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
|
|
27168
|
+
}, async ({ tool_name, arguments: args }) => {
|
|
27169
|
+
try {
|
|
27170
|
+
const sepIdx = tool_name.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
27171
|
+
if (sepIdx === -1) {
|
|
27172
|
+
return {
|
|
27173
|
+
content: [{ type: "text", text: `Error: Invalid tool name "${tool_name}"` }],
|
|
27174
|
+
isError: true
|
|
27175
|
+
};
|
|
27176
|
+
}
|
|
27177
|
+
const serverId = tool_name.slice(0, sepIdx);
|
|
27178
|
+
const entry = getServer(serverId);
|
|
27179
|
+
if (!entry) {
|
|
27180
|
+
return {
|
|
27181
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" not found.` }],
|
|
27182
|
+
isError: true
|
|
27183
|
+
};
|
|
27184
|
+
}
|
|
27185
|
+
if (!entry.enabled) {
|
|
27186
|
+
return {
|
|
27187
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" is disabled.` }],
|
|
27188
|
+
isError: true
|
|
27189
|
+
};
|
|
27190
|
+
}
|
|
27191
|
+
await connectToServer(entry);
|
|
27192
|
+
const result = await callTool(tool_name, args || {});
|
|
27193
|
+
return { content: result.content };
|
|
27194
|
+
} catch (err) {
|
|
27195
|
+
return {
|
|
27196
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
27197
|
+
isError: true
|
|
27198
|
+
};
|
|
27199
|
+
}
|
|
27200
|
+
});
|
|
27201
|
+
server.tool("diagnose_server", "Run health checks on a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
|
|
27202
|
+
const entry = getServer(id);
|
|
27203
|
+
if (!entry)
|
|
27204
|
+
return { content: [{ type: "text", text: `Server "${id}" not found.` }], isError: true };
|
|
27205
|
+
const report = await diagnoseServer(entry);
|
|
27206
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
27207
|
+
});
|
|
27208
|
+
async function startMcpServer() {
|
|
27209
|
+
const transport = new StdioServerTransport;
|
|
27210
|
+
await server.connect(transport);
|
|
27211
|
+
}
|
|
27212
|
+
var isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/mcp/index.ts") || process.argv[1]?.endsWith("/bin/mcp.js");
|
|
27213
|
+
if (isDirectRun) {
|
|
26335
27214
|
startMcpServer().catch((error2) => {
|
|
26336
27215
|
console.error("MCP server error:", error2);
|
|
26337
27216
|
process.exit(1);
|
|
@@ -26339,27 +27218,31 @@ if (isDirectRun) {
|
|
|
26339
27218
|
}
|
|
26340
27219
|
|
|
26341
27220
|
// src/server/serve.ts
|
|
26342
|
-
import { existsSync } from "fs";
|
|
26343
|
-
import { join as
|
|
26344
|
-
import { fileURLToPath } from "url";
|
|
27221
|
+
import { existsSync as existsSync3 } from "fs";
|
|
27222
|
+
import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
|
|
27223
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
27224
|
+
init_db();
|
|
27225
|
+
function redactServer(server2) {
|
|
27226
|
+
return { ...server2, env: {} };
|
|
27227
|
+
}
|
|
26345
27228
|
function resolveDashboardDir() {
|
|
26346
27229
|
const candidates = [];
|
|
26347
27230
|
try {
|
|
26348
|
-
const scriptDir =
|
|
26349
|
-
candidates.push(
|
|
26350
|
-
candidates.push(
|
|
27231
|
+
const scriptDir = dirname3(fileURLToPath2(import.meta.url));
|
|
27232
|
+
candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
|
|
27233
|
+
candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
|
|
26351
27234
|
} catch {}
|
|
26352
27235
|
if (process.argv[1]) {
|
|
26353
|
-
const mainDir =
|
|
26354
|
-
candidates.push(
|
|
26355
|
-
candidates.push(
|
|
27236
|
+
const mainDir = dirname3(process.argv[1]);
|
|
27237
|
+
candidates.push(join5(mainDir, "..", "dashboard", "dist"));
|
|
27238
|
+
candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
|
|
26356
27239
|
}
|
|
26357
|
-
candidates.push(
|
|
27240
|
+
candidates.push(join5(process.cwd(), "dashboard", "dist"));
|
|
26358
27241
|
for (const candidate of candidates) {
|
|
26359
|
-
if (
|
|
27242
|
+
if (existsSync3(candidate))
|
|
26360
27243
|
return candidate;
|
|
26361
27244
|
}
|
|
26362
|
-
return
|
|
27245
|
+
return join5(process.cwd(), "dashboard", "dist");
|
|
26363
27246
|
}
|
|
26364
27247
|
var MIME_TYPES = {
|
|
26365
27248
|
".html": "text/html; charset=utf-8",
|
|
@@ -26375,13 +27258,15 @@ var MIME_TYPES = {
|
|
|
26375
27258
|
};
|
|
26376
27259
|
var SECURITY_HEADERS = {
|
|
26377
27260
|
"X-Content-Type-Options": "nosniff",
|
|
26378
|
-
"X-Frame-Options": "DENY"
|
|
27261
|
+
"X-Frame-Options": "DENY",
|
|
27262
|
+
"Referrer-Policy": "no-referrer"
|
|
26379
27263
|
};
|
|
26380
27264
|
function json(data, status = 200, port) {
|
|
26381
27265
|
return new Response(JSON.stringify(data), {
|
|
26382
27266
|
status,
|
|
26383
27267
|
headers: {
|
|
26384
27268
|
"Content-Type": "application/json",
|
|
27269
|
+
"Cache-Control": "no-store",
|
|
26385
27270
|
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
26386
27271
|
...SECURITY_HEADERS
|
|
26387
27272
|
}
|
|
@@ -26391,27 +27276,94 @@ function isValidId(id) {
|
|
|
26391
27276
|
return /^[a-z0-9-]+$/.test(id);
|
|
26392
27277
|
}
|
|
26393
27278
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
27279
|
+
function isLoopbackHost(hostname2) {
|
|
27280
|
+
return hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1";
|
|
27281
|
+
}
|
|
27282
|
+
function isAllowedOrigin(req, port, host) {
|
|
27283
|
+
const origin = req.headers.get("origin");
|
|
27284
|
+
if (!origin)
|
|
27285
|
+
return true;
|
|
27286
|
+
if (origin === "null")
|
|
27287
|
+
return false;
|
|
27288
|
+
try {
|
|
27289
|
+
const url2 = new URL(origin);
|
|
27290
|
+
if (url2.port && url2.port !== String(port))
|
|
27291
|
+
return false;
|
|
27292
|
+
if (isLoopbackHost(url2.hostname))
|
|
27293
|
+
return true;
|
|
27294
|
+
const normalizedHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
27295
|
+
return url2.hostname === normalizedHost;
|
|
27296
|
+
} catch {
|
|
27297
|
+
return false;
|
|
27298
|
+
}
|
|
27299
|
+
}
|
|
27300
|
+
function isAuthorized(req, host) {
|
|
27301
|
+
const token = process.env.MCPS_API_TOKEN;
|
|
27302
|
+
if (!token) {
|
|
27303
|
+
return isLoopbackHost(host);
|
|
27304
|
+
}
|
|
27305
|
+
const auth2 = req.headers.get("authorization");
|
|
27306
|
+
if (auth2 === `Bearer ${token}`)
|
|
27307
|
+
return true;
|
|
27308
|
+
return isLoopbackHost(host);
|
|
27309
|
+
}
|
|
27310
|
+
function unauthorizedResponse(port) {
|
|
27311
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
27312
|
+
status: 401,
|
|
27313
|
+
headers: {
|
|
27314
|
+
"Content-Type": "application/json",
|
|
27315
|
+
"Cache-Control": "no-store",
|
|
27316
|
+
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
27317
|
+
"WWW-Authenticate": "Bearer",
|
|
27318
|
+
...SECURITY_HEADERS
|
|
27319
|
+
}
|
|
27320
|
+
});
|
|
27321
|
+
}
|
|
26394
27322
|
function getAllServersWithToolCount() {
|
|
26395
27323
|
const servers = listServers();
|
|
27324
|
+
if (servers.length === 0)
|
|
27325
|
+
return [];
|
|
27326
|
+
const counts = getToolCounts();
|
|
26396
27327
|
return servers.map((s) => ({
|
|
26397
|
-
...s,
|
|
26398
|
-
toolCount:
|
|
27328
|
+
...redactServer(s),
|
|
27329
|
+
toolCount: counts.get(s.id) ?? 0
|
|
26399
27330
|
}));
|
|
26400
27331
|
}
|
|
26401
27332
|
function serveStaticFile(filePath) {
|
|
26402
|
-
if (!
|
|
27333
|
+
if (!existsSync3(filePath))
|
|
26403
27334
|
return null;
|
|
26404
27335
|
const ext = extname(filePath);
|
|
26405
27336
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
26406
27337
|
return new Response(Bun.file(filePath), {
|
|
26407
|
-
headers: { "Content-Type": contentType }
|
|
27338
|
+
headers: { "Content-Type": contentType, ...SECURITY_HEADERS }
|
|
26408
27339
|
});
|
|
26409
27340
|
}
|
|
27341
|
+
function resolveStaticPath(baseDir, urlPath) {
|
|
27342
|
+
let decoded;
|
|
27343
|
+
try {
|
|
27344
|
+
decoded = decodeURIComponent(urlPath);
|
|
27345
|
+
} catch {
|
|
27346
|
+
return null;
|
|
27347
|
+
}
|
|
27348
|
+
const stripped = decoded.replace(/^\/+/, "");
|
|
27349
|
+
const resolved = resolve(baseDir, stripped);
|
|
27350
|
+
const rel = relative(baseDir, resolved);
|
|
27351
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
27352
|
+
return null;
|
|
27353
|
+
}
|
|
27354
|
+
return resolved;
|
|
27355
|
+
}
|
|
27356
|
+
function formatHostForUrl(host) {
|
|
27357
|
+
if (host.includes(":") && !host.startsWith("["))
|
|
27358
|
+
return `[${host}]`;
|
|
27359
|
+
return host;
|
|
27360
|
+
}
|
|
26410
27361
|
async function startServer(port, options) {
|
|
26411
27362
|
const shouldOpen = options?.open ?? true;
|
|
27363
|
+
const host = options?.host ?? "127.0.0.1";
|
|
26412
27364
|
getDb();
|
|
26413
27365
|
const dashboardDir = resolveDashboardDir();
|
|
26414
|
-
const dashboardExists =
|
|
27366
|
+
const dashboardExists = existsSync3(dashboardDir);
|
|
26415
27367
|
if (!dashboardExists) {
|
|
26416
27368
|
console.error(`
|
|
26417
27369
|
Dashboard not found at: ${dashboardDir}`);
|
|
@@ -26422,10 +27374,19 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26422
27374
|
}
|
|
26423
27375
|
const server2 = Bun.serve({
|
|
26424
27376
|
port,
|
|
27377
|
+
hostname: host,
|
|
26425
27378
|
async fetch(req) {
|
|
26426
27379
|
const url2 = new URL(req.url);
|
|
26427
27380
|
const path = url2.pathname;
|
|
26428
27381
|
const method = req.method;
|
|
27382
|
+
if (path.startsWith("/api/") && method !== "OPTIONS") {
|
|
27383
|
+
if (!isAuthorized(req, host)) {
|
|
27384
|
+
return unauthorizedResponse(port);
|
|
27385
|
+
}
|
|
27386
|
+
if (!isAllowedOrigin(req, port, host)) {
|
|
27387
|
+
return json({ error: "Forbidden" }, 403, port);
|
|
27388
|
+
}
|
|
27389
|
+
}
|
|
26429
27390
|
if (path === "/api/servers" && method === "GET") {
|
|
26430
27391
|
return json(getAllServersWithToolCount(), 200, port);
|
|
26431
27392
|
}
|
|
@@ -26434,15 +27395,39 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26434
27395
|
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
26435
27396
|
if (contentLength > MAX_BODY_SIZE)
|
|
26436
27397
|
return json({ error: "Request body too large" }, 413, port);
|
|
26437
|
-
|
|
26438
|
-
|
|
27398
|
+
let body;
|
|
27399
|
+
try {
|
|
27400
|
+
body = await req.json();
|
|
27401
|
+
} catch {
|
|
27402
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27403
|
+
}
|
|
27404
|
+
const command = body.command?.trim();
|
|
27405
|
+
if (!command)
|
|
26439
27406
|
return json({ error: "Missing 'command'" }, 400, port);
|
|
27407
|
+
const transport = body.transport || "stdio";
|
|
27408
|
+
if (!["stdio", "sse", "streamable-http"].includes(transport)) {
|
|
27409
|
+
return json({ error: "Invalid transport type" }, 400, port);
|
|
27410
|
+
}
|
|
27411
|
+
if (transport !== "stdio" && !body.url) {
|
|
27412
|
+
return json({ error: "Missing 'url' for non-stdio transport" }, 400, port);
|
|
27413
|
+
}
|
|
27414
|
+
if (body.url) {
|
|
27415
|
+
try {
|
|
27416
|
+
new URL(body.url);
|
|
27417
|
+
} catch {
|
|
27418
|
+
return json({ error: "Invalid 'url' format" }, 400, port);
|
|
27419
|
+
}
|
|
27420
|
+
}
|
|
27421
|
+
if (body.args && (!Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string"))) {
|
|
27422
|
+
return json({ error: "Invalid 'args' format" }, 400, port);
|
|
27423
|
+
}
|
|
27424
|
+
const args = body.args || [];
|
|
26440
27425
|
const entry = addServer({
|
|
26441
27426
|
name: body.name,
|
|
26442
|
-
command
|
|
26443
|
-
args
|
|
27427
|
+
command,
|
|
27428
|
+
args,
|
|
26444
27429
|
description: body.description,
|
|
26445
|
-
transport
|
|
27430
|
+
transport,
|
|
26446
27431
|
url: body.url
|
|
26447
27432
|
});
|
|
26448
27433
|
return json(entry, 200, port);
|
|
@@ -26459,7 +27444,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26459
27444
|
if (!entry)
|
|
26460
27445
|
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26461
27446
|
const tools = getCachedTools(id);
|
|
26462
|
-
return json({ ...entry, toolCount: tools.length, tools }, 200, port);
|
|
27447
|
+
return json({ ...redactServer(entry), toolCount: tools.length, tools }, 200, port);
|
|
26463
27448
|
}
|
|
26464
27449
|
if (singleMatch && method === "DELETE") {
|
|
26465
27450
|
const id = singleMatch[1];
|
|
@@ -26477,6 +27462,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26477
27462
|
if (!isValidId(id))
|
|
26478
27463
|
return json({ error: "Invalid server ID" }, 400, port);
|
|
26479
27464
|
try {
|
|
27465
|
+
const existing = getServer(id);
|
|
27466
|
+
if (!existing)
|
|
27467
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26480
27468
|
enableServer(id);
|
|
26481
27469
|
return json({ success: true }, 200, port);
|
|
26482
27470
|
} catch (e) {
|
|
@@ -26489,13 +27477,47 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26489
27477
|
if (!isValidId(id))
|
|
26490
27478
|
return json({ error: "Invalid server ID" }, 400, port);
|
|
26491
27479
|
try {
|
|
27480
|
+
const existing = getServer(id);
|
|
27481
|
+
if (!existing)
|
|
27482
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26492
27483
|
disableServer(id);
|
|
26493
27484
|
return json({ success: true }, 200, port);
|
|
26494
27485
|
} catch (e) {
|
|
26495
27486
|
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
26496
27487
|
}
|
|
26497
27488
|
}
|
|
27489
|
+
if (path === "/api/update" && method === "POST") {
|
|
27490
|
+
if (!isLoopbackHost(host)) {
|
|
27491
|
+
return json({ error: "Update only allowed on loopback host" }, 403, port);
|
|
27492
|
+
}
|
|
27493
|
+
try {
|
|
27494
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
27495
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
27496
|
+
const currentVersion = pkg.version;
|
|
27497
|
+
const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
|
|
27498
|
+
encoding: "utf-8"
|
|
27499
|
+
}).trim();
|
|
27500
|
+
if (latest === currentVersion) {
|
|
27501
|
+
return json({ success: true, current: currentVersion, latest, upToDate: true }, 200, port);
|
|
27502
|
+
}
|
|
27503
|
+
execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "pipe" });
|
|
27504
|
+
return json({ success: true, current: currentVersion, latest, upToDate: false, updated: true }, 200, port);
|
|
27505
|
+
} catch (e) {
|
|
27506
|
+
return json({ error: e instanceof Error ? e.message : "Update failed" }, 500, port);
|
|
27507
|
+
}
|
|
27508
|
+
}
|
|
27509
|
+
if (path === "/api/version" && method === "GET") {
|
|
27510
|
+
try {
|
|
27511
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
27512
|
+
return json({ version: pkg.version }, 200, port);
|
|
27513
|
+
} catch {
|
|
27514
|
+
return json({ version: "unknown" }, 200, port);
|
|
27515
|
+
}
|
|
27516
|
+
}
|
|
26498
27517
|
if (method === "OPTIONS") {
|
|
27518
|
+
if (!isAllowedOrigin(req, port, host)) {
|
|
27519
|
+
return json({ error: "Forbidden" }, 403, port);
|
|
27520
|
+
}
|
|
26499
27521
|
return new Response(null, {
|
|
26500
27522
|
headers: {
|
|
26501
27523
|
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
@@ -26506,12 +27528,14 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26506
27528
|
}
|
|
26507
27529
|
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
26508
27530
|
if (path !== "/") {
|
|
26509
|
-
const
|
|
26510
|
-
|
|
26511
|
-
|
|
26512
|
-
|
|
27531
|
+
const safePath = resolveStaticPath(dashboardDir, path);
|
|
27532
|
+
if (safePath) {
|
|
27533
|
+
const res2 = serveStaticFile(safePath);
|
|
27534
|
+
if (res2)
|
|
27535
|
+
return res2;
|
|
27536
|
+
}
|
|
26513
27537
|
}
|
|
26514
|
-
const indexPath =
|
|
27538
|
+
const indexPath = join5(dashboardDir, "index.html");
|
|
26515
27539
|
const res = serveStaticFile(indexPath);
|
|
26516
27540
|
if (res)
|
|
26517
27541
|
return res;
|
|
@@ -26526,19 +27550,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26526
27550
|
};
|
|
26527
27551
|
process.on("SIGINT", shutdown);
|
|
26528
27552
|
process.on("SIGTERM", shutdown);
|
|
26529
|
-
const
|
|
27553
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
27554
|
+
const serverUrl = `http://${formatHostForUrl(displayHost)}:${port}`;
|
|
26530
27555
|
console.log(`MCPs Dashboard running at ${serverUrl}`);
|
|
26531
27556
|
if (shouldOpen) {
|
|
26532
27557
|
try {
|
|
26533
|
-
const {
|
|
26534
|
-
|
|
26535
|
-
|
|
27558
|
+
const { execFile } = await import("child_process");
|
|
27559
|
+
if (process.platform === "win32") {
|
|
27560
|
+
execFile("cmd", ["/c", "start", "", serverUrl]);
|
|
27561
|
+
} else {
|
|
27562
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
27563
|
+
execFile(openCmd, [serverUrl]);
|
|
27564
|
+
}
|
|
26536
27565
|
} catch {}
|
|
26537
27566
|
}
|
|
26538
27567
|
}
|
|
26539
27568
|
|
|
26540
27569
|
// src/cli/components/App.tsx
|
|
26541
|
-
import { useState as useState7 } from "react";
|
|
27570
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
26542
27571
|
import { Box as Box7, Text as Text9, useApp, useInput as useInput4 } from "ink";
|
|
26543
27572
|
|
|
26544
27573
|
// src/cli/components/ServerList.tsx
|
|
@@ -26951,6 +27980,7 @@ var SelectInput_default = SelectInput;
|
|
|
26951
27980
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
26952
27981
|
function ServerList({ onSelect, onSearch }) {
|
|
26953
27982
|
const servers = listServers();
|
|
27983
|
+
const toolCounts = getToolCounts();
|
|
26954
27984
|
useInput2((input) => {
|
|
26955
27985
|
if (input === "s") {
|
|
26956
27986
|
onSearch();
|
|
@@ -26972,10 +28002,9 @@ function ServerList({ onSelect, onSearch }) {
|
|
|
26972
28002
|
}, undefined, true, undefined, this);
|
|
26973
28003
|
}
|
|
26974
28004
|
const items = servers.map((s) => {
|
|
26975
|
-
const
|
|
28005
|
+
const cachedCount = toolCounts.get(s.id) ?? 0;
|
|
26976
28006
|
const status = s.enabled ? "\u25CF" : "\u25CB";
|
|
26977
|
-
const
|
|
26978
|
-
const toolInfo = cached2.length > 0 ? ` (${cached2.length} tools)` : "";
|
|
28007
|
+
const toolInfo = cachedCount > 0 ? ` (${cachedCount} tools)` : "";
|
|
26979
28008
|
return {
|
|
26980
28009
|
label: `${status} ${s.name} [${s.id}]${toolInfo}`,
|
|
26981
28010
|
value: s.id,
|
|
@@ -27033,7 +28062,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
|
|
|
27033
28062
|
const [loading, setLoading] = useState3(false);
|
|
27034
28063
|
const [error2, setError] = useState3(null);
|
|
27035
28064
|
const cachedTools = getCachedTools(server2.id);
|
|
28065
|
+
const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
|
|
27036
28066
|
useEffect3(() => {
|
|
28067
|
+
setLoading(false);
|
|
28068
|
+
setError(null);
|
|
27037
28069
|
if (cachedTools.length > 0) {
|
|
27038
28070
|
setTools(cachedTools.map((t) => ({
|
|
27039
28071
|
server_id: server2.id,
|
|
@@ -27041,8 +28073,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
|
|
|
27041
28073
|
description: t.description,
|
|
27042
28074
|
input_schema: t.input_schema
|
|
27043
28075
|
})));
|
|
28076
|
+
} else {
|
|
28077
|
+
setTools([]);
|
|
27044
28078
|
}
|
|
27045
|
-
}, [server2.id]);
|
|
28079
|
+
}, [server2.id, cachedKey]);
|
|
27046
28080
|
const handleConnect = async () => {
|
|
27047
28081
|
setLoading(true);
|
|
27048
28082
|
setError(null);
|
|
@@ -27366,6 +28400,7 @@ function SearchView({ onBack }) {
|
|
|
27366
28400
|
// src/cli/components/ToolCall.tsx
|
|
27367
28401
|
import { useState as useState6 } from "react";
|
|
27368
28402
|
import { Box as Box6, Text as Text8 } from "ink";
|
|
28403
|
+
init_config();
|
|
27369
28404
|
import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
|
|
27370
28405
|
function ToolCall({ server: server2, tool, onBack }) {
|
|
27371
28406
|
const [argsInput, setArgsInput] = useState6("{}");
|
|
@@ -27496,6 +28531,7 @@ function ToolCall({ server: server2, tool, onBack }) {
|
|
|
27496
28531
|
}
|
|
27497
28532
|
|
|
27498
28533
|
// src/cli/components/App.tsx
|
|
28534
|
+
init_db();
|
|
27499
28535
|
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
27500
28536
|
function App() {
|
|
27501
28537
|
const { exit } = useApp();
|
|
@@ -27503,7 +28539,7 @@ function App() {
|
|
|
27503
28539
|
const [selectedServer, setSelectedServer] = useState7(null);
|
|
27504
28540
|
const [selectedTool, setSelectedTool] = useState7(null);
|
|
27505
28541
|
useInput4((input, key) => {
|
|
27506
|
-
if (input === "q" && view
|
|
28542
|
+
if (input === "q" && view !== "search" && view !== "call") {
|
|
27507
28543
|
exit();
|
|
27508
28544
|
}
|
|
27509
28545
|
if (key.escape) {
|
|
@@ -27520,6 +28556,13 @@ function App() {
|
|
|
27520
28556
|
}
|
|
27521
28557
|
}
|
|
27522
28558
|
});
|
|
28559
|
+
useEffect5(() => {
|
|
28560
|
+
return () => {
|
|
28561
|
+
disconnectAll().catch(() => {
|
|
28562
|
+
return;
|
|
28563
|
+
}).finally(() => closeDb());
|
|
28564
|
+
};
|
|
28565
|
+
}, []);
|
|
27523
28566
|
const handleSelectServer = (server2) => {
|
|
27524
28567
|
setSelectedServer(server2);
|
|
27525
28568
|
setView("detail");
|
|
@@ -27581,7 +28624,7 @@ function App() {
|
|
|
27581
28624
|
marginTop: 1,
|
|
27582
28625
|
children: /* @__PURE__ */ jsxDEV5(Text9, {
|
|
27583
28626
|
dimColor: true,
|
|
27584
|
-
children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : "esc back \xB7 q quit"
|
|
28627
|
+
children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : view === "detail" ? "esc back \xB7 q quit" : "esc back"
|
|
27585
28628
|
}, undefined, false, undefined, this)
|
|
27586
28629
|
}, undefined, false, undefined, this)
|
|
27587
28630
|
]
|
|
@@ -27589,20 +28632,37 @@ function App() {
|
|
|
27589
28632
|
}
|
|
27590
28633
|
|
|
27591
28634
|
// src/cli/index.tsx
|
|
28635
|
+
var VERSION2 = (() => {
|
|
28636
|
+
try {
|
|
28637
|
+
const pkgPath = join6(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
|
|
28638
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
28639
|
+
return pkg.version || "0.0.1";
|
|
28640
|
+
} catch {
|
|
28641
|
+
return "0.0.1";
|
|
28642
|
+
}
|
|
28643
|
+
})();
|
|
27592
28644
|
var program2 = new Command;
|
|
27593
|
-
program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(
|
|
27594
|
-
program2.command("list").description("List registered MCP servers").action(() => {
|
|
28645
|
+
program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
|
|
28646
|
+
program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").action((opts) => {
|
|
27595
28647
|
const servers = listServers();
|
|
28648
|
+
if (opts.json) {
|
|
28649
|
+
const toolCounts2 = getToolCounts();
|
|
28650
|
+
console.log(JSON.stringify(servers.map((s) => ({ ...s, toolCount: toolCounts2.get(s.id) ?? 0 })), null, 2));
|
|
28651
|
+
closeDb();
|
|
28652
|
+
return;
|
|
28653
|
+
}
|
|
27596
28654
|
if (servers.length === 0) {
|
|
27597
28655
|
console.log(chalk2.dim("No servers registered. Use `mcps add` or `mcps search` to get started."));
|
|
27598
28656
|
closeDb();
|
|
27599
28657
|
return;
|
|
27600
28658
|
}
|
|
28659
|
+
const toolCounts = getToolCounts();
|
|
27601
28660
|
for (const s of servers) {
|
|
27602
28661
|
const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
|
|
27603
|
-
const
|
|
27604
|
-
const toolCount =
|
|
27605
|
-
|
|
28662
|
+
const cachedCount = toolCounts.get(s.id) ?? 0;
|
|
28663
|
+
const toolCount = cachedCount > 0 ? chalk2.dim(` (${cachedCount} tools)`) : "";
|
|
28664
|
+
const errorWarning = s.last_error ? chalk2.red(" \u26A0") : "";
|
|
28665
|
+
console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
|
|
27606
28666
|
if (s.description)
|
|
27607
28667
|
console.log(` ${chalk2.dim(s.description)}`);
|
|
27608
28668
|
console.log(` ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
|
|
@@ -27630,8 +28690,9 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
|
|
|
27630
28690
|
} catch (err) {
|
|
27631
28691
|
console.error(chalk2.red(`Search failed: ${err.message}`));
|
|
27632
28692
|
process.exit(1);
|
|
28693
|
+
} finally {
|
|
28694
|
+
closeDb();
|
|
27633
28695
|
}
|
|
27634
|
-
closeDb();
|
|
27635
28696
|
});
|
|
27636
28697
|
program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").description("Add a local MCP server").action(async (command, args, opts) => {
|
|
27637
28698
|
try {
|
|
@@ -27645,12 +28706,15 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
27645
28706
|
}
|
|
27646
28707
|
if (!command) {
|
|
27647
28708
|
console.error(chalk2.red("Error: command is required (or use --from-registry)"));
|
|
28709
|
+
closeDb();
|
|
27648
28710
|
process.exit(1);
|
|
27649
28711
|
}
|
|
27650
28712
|
const envMap = {};
|
|
27651
28713
|
if (opts.env) {
|
|
27652
28714
|
for (const pair of opts.env) {
|
|
27653
28715
|
const [key, ...rest] = pair.split("=");
|
|
28716
|
+
if (!key)
|
|
28717
|
+
continue;
|
|
27654
28718
|
envMap[key] = rest.join("=");
|
|
27655
28719
|
}
|
|
27656
28720
|
}
|
|
@@ -27671,6 +28735,7 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
27671
28735
|
} else {
|
|
27672
28736
|
console.error(chalk2.red(`Failed to add server: ${err.message}`));
|
|
27673
28737
|
}
|
|
28738
|
+
closeDb();
|
|
27674
28739
|
process.exit(1);
|
|
27675
28740
|
}
|
|
27676
28741
|
closeDb();
|
|
@@ -27679,6 +28744,7 @@ program2.command("remove").argument("<id>", "Server ID to remove").description("
|
|
|
27679
28744
|
const server2 = getServer(id);
|
|
27680
28745
|
if (!server2) {
|
|
27681
28746
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
28747
|
+
closeDb();
|
|
27682
28748
|
process.exit(1);
|
|
27683
28749
|
}
|
|
27684
28750
|
removeServer(id);
|
|
@@ -27689,6 +28755,7 @@ program2.command("enable").argument("<id>", "Server ID to enable").description("
|
|
|
27689
28755
|
const server2 = getServer(id);
|
|
27690
28756
|
if (!server2) {
|
|
27691
28757
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
28758
|
+
closeDb();
|
|
27692
28759
|
process.exit(1);
|
|
27693
28760
|
}
|
|
27694
28761
|
enableServer(id);
|
|
@@ -27699,6 +28766,7 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
|
|
|
27699
28766
|
const server2 = getServer(id);
|
|
27700
28767
|
if (!server2) {
|
|
27701
28768
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
28769
|
+
closeDb();
|
|
27702
28770
|
process.exit(1);
|
|
27703
28771
|
}
|
|
27704
28772
|
disableServer(id);
|
|
@@ -27778,6 +28846,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
|
|
|
27778
28846
|
}
|
|
27779
28847
|
}
|
|
27780
28848
|
}
|
|
28849
|
+
let exitCode = 0;
|
|
27781
28850
|
try {
|
|
27782
28851
|
console.log(chalk2.dim(`Connecting to servers...`));
|
|
27783
28852
|
await connectAllEnabled();
|
|
@@ -27786,17 +28855,22 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
|
|
|
27786
28855
|
for (const c of result.content) {
|
|
27787
28856
|
console.log(c.text);
|
|
27788
28857
|
}
|
|
27789
|
-
await disconnectAll();
|
|
27790
28858
|
} catch (err) {
|
|
27791
28859
|
console.error(chalk2.red(`Call failed: ${err.message}`));
|
|
27792
|
-
|
|
28860
|
+
exitCode = 1;
|
|
27793
28861
|
}
|
|
28862
|
+
await disconnectAll().catch(() => {
|
|
28863
|
+
return;
|
|
28864
|
+
});
|
|
27794
28865
|
closeDb();
|
|
28866
|
+
if (exitCode !== 0)
|
|
28867
|
+
process.exit(exitCode);
|
|
27795
28868
|
});
|
|
27796
28869
|
program2.command("info").argument("<id>", "Server ID").description("Show server details & tools").action((id) => {
|
|
27797
28870
|
const server2 = getServer(id);
|
|
27798
28871
|
if (!server2) {
|
|
27799
28872
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
28873
|
+
closeDb();
|
|
27800
28874
|
process.exit(1);
|
|
27801
28875
|
}
|
|
27802
28876
|
console.log(chalk2.bold(server2.name) + " " + chalk2.dim(`[${server2.id}]`));
|
|
@@ -27825,21 +28899,485 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
|
|
|
27825
28899
|
}
|
|
27826
28900
|
closeDb();
|
|
27827
28901
|
});
|
|
27828
|
-
program2.command("status").description("Show registry stats").action(() => {
|
|
28902
|
+
program2.command("status").description("Show registry stats").option("--json", "Output as JSON").action((opts) => {
|
|
27829
28903
|
const servers = listServers();
|
|
27830
28904
|
const enabled = servers.filter((s) => s.enabled).length;
|
|
27831
28905
|
const disabled = servers.length - enabled;
|
|
28906
|
+
const toolCounts = getToolCounts();
|
|
27832
28907
|
let totalTools = 0;
|
|
27833
|
-
for (const s of servers)
|
|
27834
|
-
totalTools +=
|
|
28908
|
+
for (const s of servers)
|
|
28909
|
+
totalTools += toolCounts.get(s.id) ?? 0;
|
|
28910
|
+
if (opts.json) {
|
|
28911
|
+
console.log(JSON.stringify({ total: servers.length, enabled, disabled, totalTools }, null, 2));
|
|
28912
|
+
closeDb();
|
|
28913
|
+
return;
|
|
27835
28914
|
}
|
|
27836
28915
|
console.log(chalk2.bold("Registry Status"));
|
|
27837
28916
|
console.log(` Servers: ${servers.length} (${chalk2.green(`${enabled} enabled`)}, ${chalk2.red(`${disabled} disabled`)})`);
|
|
27838
28917
|
console.log(` Tools: ${totalTools} (cached)`);
|
|
27839
28918
|
closeDb();
|
|
27840
28919
|
});
|
|
27841
|
-
program2.command("
|
|
27842
|
-
|
|
28920
|
+
program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").action(async (id) => {
|
|
28921
|
+
const servers = id ? [getServer(id)].filter(Boolean) : listServers();
|
|
28922
|
+
if (servers.length === 0) {
|
|
28923
|
+
console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
|
|
28924
|
+
closeDb();
|
|
28925
|
+
return;
|
|
28926
|
+
}
|
|
28927
|
+
let allHealthy = true;
|
|
28928
|
+
for (const server2 of servers) {
|
|
28929
|
+
console.log(chalk2.bold(`
|
|
28930
|
+
${server2.name} [${server2.id}]`));
|
|
28931
|
+
const report = await diagnoseServer(server2);
|
|
28932
|
+
for (const check2 of report.checks) {
|
|
28933
|
+
const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
|
|
28934
|
+
console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
|
|
28935
|
+
}
|
|
28936
|
+
if (!report.healthy)
|
|
28937
|
+
allHealthy = false;
|
|
28938
|
+
}
|
|
28939
|
+
console.log("");
|
|
28940
|
+
if (allHealthy) {
|
|
28941
|
+
console.log(chalk2.green("All checks passed."));
|
|
28942
|
+
} else {
|
|
28943
|
+
console.log(chalk2.red("Some checks failed. Fix issues above."));
|
|
28944
|
+
}
|
|
28945
|
+
closeDb();
|
|
28946
|
+
});
|
|
28947
|
+
program2.command("serve").description("Start the web dashboard").option("--port <port>", "Port to listen on", "19427").option("--host <host>", "Host to bind (default: 127.0.0.1)", "127.0.0.1").option("--no-open", "Don't open browser automatically").action(async (opts) => {
|
|
28948
|
+
await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
|
|
28949
|
+
});
|
|
28950
|
+
program2.command("update").description("Update mcps to the latest version").action(async () => {
|
|
28951
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
28952
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
28953
|
+
const currentVersion = pkg.version;
|
|
28954
|
+
console.log(chalk2.dim(`Current version: ${currentVersion}`));
|
|
28955
|
+
console.log(chalk2.dim("Checking for updates..."));
|
|
28956
|
+
try {
|
|
28957
|
+
const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
|
|
28958
|
+
encoding: "utf-8"
|
|
28959
|
+
}).trim();
|
|
28960
|
+
if (latest === currentVersion) {
|
|
28961
|
+
console.log(chalk2.green(`Already on the latest version (${currentVersion}).`));
|
|
28962
|
+
return;
|
|
28963
|
+
}
|
|
28964
|
+
console.log(chalk2.dim(`New version available: ${latest}`));
|
|
28965
|
+
console.log(chalk2.dim("Updating..."));
|
|
28966
|
+
execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "inherit" });
|
|
28967
|
+
console.log(chalk2.green(`Updated to ${latest}`));
|
|
28968
|
+
} catch (err) {
|
|
28969
|
+
console.error(chalk2.red(`Update failed: ${err.message}`));
|
|
28970
|
+
closeDb();
|
|
28971
|
+
process.exit(1);
|
|
28972
|
+
}
|
|
28973
|
+
});
|
|
28974
|
+
program2.command("find").argument("[query]", "Search query (omit to list all from awesome list)").description("Find MCP servers across npm, GitHub, official registry, and awesome lists").option("--source <sources...>", "Source IDs to search (see `mcps sources list`)").option("--limit <n>", "Max results per source", "20").option("--awesome", "List curated servers from punkpeye/awesome-mcp-servers").option("--json", "Output as JSON").option("--install", "After showing results, prompt to select one and install it").option("--yes", "Auto-install without prompting (only when there is exactly 1 result)").option("--no-cache", "Bypass source cache and fetch fresh results").action(async (query, opts) => {
|
|
28975
|
+
try {
|
|
28976
|
+
if (opts.awesome) {
|
|
28977
|
+
console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
|
|
28978
|
+
const results2 = await listAwesomeServers();
|
|
28979
|
+
if (opts.json) {
|
|
28980
|
+
console.log(JSON.stringify(results2, null, 2));
|
|
28981
|
+
closeDb();
|
|
28982
|
+
return;
|
|
28983
|
+
}
|
|
28984
|
+
const allSources2 = listSources();
|
|
28985
|
+
const sourceNameMap2 = new Map(allSources2.map((s) => [s.id, s.name]));
|
|
28986
|
+
for (const r of results2) {
|
|
28987
|
+
const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
|
|
28988
|
+
console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
|
|
28989
|
+
if (r.description)
|
|
28990
|
+
console.log(` ${chalk2.dim(r.description)}`);
|
|
28991
|
+
if (r.url)
|
|
28992
|
+
console.log(` ${chalk2.cyan(r.url)}`);
|
|
28993
|
+
}
|
|
28994
|
+
console.log(chalk2.dim(`
|
|
28995
|
+
${results2.length} servers in awesome list.`));
|
|
28996
|
+
closeDb();
|
|
28997
|
+
return;
|
|
28998
|
+
}
|
|
28999
|
+
const q = query || "";
|
|
29000
|
+
const sources = opts.source;
|
|
29001
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
29002
|
+
const noCache = opts.cache === false;
|
|
29003
|
+
if (!q) {
|
|
29004
|
+
console.log(chalk2.dim("Tip: provide a query to search, or use --awesome to browse the curated list."));
|
|
29005
|
+
} else {
|
|
29006
|
+
console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
|
|
29007
|
+
}
|
|
29008
|
+
const results = await findServers(q, { sources, limit, noCache });
|
|
29009
|
+
if (opts.json) {
|
|
29010
|
+
console.log(JSON.stringify(results, null, 2));
|
|
29011
|
+
closeDb();
|
|
29012
|
+
return;
|
|
29013
|
+
}
|
|
29014
|
+
if (results.length === 0) {
|
|
29015
|
+
console.log(chalk2.dim("No servers found."));
|
|
29016
|
+
closeDb();
|
|
29017
|
+
return;
|
|
29018
|
+
}
|
|
29019
|
+
const sourceColors = {
|
|
29020
|
+
registry: chalk2.blue,
|
|
29021
|
+
npm: chalk2.red,
|
|
29022
|
+
awesome: chalk2.yellow,
|
|
29023
|
+
github: chalk2.magenta
|
|
29024
|
+
};
|
|
29025
|
+
const allSources = listSources();
|
|
29026
|
+
const sourceNameMap = new Map(allSources.map((s) => [s.id, s.name]));
|
|
29027
|
+
for (let i = 0;i < results.length; i++) {
|
|
29028
|
+
const r = results[i];
|
|
29029
|
+
const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
|
|
29030
|
+
const sourceLabel = (sourceColors[r.source] ?? chalk2.dim)(`[${sourceName}]`);
|
|
29031
|
+
const stars = r.stars ? chalk2.dim(` \u2605${r.stars}`) : "";
|
|
29032
|
+
const idx = opts.install ? chalk2.dim(`${i + 1}. `) : " ";
|
|
29033
|
+
console.log(`${idx}${chalk2.bold(r.name)} ${sourceLabel}${stars}`);
|
|
29034
|
+
if (r.description)
|
|
29035
|
+
console.log(` ${chalk2.dim(r.description)}`);
|
|
29036
|
+
if (r.installCmd)
|
|
29037
|
+
console.log(` ${chalk2.green(`Install: ${r.installCmd}`)}`);
|
|
29038
|
+
else if (r.url)
|
|
29039
|
+
console.log(` ${chalk2.cyan(r.url)}`);
|
|
29040
|
+
}
|
|
29041
|
+
console.log(chalk2.dim(`
|
|
29042
|
+
${results.length} result(s). Use \`mcps add --from-registry <id>\` or \`mcps add npx -y <pkg>\` to install.`));
|
|
29043
|
+
if (opts.install) {
|
|
29044
|
+
let chosen = results[0];
|
|
29045
|
+
if (results.length === 1 && opts.yes) {} else {
|
|
29046
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29047
|
+
const answer = await new Promise((resolve2) => {
|
|
29048
|
+
rl.question(chalk2.cyan(`
|
|
29049
|
+
Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
|
|
29050
|
+
});
|
|
29051
|
+
rl.close();
|
|
29052
|
+
const num = parseInt(answer, 10);
|
|
29053
|
+
if (!num || num < 1 || num > results.length) {
|
|
29054
|
+
console.log(chalk2.dim("Installation cancelled."));
|
|
29055
|
+
closeDb();
|
|
29056
|
+
return;
|
|
29057
|
+
}
|
|
29058
|
+
chosen = results[num - 1];
|
|
29059
|
+
}
|
|
29060
|
+
if (!chosen.npmPackage && !chosen.installCmd) {
|
|
29061
|
+
console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
|
|
29062
|
+
closeDb();
|
|
29063
|
+
return;
|
|
29064
|
+
}
|
|
29065
|
+
const pkg = chosen.npmPackage ?? chosen.installCmd?.replace(/^npx -y /, "");
|
|
29066
|
+
if (!pkg) {
|
|
29067
|
+
console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
|
|
29068
|
+
closeDb();
|
|
29069
|
+
return;
|
|
29070
|
+
}
|
|
29071
|
+
console.log(chalk2.dim(`Installing ${chosen.name}...`));
|
|
29072
|
+
const server2 = addServer({
|
|
29073
|
+
command: "npx",
|
|
29074
|
+
args: ["-y", pkg],
|
|
29075
|
+
name: chosen.name,
|
|
29076
|
+
description: chosen.description,
|
|
29077
|
+
transport: "stdio"
|
|
29078
|
+
});
|
|
29079
|
+
const results2 = installToAgents(server2, ["claude", "codex", "gemini"]);
|
|
29080
|
+
for (const r of results2) {
|
|
29081
|
+
if (r.success) {
|
|
29082
|
+
console.log(chalk2.green(` \u2713 ${r.agent}`));
|
|
29083
|
+
} else {
|
|
29084
|
+
console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
|
|
29085
|
+
}
|
|
29086
|
+
}
|
|
29087
|
+
console.log(chalk2.green(`
|
|
29088
|
+
Installed ${server2.name} [${server2.id}]`));
|
|
29089
|
+
}
|
|
29090
|
+
} catch (err) {
|
|
29091
|
+
console.error(chalk2.red(`Find failed: ${err.message}`));
|
|
29092
|
+
process.exit(1);
|
|
29093
|
+
} finally {
|
|
29094
|
+
closeDb();
|
|
29095
|
+
}
|
|
29096
|
+
});
|
|
29097
|
+
var sourcesCmd = program2.command("sources").description("Manage search sources");
|
|
29098
|
+
sourcesCmd.command("list").description("List all search sources").action(() => {
|
|
29099
|
+
const sources = listSources();
|
|
29100
|
+
if (sources.length === 0) {
|
|
29101
|
+
console.log(chalk2.dim("No sources configured."));
|
|
29102
|
+
closeDb();
|
|
29103
|
+
return;
|
|
29104
|
+
}
|
|
29105
|
+
for (const s of sources) {
|
|
29106
|
+
const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
|
|
29107
|
+
console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${chalk2.dim(s.type)} \u2014 ${status}`);
|
|
29108
|
+
if (s.description)
|
|
29109
|
+
console.log(` ${chalk2.dim(s.description)}`);
|
|
29110
|
+
console.log(` ${chalk2.cyan(s.url)}`);
|
|
29111
|
+
}
|
|
29112
|
+
closeDb();
|
|
29113
|
+
});
|
|
29114
|
+
sourcesCmd.command("add").description("Add a new search source").option("--name <name>", "Source name (required)").option("--type <type>", "Source type: mcp-registry, awesome-list, npm-search, github-topic (required)").option("--url <url>", "Source URL (required)").option("--description <desc>", "Description").option("--test", "Test the source after adding by running a sample search").action(async (opts) => {
|
|
29115
|
+
if (!opts.name || !opts.type || !opts.url) {
|
|
29116
|
+
console.error(chalk2.red("Error: --name, --type, and --url are required"));
|
|
29117
|
+
closeDb();
|
|
29118
|
+
process.exit(1);
|
|
29119
|
+
}
|
|
29120
|
+
const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
|
|
29121
|
+
if (!validTypes.includes(opts.type)) {
|
|
29122
|
+
console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
|
|
29123
|
+
closeDb();
|
|
29124
|
+
process.exit(1);
|
|
29125
|
+
}
|
|
29126
|
+
try {
|
|
29127
|
+
const source = addSource({
|
|
29128
|
+
name: opts.name,
|
|
29129
|
+
type: opts.type,
|
|
29130
|
+
url: opts.url,
|
|
29131
|
+
description: opts.description
|
|
29132
|
+
});
|
|
29133
|
+
console.log(chalk2.green(`Added source: ${source.name} [${source.id}]`));
|
|
29134
|
+
if (opts.test) {
|
|
29135
|
+
console.log(chalk2.dim("Testing source..."));
|
|
29136
|
+
const testResults = await searchSource(source, "");
|
|
29137
|
+
console.log(chalk2.dim(` Found ${testResults.length} results`));
|
|
29138
|
+
if (testResults.length === 0)
|
|
29139
|
+
console.log(chalk2.yellow(" Warning: source returned no results"));
|
|
29140
|
+
}
|
|
29141
|
+
} catch (err) {
|
|
29142
|
+
console.error(chalk2.red(`Failed to add source: ${err.message}`));
|
|
29143
|
+
closeDb();
|
|
29144
|
+
process.exit(1);
|
|
29145
|
+
}
|
|
29146
|
+
closeDb();
|
|
29147
|
+
});
|
|
29148
|
+
sourcesCmd.command("remove").argument("<id>", "Source ID to remove").description("Remove a search source").action((id) => {
|
|
29149
|
+
const source = getSource(id);
|
|
29150
|
+
if (!source) {
|
|
29151
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29152
|
+
closeDb();
|
|
29153
|
+
process.exit(1);
|
|
29154
|
+
}
|
|
29155
|
+
removeSource(id);
|
|
29156
|
+
console.log(chalk2.green(`Removed source: ${source.name} [${id}]`));
|
|
29157
|
+
closeDb();
|
|
29158
|
+
});
|
|
29159
|
+
sourcesCmd.command("enable").argument("<id>", "Source ID to enable").description("Enable a search source").action((id) => {
|
|
29160
|
+
const source = getSource(id);
|
|
29161
|
+
if (!source) {
|
|
29162
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29163
|
+
closeDb();
|
|
29164
|
+
process.exit(1);
|
|
29165
|
+
}
|
|
29166
|
+
enableSource(id);
|
|
29167
|
+
console.log(chalk2.green(`Enabled source: ${source.name}`));
|
|
29168
|
+
closeDb();
|
|
29169
|
+
});
|
|
29170
|
+
sourcesCmd.command("disable").argument("<id>", "Source ID to disable").description("Disable a search source").action((id) => {
|
|
29171
|
+
const source = getSource(id);
|
|
29172
|
+
if (!source) {
|
|
29173
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29174
|
+
closeDb();
|
|
29175
|
+
process.exit(1);
|
|
29176
|
+
}
|
|
29177
|
+
disableSource(id);
|
|
29178
|
+
console.log(chalk2.yellow(`Disabled source: ${source.name}`));
|
|
29179
|
+
closeDb();
|
|
29180
|
+
});
|
|
29181
|
+
sourcesCmd.command("refresh").argument("[id]", "Source ID to refresh (omit to refresh all)").description("Clear cached results for a source").action((id) => {
|
|
29182
|
+
clearCache(id);
|
|
29183
|
+
if (id) {
|
|
29184
|
+
console.log(chalk2.green(`Cleared cache for source: ${id}`));
|
|
29185
|
+
} else {
|
|
29186
|
+
console.log(chalk2.green("Cleared all source caches."));
|
|
29187
|
+
}
|
|
29188
|
+
closeDb();
|
|
29189
|
+
});
|
|
29190
|
+
sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Test a source by running a sample search").action(async (id) => {
|
|
29191
|
+
const source = getSource(id);
|
|
29192
|
+
if (!source) {
|
|
29193
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29194
|
+
closeDb();
|
|
29195
|
+
process.exit(1);
|
|
29196
|
+
}
|
|
29197
|
+
console.log(chalk2.dim(`Testing source "${source.name}"...`));
|
|
29198
|
+
try {
|
|
29199
|
+
const results = await searchSource(source, "", true);
|
|
29200
|
+
console.log(chalk2.green(`\u2713 Source returned ${results.length} results`));
|
|
29201
|
+
if (results.length > 0) {
|
|
29202
|
+
console.log(chalk2.dim(" Sample results:"));
|
|
29203
|
+
for (const r of results.slice(0, 3)) {
|
|
29204
|
+
console.log(` ${chalk2.bold(r.name)}: ${chalk2.dim(r.description?.slice(0, 60) || "no description")}`);
|
|
29205
|
+
}
|
|
29206
|
+
}
|
|
29207
|
+
} catch (err) {
|
|
29208
|
+
console.error(chalk2.red(`\u2717 Source test failed: ${err.message}`));
|
|
29209
|
+
}
|
|
29210
|
+
closeDb();
|
|
29211
|
+
});
|
|
29212
|
+
program2.command("install").argument("[id]", "Server ID (from `mcps list`) to install into AI agents").description("Install a registered MCP server into Claude Code, Codex, and/or Gemini").option("--claude", "Install to Claude Code").option("--codex", "Install to Codex").option("--gemini", "Install to Gemini").option("--all", "Install to all agents (default if none specified)").option("--to <agents...>", "Agents to install to: claude, codex, gemini").option("--from-registry <id>", "Add from official registry and install in one step").option("--npm <package>", "Add an npm package as a server and install in one step").action(async (id, opts) => {
|
|
29213
|
+
const targets = [];
|
|
29214
|
+
if (opts.to) {
|
|
29215
|
+
for (const t of opts.to) {
|
|
29216
|
+
if (t === "claude" || t === "codex" || t === "gemini")
|
|
29217
|
+
targets.push(t);
|
|
29218
|
+
}
|
|
29219
|
+
}
|
|
29220
|
+
if (opts.claude && !targets.includes("claude"))
|
|
29221
|
+
targets.push("claude");
|
|
29222
|
+
if (opts.codex && !targets.includes("codex"))
|
|
29223
|
+
targets.push("codex");
|
|
29224
|
+
if (opts.gemini && !targets.includes("gemini"))
|
|
29225
|
+
targets.push("gemini");
|
|
29226
|
+
if (opts.all || targets.length === 0) {
|
|
29227
|
+
if (!targets.includes("claude"))
|
|
29228
|
+
targets.push("claude");
|
|
29229
|
+
if (!targets.includes("codex"))
|
|
29230
|
+
targets.push("codex");
|
|
29231
|
+
if (!targets.includes("gemini"))
|
|
29232
|
+
targets.push("gemini");
|
|
29233
|
+
}
|
|
29234
|
+
let server2;
|
|
29235
|
+
if (opts.fromRegistry) {
|
|
29236
|
+
console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
|
|
29237
|
+
try {
|
|
29238
|
+
server2 = await installFromRegistry(opts.fromRegistry);
|
|
29239
|
+
console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
|
|
29240
|
+
} catch (err) {
|
|
29241
|
+
console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
|
|
29242
|
+
closeDb();
|
|
29243
|
+
process.exit(1);
|
|
29244
|
+
}
|
|
29245
|
+
} else if (opts.npm) {
|
|
29246
|
+
const pkg = opts.npm;
|
|
29247
|
+
server2 = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
|
|
29248
|
+
console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
|
|
29249
|
+
} else {
|
|
29250
|
+
if (!id) {
|
|
29251
|
+
console.error(chalk2.red("Error: server ID is required (or use --from-registry or --npm)"));
|
|
29252
|
+
closeDb();
|
|
29253
|
+
process.exit(1);
|
|
29254
|
+
}
|
|
29255
|
+
server2 = getServer(id);
|
|
29256
|
+
if (!server2) {
|
|
29257
|
+
console.error(chalk2.red(`Server "${id}" not found. Use \`mcps add\` first.`));
|
|
29258
|
+
closeDb();
|
|
29259
|
+
process.exit(1);
|
|
29260
|
+
}
|
|
29261
|
+
}
|
|
29262
|
+
console.log(chalk2.dim(`Installing "${server2.name}" to: ${targets.join(", ")}...`));
|
|
29263
|
+
const results = installToAgents(server2, targets);
|
|
29264
|
+
for (const r of results) {
|
|
29265
|
+
if (r.success) {
|
|
29266
|
+
console.log(chalk2.green(` \u2713 ${r.agent}`));
|
|
29267
|
+
} else {
|
|
29268
|
+
console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
|
|
29269
|
+
}
|
|
29270
|
+
}
|
|
29271
|
+
closeDb();
|
|
29272
|
+
});
|
|
29273
|
+
program2.command("export").description("Export all servers and sources to a JSON file").option("--file <path>", "Output file path", `${process.env.HOME ?? "~"}/.mcps/export.json`).option("--stdout", "Write to stdout instead of a file").action((opts) => {
|
|
29274
|
+
const servers = listServers();
|
|
29275
|
+
const sources = listSources();
|
|
29276
|
+
const payload = {
|
|
29277
|
+
version: 1,
|
|
29278
|
+
exported_at: new Date().toISOString(),
|
|
29279
|
+
servers,
|
|
29280
|
+
sources
|
|
29281
|
+
};
|
|
29282
|
+
const json2 = JSON.stringify(payload, null, 2);
|
|
29283
|
+
if (opts.stdout) {
|
|
29284
|
+
console.log(json2);
|
|
29285
|
+
} else {
|
|
29286
|
+
writeFileSync3(opts.file, json2, "utf-8");
|
|
29287
|
+
console.log(chalk2.green(`Exported ${servers.length} server(s) and ${sources.length} source(s) to ${opts.file}`));
|
|
29288
|
+
}
|
|
29289
|
+
closeDb();
|
|
29290
|
+
});
|
|
29291
|
+
program2.command("import").argument("<file>", "Path to the export JSON file").description("Import servers and sources from a JSON export file").option("--overwrite", "Overwrite existing entries with matching IDs").action((file, opts) => {
|
|
29292
|
+
let payload;
|
|
29293
|
+
try {
|
|
29294
|
+
payload = JSON.parse(readFileSync4(file, "utf-8"));
|
|
29295
|
+
} catch (err) {
|
|
29296
|
+
console.error(chalk2.red(`Failed to read file: ${err.message}`));
|
|
29297
|
+
closeDb();
|
|
29298
|
+
process.exit(1);
|
|
29299
|
+
}
|
|
29300
|
+
if (!Array.isArray(payload.servers) || !Array.isArray(payload.sources)) {
|
|
29301
|
+
console.error(chalk2.red("Invalid export file format."));
|
|
29302
|
+
closeDb();
|
|
29303
|
+
process.exit(1);
|
|
29304
|
+
}
|
|
29305
|
+
const db2 = getDb();
|
|
29306
|
+
const overwrite = opts.overwrite;
|
|
29307
|
+
const orReplace = overwrite ? "OR REPLACE" : "OR IGNORE";
|
|
29308
|
+
let serversImported = 0;
|
|
29309
|
+
let serversSkipped = 0;
|
|
29310
|
+
for (const s of payload.servers) {
|
|
29311
|
+
const existing = getServer(s.id);
|
|
29312
|
+
if (existing && !overwrite) {
|
|
29313
|
+
serversSkipped++;
|
|
29314
|
+
continue;
|
|
29315
|
+
}
|
|
29316
|
+
db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, [s.id, s.name, s.description, s.command, JSON.stringify(s.args ?? []), JSON.stringify(s.env ?? {}), s.transport, s.url, s.source, s.enabled ? 1 : 0, s.created_at, s.updated_at]);
|
|
29317
|
+
serversImported++;
|
|
29318
|
+
}
|
|
29319
|
+
let sourcesImported = 0;
|
|
29320
|
+
let sourcesSkipped = 0;
|
|
29321
|
+
for (const s of payload.sources) {
|
|
29322
|
+
const existing = getSource(s.id);
|
|
29323
|
+
if (existing && !overwrite) {
|
|
29324
|
+
sourcesSkipped++;
|
|
29325
|
+
continue;
|
|
29326
|
+
}
|
|
29327
|
+
db2.run(`INSERT ${orReplace} INTO sources (id, name, type, url, description, enabled, created_at) VALUES (?,?,?,?,?,?,?)`, [s.id, s.name, s.type, s.url, s.description, s.enabled ? 1 : 0, s.created_at]);
|
|
29328
|
+
sourcesImported++;
|
|
29329
|
+
}
|
|
29330
|
+
console.log(chalk2.green(`Servers: ${serversImported} imported, ${serversSkipped} skipped.`));
|
|
29331
|
+
console.log(chalk2.green(`Sources: ${sourcesImported} imported, ${sourcesSkipped} skipped.`));
|
|
29332
|
+
closeDb();
|
|
29333
|
+
});
|
|
29334
|
+
var envCmd = program2.command("env").description("Manage server environment variables");
|
|
29335
|
+
envCmd.command("list").argument("<id>").description("List env vars for a server").action((id) => {
|
|
29336
|
+
const server2 = getServer(id);
|
|
29337
|
+
if (!server2) {
|
|
29338
|
+
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29339
|
+
closeDb();
|
|
29340
|
+
process.exit(1);
|
|
29341
|
+
}
|
|
29342
|
+
const entries = Object.entries(server2.env);
|
|
29343
|
+
if (entries.length === 0) {
|
|
29344
|
+
console.log(chalk2.dim("No env vars set."));
|
|
29345
|
+
closeDb();
|
|
29346
|
+
return;
|
|
29347
|
+
}
|
|
29348
|
+
for (const [k, v] of entries)
|
|
29349
|
+
console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
|
|
29350
|
+
closeDb();
|
|
29351
|
+
});
|
|
29352
|
+
envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
|
|
29353
|
+
const eqIdx = pair.indexOf("=");
|
|
29354
|
+
if (eqIdx === -1) {
|
|
29355
|
+
console.error(chalk2.red("Format: KEY=VALUE"));
|
|
29356
|
+
closeDb();
|
|
29357
|
+
process.exit(1);
|
|
29358
|
+
}
|
|
29359
|
+
const key = pair.slice(0, eqIdx);
|
|
29360
|
+
const value = pair.slice(eqIdx + 1);
|
|
29361
|
+
try {
|
|
29362
|
+
setServerEnv(id, key, value);
|
|
29363
|
+
console.log(chalk2.green(`Set ${key} on ${id}`));
|
|
29364
|
+
} catch (err) {
|
|
29365
|
+
console.error(chalk2.red(err.message));
|
|
29366
|
+
closeDb();
|
|
29367
|
+
process.exit(1);
|
|
29368
|
+
}
|
|
29369
|
+
closeDb();
|
|
29370
|
+
});
|
|
29371
|
+
envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
|
|
29372
|
+
try {
|
|
29373
|
+
unsetServerEnv(id, key);
|
|
29374
|
+
console.log(chalk2.green(`Unset ${key} on ${id}`));
|
|
29375
|
+
} catch (err) {
|
|
29376
|
+
console.error(chalk2.red(err.message));
|
|
29377
|
+
closeDb();
|
|
29378
|
+
process.exit(1);
|
|
29379
|
+
}
|
|
29380
|
+
closeDb();
|
|
27843
29381
|
});
|
|
27844
29382
|
program2.command("mcp").description("Start meta-MCP server (stdio)").action(async () => {
|
|
27845
29383
|
await startMcpServer();
|