@hasna/mcps 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +2269 -232
- package/bin/mcp.js +976 -104
- package/dashboard/dist/assets/index-Df11SKJo.css +1 -0
- package/dashboard/dist/assets/index-a3768emB.js +264 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +852 -154
- package/dist/lib/doctor.d.ts +14 -0
- package/dist/lib/finder.d.ts +8 -0
- package/dist/lib/install.d.ts +8 -0
- package/dist/lib/registry.d.ts +4 -0
- package/dist/lib/sources.d.ts +18 -0
- package/dist/server/serve.d.ts +1 -0
- package/dist/types.d.ts +38 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-Bl3d9_Ks.css +0 -1
- package/dashboard/dist/assets/index-KmgzJKQR.js +0 -224
package/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.4",
|
|
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,79 @@ 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)]));
|
|
11097
|
+
}
|
|
11098
|
+
function cloneServer(id, newName) {
|
|
11099
|
+
const server = getServer(id);
|
|
11100
|
+
if (!server)
|
|
11101
|
+
throw new Error(`Server "${id}" not found`);
|
|
11102
|
+
return addServer({
|
|
11103
|
+
name: newName,
|
|
11104
|
+
description: server.description ?? undefined,
|
|
11105
|
+
command: server.command,
|
|
11106
|
+
args: server.args,
|
|
11107
|
+
env: server.env,
|
|
11108
|
+
transport: server.transport,
|
|
11109
|
+
url: server.url ?? undefined,
|
|
11110
|
+
source: server.source
|
|
11111
|
+
});
|
|
10615
11112
|
}
|
|
10616
11113
|
function getCachedTools(serverId) {
|
|
10617
11114
|
const db2 = getDb();
|
|
10618
|
-
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ?").all(serverId);
|
|
11115
|
+
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
|
|
10619
11116
|
return rows.map((r) => ({
|
|
10620
11117
|
name: r.name,
|
|
10621
11118
|
description: r.description,
|
|
10622
|
-
input_schema:
|
|
11119
|
+
input_schema: safeJsonParse(r.input_schema, {})
|
|
10623
11120
|
}));
|
|
10624
11121
|
}
|
|
10625
11122
|
|
|
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
|
-
}
|
|
11123
|
+
// src/lib/doctor.ts
|
|
11124
|
+
import { execFileSync } from "child_process";
|
|
10686
11125
|
|
|
10687
11126
|
// node_modules/zod/v3/external.js
|
|
10688
11127
|
var exports_external = {};
|
|
@@ -24909,54 +25348,127 @@ class StreamableHTTPClientTransport {
|
|
|
24909
25348
|
}
|
|
24910
25349
|
|
|
24911
25350
|
// src/lib/proxy.ts
|
|
25351
|
+
init_config();
|
|
25352
|
+
init_db();
|
|
24912
25353
|
var connections = new Map;
|
|
25354
|
+
var inflightConnections = new Map;
|
|
25355
|
+
var CONNECT_CONCURRENCY = 4;
|
|
25356
|
+
function buildEnv(extra) {
|
|
25357
|
+
const merged = {};
|
|
25358
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
25359
|
+
if (typeof value === "string")
|
|
25360
|
+
merged[key] = value;
|
|
25361
|
+
}
|
|
25362
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
25363
|
+
if (value === undefined || value === null)
|
|
25364
|
+
continue;
|
|
25365
|
+
merged[key] = String(value);
|
|
25366
|
+
}
|
|
25367
|
+
return merged;
|
|
25368
|
+
}
|
|
25369
|
+
function requireUrl(entry) {
|
|
25370
|
+
if (!entry.url) {
|
|
25371
|
+
throw new Error(`Server "${entry.id}" is missing a URL for ${entry.transport} transport`);
|
|
25372
|
+
}
|
|
25373
|
+
try {
|
|
25374
|
+
return new URL(entry.url);
|
|
25375
|
+
} catch {
|
|
25376
|
+
throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
|
|
25377
|
+
}
|
|
25378
|
+
}
|
|
24913
25379
|
async function connectToServer(entry) {
|
|
24914
25380
|
if (connections.has(entry.id)) {
|
|
24915
25381
|
return connections.get(entry.id);
|
|
24916
25382
|
}
|
|
25383
|
+
const inflight = inflightConnections.get(entry.id);
|
|
25384
|
+
if (inflight) {
|
|
25385
|
+
return inflight;
|
|
25386
|
+
}
|
|
24917
25387
|
const client = new Client({ name: "mcps-proxy", version: "0.0.1" });
|
|
24918
25388
|
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
|
-
|
|
25389
|
+
const connectPromise = (async () => {
|
|
25390
|
+
try {
|
|
25391
|
+
if (entry.transport === "stdio") {
|
|
25392
|
+
if (!entry.command?.trim()) {
|
|
25393
|
+
throw new Error(`Server "${entry.id}" is missing a command`);
|
|
25394
|
+
}
|
|
25395
|
+
transport = new StdioClientTransport({
|
|
25396
|
+
command: entry.command,
|
|
25397
|
+
args: entry.args,
|
|
25398
|
+
env: buildEnv(entry.env)
|
|
25399
|
+
});
|
|
25400
|
+
} else if (entry.transport === "sse") {
|
|
25401
|
+
transport = new SSEClientTransport(requireUrl(entry));
|
|
25402
|
+
} else {
|
|
25403
|
+
transport = new StreamableHTTPClientTransport(requireUrl(entry));
|
|
25404
|
+
}
|
|
25405
|
+
await client.connect(transport);
|
|
25406
|
+
const result = await client.listTools();
|
|
25407
|
+
const tools = (result.tools || []).map((t) => ({
|
|
25408
|
+
server_id: entry.id,
|
|
25409
|
+
name: t.name,
|
|
25410
|
+
description: t.description || "",
|
|
25411
|
+
input_schema: t.inputSchema || {}
|
|
25412
|
+
}));
|
|
25413
|
+
cacheTools(entry.id, tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })));
|
|
25414
|
+
const connected = {
|
|
25415
|
+
entry,
|
|
25416
|
+
tools,
|
|
25417
|
+
disconnect: async () => {
|
|
25418
|
+
try {
|
|
25419
|
+
await client.close();
|
|
25420
|
+
} finally {
|
|
25421
|
+
connections.delete(entry.id);
|
|
25422
|
+
}
|
|
25423
|
+
}
|
|
25424
|
+
};
|
|
25425
|
+
connected._client = client;
|
|
25426
|
+
connections.set(entry.id, connected);
|
|
25427
|
+
try {
|
|
25428
|
+
getDb().prepare("UPDATE servers SET last_connected_at = datetime('now'), last_error = NULL WHERE id = ?").run(entry.id);
|
|
25429
|
+
} catch {}
|
|
25430
|
+
return connected;
|
|
25431
|
+
} catch (err) {
|
|
25432
|
+
try {
|
|
25433
|
+
getDb().prepare("UPDATE servers SET last_error = ? WHERE id = ?").run(err.message, entry.id);
|
|
25434
|
+
} catch {}
|
|
25435
|
+
try {
|
|
25436
|
+
await client.close();
|
|
25437
|
+
} catch {}
|
|
25438
|
+
throw err;
|
|
24945
25439
|
}
|
|
24946
|
-
};
|
|
24947
|
-
|
|
24948
|
-
|
|
24949
|
-
|
|
25440
|
+
})();
|
|
25441
|
+
inflightConnections.set(entry.id, connectPromise);
|
|
25442
|
+
try {
|
|
25443
|
+
return await connectPromise;
|
|
25444
|
+
} finally {
|
|
25445
|
+
inflightConnections.delete(entry.id);
|
|
25446
|
+
}
|
|
24950
25447
|
}
|
|
24951
25448
|
async function disconnectServer(id) {
|
|
24952
25449
|
const conn = connections.get(id);
|
|
24953
25450
|
if (conn) {
|
|
24954
25451
|
await conn.disconnect();
|
|
25452
|
+
return;
|
|
25453
|
+
}
|
|
25454
|
+
const inflight = inflightConnections.get(id);
|
|
25455
|
+
if (inflight) {
|
|
25456
|
+
try {
|
|
25457
|
+
const pending = await inflight;
|
|
25458
|
+
await pending.disconnect();
|
|
25459
|
+
} catch {}
|
|
24955
25460
|
}
|
|
24956
25461
|
}
|
|
24957
25462
|
async function disconnectAll() {
|
|
24958
25463
|
const ids = Array.from(connections.keys());
|
|
24959
|
-
await Promise.
|
|
25464
|
+
await Promise.allSettled(ids.map((id) => disconnectServer(id)));
|
|
25465
|
+
const inflight = Array.from(inflightConnections.values());
|
|
25466
|
+
await Promise.allSettled(inflight.map(async (promise2) => {
|
|
25467
|
+
try {
|
|
25468
|
+
const conn = await promise2;
|
|
25469
|
+
await conn.disconnect();
|
|
25470
|
+
} catch {}
|
|
25471
|
+
}));
|
|
24960
25472
|
}
|
|
24961
25473
|
function listAllTools() {
|
|
24962
25474
|
const tools = [];
|
|
@@ -24971,12 +25483,19 @@ function listAllTools() {
|
|
|
24971
25483
|
return tools;
|
|
24972
25484
|
}
|
|
24973
25485
|
async function callTool(prefixedName, args) {
|
|
24974
|
-
const
|
|
25486
|
+
const normalized = prefixedName.trim();
|
|
25487
|
+
if (!normalized) {
|
|
25488
|
+
throw new Error("Tool name is required");
|
|
25489
|
+
}
|
|
25490
|
+
const sepIdx = normalized.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
24975
25491
|
if (sepIdx === -1) {
|
|
24976
25492
|
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
24977
25493
|
}
|
|
24978
|
-
const serverId =
|
|
24979
|
-
const toolName =
|
|
25494
|
+
const serverId = normalized.slice(0, sepIdx).trim();
|
|
25495
|
+
const toolName = normalized.slice(sepIdx + TOOL_PREFIX_SEPARATOR.length).trim();
|
|
25496
|
+
if (!serverId || !toolName) {
|
|
25497
|
+
throw new Error(`Invalid tool name "${prefixedName}" \u2014 expected format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`);
|
|
25498
|
+
}
|
|
24980
25499
|
const conn = connections.get(serverId);
|
|
24981
25500
|
if (!conn) {
|
|
24982
25501
|
throw new Error(`Server "${serverId}" is not connected`);
|
|
@@ -24992,17 +25511,247 @@ async function callTool(prefixedName, args) {
|
|
|
24992
25511
|
async function connectAllEnabled() {
|
|
24993
25512
|
const servers = listServers().filter((s) => s.enabled);
|
|
24994
25513
|
const results = [];
|
|
24995
|
-
|
|
25514
|
+
let index = 0;
|
|
25515
|
+
const workerCount = Math.min(CONNECT_CONCURRENCY, servers.length);
|
|
25516
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
25517
|
+
while (true) {
|
|
25518
|
+
const current = index++;
|
|
25519
|
+
if (current >= servers.length)
|
|
25520
|
+
return;
|
|
25521
|
+
const server = servers[current];
|
|
25522
|
+
try {
|
|
25523
|
+
const conn = await connectToServer(server);
|
|
25524
|
+
results.push(conn);
|
|
25525
|
+
} catch (err) {
|
|
25526
|
+
console.error(`Failed to connect to ${server.name}: ${err.message}`);
|
|
25527
|
+
}
|
|
25528
|
+
}
|
|
25529
|
+
});
|
|
25530
|
+
await Promise.all(workers);
|
|
25531
|
+
return results;
|
|
25532
|
+
}
|
|
25533
|
+
|
|
25534
|
+
// src/lib/doctor.ts
|
|
25535
|
+
async function diagnoseServer(server) {
|
|
25536
|
+
const checks4 = [];
|
|
25537
|
+
if (server.transport === "stdio") {
|
|
25538
|
+
try {
|
|
25539
|
+
const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
|
|
25540
|
+
let version2 = "";
|
|
25541
|
+
try {
|
|
25542
|
+
version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
|
|
25543
|
+
`)[0];
|
|
25544
|
+
} catch {}
|
|
25545
|
+
checks4.push({ name: "command on PATH", pass: true, message: `${path}${version2 ? ` (${version2})` : ""}` });
|
|
25546
|
+
} catch {
|
|
25547
|
+
checks4.push({
|
|
25548
|
+
name: "command on PATH",
|
|
25549
|
+
pass: false,
|
|
25550
|
+
message: `${server.command} not found on PATH`,
|
|
25551
|
+
fixable: true,
|
|
25552
|
+
fixHint: server.args[0] || server.command
|
|
25553
|
+
});
|
|
25554
|
+
}
|
|
25555
|
+
}
|
|
25556
|
+
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
25557
|
+
if (Object.keys(server.env).length === 0) {
|
|
25558
|
+
checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
|
|
25559
|
+
} else if (missingEnv.length > 0) {
|
|
25560
|
+
checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
|
|
25561
|
+
} else {
|
|
25562
|
+
checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
|
|
25563
|
+
}
|
|
25564
|
+
if (server.transport !== "stdio" && server.url) {
|
|
24996
25565
|
try {
|
|
24997
|
-
const
|
|
24998
|
-
|
|
25566
|
+
const res = await fetch(server.url, { signal: AbortSignal.timeout(5000) });
|
|
25567
|
+
checks4.push({ name: "URL reachable", pass: res.ok || res.status < 500, message: `HTTP ${res.status}` });
|
|
24999
25568
|
} catch (err) {
|
|
25000
|
-
|
|
25569
|
+
checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
|
|
25001
25570
|
}
|
|
25002
25571
|
}
|
|
25003
|
-
|
|
25572
|
+
if (server.enabled) {
|
|
25573
|
+
try {
|
|
25574
|
+
await Promise.race([
|
|
25575
|
+
connectToServer(server),
|
|
25576
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
|
|
25577
|
+
]);
|
|
25578
|
+
await disconnectServer(server.id);
|
|
25579
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "connected successfully" });
|
|
25580
|
+
} catch (err) {
|
|
25581
|
+
checks4.push({ name: "connect & list tools", pass: false, message: err.message });
|
|
25582
|
+
}
|
|
25583
|
+
} else {
|
|
25584
|
+
checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
|
|
25585
|
+
}
|
|
25586
|
+
return {
|
|
25587
|
+
server,
|
|
25588
|
+
checks: checks4,
|
|
25589
|
+
healthy: checks4.every((c) => c.pass)
|
|
25590
|
+
};
|
|
25004
25591
|
}
|
|
25005
25592
|
|
|
25593
|
+
// src/lib/remote.ts
|
|
25594
|
+
init_config();
|
|
25595
|
+
function parseRegistryEntry(entry) {
|
|
25596
|
+
const s = entry.server;
|
|
25597
|
+
return {
|
|
25598
|
+
id: s.name,
|
|
25599
|
+
name: s.name,
|
|
25600
|
+
description: s.description || "",
|
|
25601
|
+
repository: s.repository,
|
|
25602
|
+
packages: s.packages || []
|
|
25603
|
+
};
|
|
25604
|
+
}
|
|
25605
|
+
async function searchRegistry(query) {
|
|
25606
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
25607
|
+
if (!res.ok) {
|
|
25608
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
25609
|
+
}
|
|
25610
|
+
const data = await res.json();
|
|
25611
|
+
const entries = data.servers || [];
|
|
25612
|
+
const q = query.toLowerCase();
|
|
25613
|
+
return entries.map(parseRegistryEntry).filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
|
25614
|
+
}
|
|
25615
|
+
async function getRegistryServer(id) {
|
|
25616
|
+
const res = await fetch(REGISTRY_API_URL);
|
|
25617
|
+
if (!res.ok) {
|
|
25618
|
+
throw new Error(`Registry API error: ${res.status} ${res.statusText}`);
|
|
25619
|
+
}
|
|
25620
|
+
const data = await res.json();
|
|
25621
|
+
const entries = data.servers || [];
|
|
25622
|
+
const all = entries.map(parseRegistryEntry);
|
|
25623
|
+
return all.find((s) => s.id === id) || null;
|
|
25624
|
+
}
|
|
25625
|
+
async function installFromRegistry(id) {
|
|
25626
|
+
const server = await getRegistryServer(id);
|
|
25627
|
+
if (!server) {
|
|
25628
|
+
throw new Error(`Server "${id}" not found in registry`);
|
|
25629
|
+
}
|
|
25630
|
+
const pkg = server.packages?.[0];
|
|
25631
|
+
let command = "npx";
|
|
25632
|
+
let args = [];
|
|
25633
|
+
let transport = "stdio";
|
|
25634
|
+
if (pkg) {
|
|
25635
|
+
if (pkg.registryType === "npm") {
|
|
25636
|
+
command = "npx";
|
|
25637
|
+
args = ["-y", pkg.identifier];
|
|
25638
|
+
} else {
|
|
25639
|
+
command = pkg.identifier;
|
|
25640
|
+
}
|
|
25641
|
+
if (pkg.transport?.type) {
|
|
25642
|
+
transport = pkg.transport.type;
|
|
25643
|
+
}
|
|
25644
|
+
}
|
|
25645
|
+
return addServer({
|
|
25646
|
+
name: server.name,
|
|
25647
|
+
description: server.description,
|
|
25648
|
+
command,
|
|
25649
|
+
args,
|
|
25650
|
+
transport,
|
|
25651
|
+
source: "registry"
|
|
25652
|
+
});
|
|
25653
|
+
}
|
|
25654
|
+
|
|
25655
|
+
// src/lib/finder.ts
|
|
25656
|
+
init_sources();
|
|
25657
|
+
async function listAwesomeServers() {
|
|
25658
|
+
const { listSources: listSources2, searchSource: searchSource2 } = await Promise.resolve().then(() => (init_sources(), exports_sources));
|
|
25659
|
+
const source = listSources2().find((s) => s.type === "awesome-list" && s.enabled);
|
|
25660
|
+
if (!source)
|
|
25661
|
+
return [];
|
|
25662
|
+
return searchSource2(source, "");
|
|
25663
|
+
}
|
|
25664
|
+
|
|
25665
|
+
// src/cli/index.tsx
|
|
25666
|
+
init_sources();
|
|
25667
|
+
|
|
25668
|
+
// src/lib/install.ts
|
|
25669
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
25670
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
25671
|
+
import { join as join3 } from "path";
|
|
25672
|
+
import { homedir as homedir2 } from "os";
|
|
25673
|
+
function installToClaude(entry) {
|
|
25674
|
+
try {
|
|
25675
|
+
const args = [
|
|
25676
|
+
"mcp",
|
|
25677
|
+
"add",
|
|
25678
|
+
"--transport",
|
|
25679
|
+
entry.transport,
|
|
25680
|
+
"--scope",
|
|
25681
|
+
"user"
|
|
25682
|
+
];
|
|
25683
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
25684
|
+
args.push("--env", `${k}=${v}`);
|
|
25685
|
+
}
|
|
25686
|
+
args.push(entry.id, "--", entry.command, ...entry.args);
|
|
25687
|
+
execFileSync2("claude", args, { stdio: "pipe" });
|
|
25688
|
+
return { agent: "claude", success: true };
|
|
25689
|
+
} catch (err) {
|
|
25690
|
+
return { agent: "claude", success: false, error: err.message };
|
|
25691
|
+
}
|
|
25692
|
+
}
|
|
25693
|
+
function installToCodex(entry) {
|
|
25694
|
+
try {
|
|
25695
|
+
const configDir = join3(homedir2(), ".codex");
|
|
25696
|
+
const configPath = join3(configDir, "config.toml");
|
|
25697
|
+
if (!existsSync2(configDir)) {
|
|
25698
|
+
mkdirSync3(configDir, { recursive: true });
|
|
25699
|
+
}
|
|
25700
|
+
const block = `
|
|
25701
|
+
[mcp_servers.${entry.id}]
|
|
25702
|
+
` + `command = ${JSON.stringify(entry.command)}
|
|
25703
|
+
` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
|
|
25704
|
+
`;
|
|
25705
|
+
const existing = existsSync2(configPath) ? readFileSync2(configPath, "utf-8") : "";
|
|
25706
|
+
if (existing.includes(`[mcp_servers.${entry.id}]`)) {
|
|
25707
|
+
return { agent: "codex", success: true };
|
|
25708
|
+
}
|
|
25709
|
+
writeFileSync2(configPath, existing + block, "utf-8");
|
|
25710
|
+
return { agent: "codex", success: true };
|
|
25711
|
+
} catch (err) {
|
|
25712
|
+
return { agent: "codex", success: false, error: err.message };
|
|
25713
|
+
}
|
|
25714
|
+
}
|
|
25715
|
+
function installToGemini(entry) {
|
|
25716
|
+
try {
|
|
25717
|
+
const configDir = join3(homedir2(), ".gemini");
|
|
25718
|
+
const configPath = join3(configDir, "settings.json");
|
|
25719
|
+
if (!existsSync2(configDir)) {
|
|
25720
|
+
mkdirSync3(configDir, { recursive: true });
|
|
25721
|
+
}
|
|
25722
|
+
let settings = {};
|
|
25723
|
+
if (existsSync2(configPath)) {
|
|
25724
|
+
settings = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
25725
|
+
}
|
|
25726
|
+
if (!settings.mcpServers)
|
|
25727
|
+
settings.mcpServers = {};
|
|
25728
|
+
settings.mcpServers[entry.id] = {
|
|
25729
|
+
command: entry.command,
|
|
25730
|
+
args: entry.args,
|
|
25731
|
+
...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
|
|
25732
|
+
};
|
|
25733
|
+
writeFileSync2(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
25734
|
+
return { agent: "gemini", success: true };
|
|
25735
|
+
} catch (err) {
|
|
25736
|
+
return { agent: "gemini", success: false, error: err.message };
|
|
25737
|
+
}
|
|
25738
|
+
}
|
|
25739
|
+
function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
|
|
25740
|
+
return targets.map((target) => {
|
|
25741
|
+
if (target === "claude")
|
|
25742
|
+
return installToClaude(entry);
|
|
25743
|
+
if (target === "codex")
|
|
25744
|
+
return installToCodex(entry);
|
|
25745
|
+
if (target === "gemini")
|
|
25746
|
+
return installToGemini(entry);
|
|
25747
|
+
return { agent: target, success: false, error: "Unknown target" };
|
|
25748
|
+
});
|
|
25749
|
+
}
|
|
25750
|
+
|
|
25751
|
+
// src/cli/index.tsx
|
|
25752
|
+
init_db();
|
|
25753
|
+
import * as readline from "readline";
|
|
25754
|
+
|
|
25006
25755
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
|
|
25007
25756
|
class ExperimentalServerTasks {
|
|
25008
25757
|
constructor(_server) {
|
|
@@ -26224,14 +26973,31 @@ class StdioServerTransport {
|
|
|
26224
26973
|
}
|
|
26225
26974
|
|
|
26226
26975
|
// src/mcp/index.ts
|
|
26976
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
26977
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
26978
|
+
import { fileURLToPath } from "url";
|
|
26979
|
+
init_sources();
|
|
26980
|
+
init_config();
|
|
26981
|
+
function redactServerEnv(server) {
|
|
26982
|
+
return { ...server, env: {} };
|
|
26983
|
+
}
|
|
26984
|
+
var VERSION = (() => {
|
|
26985
|
+
try {
|
|
26986
|
+
const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
26987
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
26988
|
+
return pkg.version || "0.0.1";
|
|
26989
|
+
} catch {
|
|
26990
|
+
return "0.0.1";
|
|
26991
|
+
}
|
|
26992
|
+
})();
|
|
26227
26993
|
var server = new McpServer({
|
|
26228
26994
|
name: "mcps",
|
|
26229
|
-
version:
|
|
26995
|
+
version: VERSION
|
|
26230
26996
|
});
|
|
26231
26997
|
server.tool("list_servers", "List all registered MCP servers", {}, async () => {
|
|
26232
26998
|
const servers = listServers();
|
|
26233
26999
|
return {
|
|
26234
|
-
content: [{ type: "text", text: JSON.stringify(servers, null, 2) }]
|
|
27000
|
+
content: [{ type: "text", text: JSON.stringify(servers.map(redactServerEnv), null, 2) }]
|
|
26235
27001
|
};
|
|
26236
27002
|
});
|
|
26237
27003
|
server.tool("search_registry", "Search the official MCP registry for servers", { query: exports_external.string().describe("Search query") }, async ({ query }) => {
|
|
@@ -26282,17 +27048,84 @@ server.tool("remove_server", "Remove a registered MCP server", { id: exports_ext
|
|
|
26282
27048
|
};
|
|
26283
27049
|
});
|
|
26284
27050
|
server.tool("enable_server", "Enable a registered MCP server", { id: exports_external.string().describe("Server ID to enable") }, async ({ id }) => {
|
|
27051
|
+
const existing = getServer(id);
|
|
27052
|
+
if (!existing) {
|
|
27053
|
+
return {
|
|
27054
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27055
|
+
isError: true
|
|
27056
|
+
};
|
|
27057
|
+
}
|
|
26285
27058
|
const entry = enableServer(id);
|
|
26286
27059
|
return {
|
|
26287
27060
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
26288
27061
|
};
|
|
26289
27062
|
});
|
|
26290
27063
|
server.tool("disable_server", "Disable a registered MCP server", { id: exports_external.string().describe("Server ID to disable") }, async ({ id }) => {
|
|
27064
|
+
const existing = getServer(id);
|
|
27065
|
+
if (!existing) {
|
|
27066
|
+
return {
|
|
27067
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27068
|
+
isError: true
|
|
27069
|
+
};
|
|
27070
|
+
}
|
|
26291
27071
|
const entry = disableServer(id);
|
|
26292
27072
|
return {
|
|
26293
27073
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
26294
27074
|
};
|
|
26295
27075
|
});
|
|
27076
|
+
server.tool("update_server", "Update fields of a registered MCP server", {
|
|
27077
|
+
id: exports_external.string().describe("Server ID to update"),
|
|
27078
|
+
name: exports_external.string().optional().describe("New display name"),
|
|
27079
|
+
description: exports_external.string().optional().describe("New description"),
|
|
27080
|
+
command: exports_external.string().optional().describe("New command"),
|
|
27081
|
+
args: exports_external.array(exports_external.string()).optional().describe("New args list"),
|
|
27082
|
+
transport: exports_external.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
|
|
27083
|
+
url: exports_external.string().optional().describe("New URL for remote transports")
|
|
27084
|
+
}, async ({ id, name, description, command, args, transport, url: url2 }) => {
|
|
27085
|
+
const existing = getServer(id);
|
|
27086
|
+
if (!existing) {
|
|
27087
|
+
return {
|
|
27088
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27089
|
+
isError: true
|
|
27090
|
+
};
|
|
27091
|
+
}
|
|
27092
|
+
const fields = {};
|
|
27093
|
+
if (name !== undefined)
|
|
27094
|
+
fields.name = name;
|
|
27095
|
+
if (description !== undefined)
|
|
27096
|
+
fields.description = description;
|
|
27097
|
+
if (command !== undefined)
|
|
27098
|
+
fields.command = command;
|
|
27099
|
+
if (args !== undefined)
|
|
27100
|
+
fields.args = args;
|
|
27101
|
+
if (transport !== undefined)
|
|
27102
|
+
fields.transport = transport;
|
|
27103
|
+
if (url2 !== undefined)
|
|
27104
|
+
fields.url = url2;
|
|
27105
|
+
const updated = updateServer(id, fields);
|
|
27106
|
+
return {
|
|
27107
|
+
content: [{ type: "text", text: JSON.stringify(redactServerEnv(updated), null, 2) }]
|
|
27108
|
+
};
|
|
27109
|
+
});
|
|
27110
|
+
server.tool("list_tools", "List all cached tools across registered servers without connecting. Optionally filter by server_id.", { server_id: exports_external.string().optional().describe("Server ID to filter by (optional)") }, async ({ server_id }) => {
|
|
27111
|
+
if (server_id) {
|
|
27112
|
+
const tools = getCachedTools(server_id);
|
|
27113
|
+
return {
|
|
27114
|
+
content: [{ type: "text", text: JSON.stringify(tools.map((t) => ({ ...t, server_id })), null, 2) }]
|
|
27115
|
+
};
|
|
27116
|
+
}
|
|
27117
|
+
const servers = listServers();
|
|
27118
|
+
const allTools = [];
|
|
27119
|
+
for (const s of servers) {
|
|
27120
|
+
const tools = getCachedTools(s.id);
|
|
27121
|
+
for (const t of tools) {
|
|
27122
|
+
allTools.push({ server_id: s.id, ...t });
|
|
27123
|
+
}
|
|
27124
|
+
}
|
|
27125
|
+
return {
|
|
27126
|
+
content: [{ type: "text", text: JSON.stringify(allTools, null, 2) }]
|
|
27127
|
+
};
|
|
27128
|
+
});
|
|
26296
27129
|
server.tool("get_server_info", "Get detailed information about a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
|
|
26297
27130
|
const entry = getServer(id);
|
|
26298
27131
|
if (!entry) {
|
|
@@ -26302,12 +27135,108 @@ server.tool("get_server_info", "Get detailed information about a registered MCP
|
|
|
26302
27135
|
};
|
|
26303
27136
|
}
|
|
26304
27137
|
return {
|
|
26305
|
-
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
27138
|
+
content: [{ type: "text", text: JSON.stringify(redactServerEnv(entry), null, 2) }]
|
|
26306
27139
|
};
|
|
26307
27140
|
});
|
|
26308
|
-
server.tool("
|
|
26309
|
-
|
|
26310
|
-
|
|
27141
|
+
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.", {
|
|
27142
|
+
query: exports_external.string().describe("Search query (e.g. 'filesystem', 'postgres', 'browser')"),
|
|
27143
|
+
sources: exports_external.array(exports_external.string()).optional().describe("Source IDs to search (default: all enabled). Use list_sources to get IDs."),
|
|
27144
|
+
limit: exports_external.number().optional().describe("Max results per source (default: 20)")
|
|
27145
|
+
}, async ({ query, sources, limit }) => {
|
|
27146
|
+
const results = await findServers(query, { sources, limit });
|
|
27147
|
+
return {
|
|
27148
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
27149
|
+
};
|
|
27150
|
+
});
|
|
27151
|
+
server.tool("list_sources", "List all configured search sources for finding MCP servers", {}, async () => {
|
|
27152
|
+
const sources = listSources();
|
|
27153
|
+
return {
|
|
27154
|
+
content: [{ type: "text", text: JSON.stringify(sources, null, 2) }]
|
|
27155
|
+
};
|
|
27156
|
+
});
|
|
27157
|
+
server.tool("add_source", "Add a new search source for finding MCP servers", {
|
|
27158
|
+
name: exports_external.string().describe("Source name"),
|
|
27159
|
+
type: exports_external.enum(["mcp-registry", "awesome-list", "npm-search", "github-topic"]).describe("Source type"),
|
|
27160
|
+
url: exports_external.string().describe("Source URL endpoint"),
|
|
27161
|
+
description: exports_external.string().optional().describe("Description")
|
|
27162
|
+
}, async ({ name, type, url: url2, description }) => {
|
|
27163
|
+
const source = addSource({ name, type, url: url2, description });
|
|
27164
|
+
return {
|
|
27165
|
+
content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
|
|
27166
|
+
};
|
|
27167
|
+
});
|
|
27168
|
+
server.tool("remove_source", "Remove a search source by ID", { id: exports_external.string().describe("Source ID to remove") }, async ({ id }) => {
|
|
27169
|
+
const existing = getSource(id);
|
|
27170
|
+
if (!existing) {
|
|
27171
|
+
return {
|
|
27172
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
27173
|
+
isError: true
|
|
27174
|
+
};
|
|
27175
|
+
}
|
|
27176
|
+
removeSource(id);
|
|
27177
|
+
return {
|
|
27178
|
+
content: [{ type: "text", text: `Removed source: ${existing.name} [${id}]` }]
|
|
27179
|
+
};
|
|
27180
|
+
});
|
|
27181
|
+
server.tool("enable_source_finder", "Enable a search source", { id: exports_external.string().describe("Source ID to enable") }, async ({ id }) => {
|
|
27182
|
+
const existing = getSource(id);
|
|
27183
|
+
if (!existing) {
|
|
27184
|
+
return {
|
|
27185
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
27186
|
+
isError: true
|
|
27187
|
+
};
|
|
27188
|
+
}
|
|
27189
|
+
enableSource(id);
|
|
27190
|
+
return {
|
|
27191
|
+
content: [{ type: "text", text: `Enabled source: ${existing.name}` }]
|
|
27192
|
+
};
|
|
27193
|
+
});
|
|
27194
|
+
server.tool("disable_source_finder", "Disable a search source", { id: exports_external.string().describe("Source ID to disable") }, async ({ id }) => {
|
|
27195
|
+
const existing = getSource(id);
|
|
27196
|
+
if (!existing) {
|
|
27197
|
+
return {
|
|
27198
|
+
content: [{ type: "text", text: `Source "${id}" not found.` }],
|
|
27199
|
+
isError: true
|
|
27200
|
+
};
|
|
27201
|
+
}
|
|
27202
|
+
disableSource(id);
|
|
27203
|
+
return {
|
|
27204
|
+
content: [{ type: "text", text: `Disabled source: ${existing.name}` }]
|
|
27205
|
+
};
|
|
27206
|
+
});
|
|
27207
|
+
server.tool("install_to_agents", "Install a registered MCP server into Claude Code, Codex, and/or Gemini", {
|
|
27208
|
+
id: exports_external.string().describe("Server ID to install (from list_servers)"),
|
|
27209
|
+
targets: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
|
|
27210
|
+
}, async ({ id, targets }) => {
|
|
27211
|
+
const entry = getServer(id);
|
|
27212
|
+
if (!entry) {
|
|
27213
|
+
return {
|
|
27214
|
+
content: [{ type: "text", text: `Server "${id}" not found.` }],
|
|
27215
|
+
isError: true
|
|
27216
|
+
};
|
|
27217
|
+
}
|
|
27218
|
+
const agentTargets = targets ?? ["claude", "codex", "gemini"];
|
|
27219
|
+
const results = installToAgents(entry, agentTargets);
|
|
27220
|
+
return {
|
|
27221
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
27222
|
+
};
|
|
27223
|
+
});
|
|
27224
|
+
server.tool("list_awesome_servers", "List all MCP servers from the curated punkpeye/awesome-mcp-servers GitHub list", {}, async () => {
|
|
27225
|
+
const results = await listAwesomeServers();
|
|
27226
|
+
return {
|
|
27227
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
27228
|
+
};
|
|
27229
|
+
});
|
|
27230
|
+
server.tool("connect_and_list_tools", "Connect to all enabled MCP servers and list their available tools", {}, async () => {
|
|
27231
|
+
let tools = [];
|
|
27232
|
+
try {
|
|
27233
|
+
await connectAllEnabled();
|
|
27234
|
+
tools = listAllTools();
|
|
27235
|
+
} finally {
|
|
27236
|
+
await disconnectAll().catch(() => {
|
|
27237
|
+
return;
|
|
27238
|
+
});
|
|
27239
|
+
}
|
|
26311
27240
|
return {
|
|
26312
27241
|
content: [{ type: "text", text: JSON.stringify(tools, null, 2) }]
|
|
26313
27242
|
};
|
|
@@ -26317,6 +27246,28 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
|
|
|
26317
27246
|
arguments: exports_external.record(exports_external.unknown()).optional().describe("Tool arguments as key-value pairs")
|
|
26318
27247
|
}, async ({ tool_name, arguments: args }) => {
|
|
26319
27248
|
try {
|
|
27249
|
+
const sepIdx = tool_name.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
27250
|
+
if (sepIdx === -1) {
|
|
27251
|
+
return {
|
|
27252
|
+
content: [{ type: "text", text: `Error: Invalid tool name "${tool_name}"` }],
|
|
27253
|
+
isError: true
|
|
27254
|
+
};
|
|
27255
|
+
}
|
|
27256
|
+
const serverId = tool_name.slice(0, sepIdx);
|
|
27257
|
+
const entry = getServer(serverId);
|
|
27258
|
+
if (!entry) {
|
|
27259
|
+
return {
|
|
27260
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" not found.` }],
|
|
27261
|
+
isError: true
|
|
27262
|
+
};
|
|
27263
|
+
}
|
|
27264
|
+
if (!entry.enabled) {
|
|
27265
|
+
return {
|
|
27266
|
+
content: [{ type: "text", text: `Error: Server "${serverId}" is disabled.` }],
|
|
27267
|
+
isError: true
|
|
27268
|
+
};
|
|
27269
|
+
}
|
|
27270
|
+
await connectToServer(entry);
|
|
26320
27271
|
const result = await callTool(tool_name, args || {});
|
|
26321
27272
|
return { content: result.content };
|
|
26322
27273
|
} catch (err) {
|
|
@@ -26326,6 +27277,13 @@ server.tool("call_upstream_tool", `Call a tool on a connected upstream MCP serve
|
|
|
26326
27277
|
};
|
|
26327
27278
|
}
|
|
26328
27279
|
});
|
|
27280
|
+
server.tool("diagnose_server", "Run health checks on a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
|
|
27281
|
+
const entry = getServer(id);
|
|
27282
|
+
if (!entry)
|
|
27283
|
+
return { content: [{ type: "text", text: `Server "${id}" not found.` }], isError: true };
|
|
27284
|
+
const report = await diagnoseServer(entry);
|
|
27285
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
27286
|
+
});
|
|
26329
27287
|
async function startMcpServer() {
|
|
26330
27288
|
const transport = new StdioServerTransport;
|
|
26331
27289
|
await server.connect(transport);
|
|
@@ -26339,27 +27297,32 @@ if (isDirectRun) {
|
|
|
26339
27297
|
}
|
|
26340
27298
|
|
|
26341
27299
|
// src/server/serve.ts
|
|
26342
|
-
import { existsSync } from "fs";
|
|
26343
|
-
import { join as
|
|
26344
|
-
import { fileURLToPath } from "url";
|
|
27300
|
+
import { existsSync as existsSync3 } from "fs";
|
|
27301
|
+
import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
|
|
27302
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
27303
|
+
init_sources();
|
|
27304
|
+
init_db();
|
|
27305
|
+
function redactServer(server2) {
|
|
27306
|
+
return { ...server2, env: {} };
|
|
27307
|
+
}
|
|
26345
27308
|
function resolveDashboardDir() {
|
|
26346
27309
|
const candidates = [];
|
|
26347
27310
|
try {
|
|
26348
|
-
const scriptDir =
|
|
26349
|
-
candidates.push(
|
|
26350
|
-
candidates.push(
|
|
27311
|
+
const scriptDir = dirname3(fileURLToPath2(import.meta.url));
|
|
27312
|
+
candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
|
|
27313
|
+
candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
|
|
26351
27314
|
} catch {}
|
|
26352
27315
|
if (process.argv[1]) {
|
|
26353
|
-
const mainDir =
|
|
26354
|
-
candidates.push(
|
|
26355
|
-
candidates.push(
|
|
27316
|
+
const mainDir = dirname3(process.argv[1]);
|
|
27317
|
+
candidates.push(join5(mainDir, "..", "dashboard", "dist"));
|
|
27318
|
+
candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
|
|
26356
27319
|
}
|
|
26357
|
-
candidates.push(
|
|
27320
|
+
candidates.push(join5(process.cwd(), "dashboard", "dist"));
|
|
26358
27321
|
for (const candidate of candidates) {
|
|
26359
|
-
if (
|
|
27322
|
+
if (existsSync3(candidate))
|
|
26360
27323
|
return candidate;
|
|
26361
27324
|
}
|
|
26362
|
-
return
|
|
27325
|
+
return join5(process.cwd(), "dashboard", "dist");
|
|
26363
27326
|
}
|
|
26364
27327
|
var MIME_TYPES = {
|
|
26365
27328
|
".html": "text/html; charset=utf-8",
|
|
@@ -26375,13 +27338,15 @@ var MIME_TYPES = {
|
|
|
26375
27338
|
};
|
|
26376
27339
|
var SECURITY_HEADERS = {
|
|
26377
27340
|
"X-Content-Type-Options": "nosniff",
|
|
26378
|
-
"X-Frame-Options": "DENY"
|
|
27341
|
+
"X-Frame-Options": "DENY",
|
|
27342
|
+
"Referrer-Policy": "no-referrer"
|
|
26379
27343
|
};
|
|
26380
27344
|
function json(data, status = 200, port) {
|
|
26381
27345
|
return new Response(JSON.stringify(data), {
|
|
26382
27346
|
status,
|
|
26383
27347
|
headers: {
|
|
26384
27348
|
"Content-Type": "application/json",
|
|
27349
|
+
"Cache-Control": "no-store",
|
|
26385
27350
|
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
26386
27351
|
...SECURITY_HEADERS
|
|
26387
27352
|
}
|
|
@@ -26391,27 +27356,94 @@ function isValidId(id) {
|
|
|
26391
27356
|
return /^[a-z0-9-]+$/.test(id);
|
|
26392
27357
|
}
|
|
26393
27358
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
27359
|
+
function isLoopbackHost(hostname2) {
|
|
27360
|
+
return hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1";
|
|
27361
|
+
}
|
|
27362
|
+
function isAllowedOrigin(req, port, host) {
|
|
27363
|
+
const origin = req.headers.get("origin");
|
|
27364
|
+
if (!origin)
|
|
27365
|
+
return true;
|
|
27366
|
+
if (origin === "null")
|
|
27367
|
+
return false;
|
|
27368
|
+
try {
|
|
27369
|
+
const url2 = new URL(origin);
|
|
27370
|
+
if (url2.port && url2.port !== String(port))
|
|
27371
|
+
return false;
|
|
27372
|
+
if (isLoopbackHost(url2.hostname))
|
|
27373
|
+
return true;
|
|
27374
|
+
const normalizedHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
27375
|
+
return url2.hostname === normalizedHost;
|
|
27376
|
+
} catch {
|
|
27377
|
+
return false;
|
|
27378
|
+
}
|
|
27379
|
+
}
|
|
27380
|
+
function isAuthorized(req, host) {
|
|
27381
|
+
const token = process.env.MCPS_API_TOKEN;
|
|
27382
|
+
if (!token) {
|
|
27383
|
+
return isLoopbackHost(host);
|
|
27384
|
+
}
|
|
27385
|
+
const auth2 = req.headers.get("authorization");
|
|
27386
|
+
if (auth2 === `Bearer ${token}`)
|
|
27387
|
+
return true;
|
|
27388
|
+
return isLoopbackHost(host);
|
|
27389
|
+
}
|
|
27390
|
+
function unauthorizedResponse(port) {
|
|
27391
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
27392
|
+
status: 401,
|
|
27393
|
+
headers: {
|
|
27394
|
+
"Content-Type": "application/json",
|
|
27395
|
+
"Cache-Control": "no-store",
|
|
27396
|
+
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
27397
|
+
"WWW-Authenticate": "Bearer",
|
|
27398
|
+
...SECURITY_HEADERS
|
|
27399
|
+
}
|
|
27400
|
+
});
|
|
27401
|
+
}
|
|
26394
27402
|
function getAllServersWithToolCount() {
|
|
26395
27403
|
const servers = listServers();
|
|
27404
|
+
if (servers.length === 0)
|
|
27405
|
+
return [];
|
|
27406
|
+
const counts = getToolCounts();
|
|
26396
27407
|
return servers.map((s) => ({
|
|
26397
|
-
...s,
|
|
26398
|
-
toolCount:
|
|
27408
|
+
...redactServer(s),
|
|
27409
|
+
toolCount: counts.get(s.id) ?? 0
|
|
26399
27410
|
}));
|
|
26400
27411
|
}
|
|
26401
27412
|
function serveStaticFile(filePath) {
|
|
26402
|
-
if (!
|
|
27413
|
+
if (!existsSync3(filePath))
|
|
26403
27414
|
return null;
|
|
26404
27415
|
const ext = extname(filePath);
|
|
26405
27416
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
26406
27417
|
return new Response(Bun.file(filePath), {
|
|
26407
|
-
headers: { "Content-Type": contentType }
|
|
27418
|
+
headers: { "Content-Type": contentType, ...SECURITY_HEADERS }
|
|
26408
27419
|
});
|
|
26409
27420
|
}
|
|
27421
|
+
function resolveStaticPath(baseDir, urlPath) {
|
|
27422
|
+
let decoded;
|
|
27423
|
+
try {
|
|
27424
|
+
decoded = decodeURIComponent(urlPath);
|
|
27425
|
+
} catch {
|
|
27426
|
+
return null;
|
|
27427
|
+
}
|
|
27428
|
+
const stripped = decoded.replace(/^\/+/, "");
|
|
27429
|
+
const resolved = resolve(baseDir, stripped);
|
|
27430
|
+
const rel = relative(baseDir, resolved);
|
|
27431
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
27432
|
+
return null;
|
|
27433
|
+
}
|
|
27434
|
+
return resolved;
|
|
27435
|
+
}
|
|
27436
|
+
function formatHostForUrl(host) {
|
|
27437
|
+
if (host.includes(":") && !host.startsWith("["))
|
|
27438
|
+
return `[${host}]`;
|
|
27439
|
+
return host;
|
|
27440
|
+
}
|
|
26410
27441
|
async function startServer(port, options) {
|
|
26411
27442
|
const shouldOpen = options?.open ?? true;
|
|
27443
|
+
const host = options?.host ?? "127.0.0.1";
|
|
26412
27444
|
getDb();
|
|
26413
27445
|
const dashboardDir = resolveDashboardDir();
|
|
26414
|
-
const dashboardExists =
|
|
27446
|
+
const dashboardExists = existsSync3(dashboardDir);
|
|
26415
27447
|
if (!dashboardExists) {
|
|
26416
27448
|
console.error(`
|
|
26417
27449
|
Dashboard not found at: ${dashboardDir}`);
|
|
@@ -26422,10 +27454,19 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26422
27454
|
}
|
|
26423
27455
|
const server2 = Bun.serve({
|
|
26424
27456
|
port,
|
|
27457
|
+
hostname: host,
|
|
26425
27458
|
async fetch(req) {
|
|
26426
27459
|
const url2 = new URL(req.url);
|
|
26427
27460
|
const path = url2.pathname;
|
|
26428
27461
|
const method = req.method;
|
|
27462
|
+
if (path.startsWith("/api/") && method !== "OPTIONS") {
|
|
27463
|
+
if (!isAuthorized(req, host)) {
|
|
27464
|
+
return unauthorizedResponse(port);
|
|
27465
|
+
}
|
|
27466
|
+
if (!isAllowedOrigin(req, port, host)) {
|
|
27467
|
+
return json({ error: "Forbidden" }, 403, port);
|
|
27468
|
+
}
|
|
27469
|
+
}
|
|
26429
27470
|
if (path === "/api/servers" && method === "GET") {
|
|
26430
27471
|
return json(getAllServersWithToolCount(), 200, port);
|
|
26431
27472
|
}
|
|
@@ -26434,15 +27475,39 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26434
27475
|
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
26435
27476
|
if (contentLength > MAX_BODY_SIZE)
|
|
26436
27477
|
return json({ error: "Request body too large" }, 413, port);
|
|
26437
|
-
|
|
26438
|
-
|
|
27478
|
+
let body;
|
|
27479
|
+
try {
|
|
27480
|
+
body = await req.json();
|
|
27481
|
+
} catch {
|
|
27482
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27483
|
+
}
|
|
27484
|
+
const command = body.command?.trim();
|
|
27485
|
+
if (!command)
|
|
26439
27486
|
return json({ error: "Missing 'command'" }, 400, port);
|
|
27487
|
+
const transport = body.transport || "stdio";
|
|
27488
|
+
if (!["stdio", "sse", "streamable-http"].includes(transport)) {
|
|
27489
|
+
return json({ error: "Invalid transport type" }, 400, port);
|
|
27490
|
+
}
|
|
27491
|
+
if (transport !== "stdio" && !body.url) {
|
|
27492
|
+
return json({ error: "Missing 'url' for non-stdio transport" }, 400, port);
|
|
27493
|
+
}
|
|
27494
|
+
if (body.url) {
|
|
27495
|
+
try {
|
|
27496
|
+
new URL(body.url);
|
|
27497
|
+
} catch {
|
|
27498
|
+
return json({ error: "Invalid 'url' format" }, 400, port);
|
|
27499
|
+
}
|
|
27500
|
+
}
|
|
27501
|
+
if (body.args && (!Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string"))) {
|
|
27502
|
+
return json({ error: "Invalid 'args' format" }, 400, port);
|
|
27503
|
+
}
|
|
27504
|
+
const args = body.args || [];
|
|
26440
27505
|
const entry = addServer({
|
|
26441
27506
|
name: body.name,
|
|
26442
|
-
command
|
|
26443
|
-
args
|
|
27507
|
+
command,
|
|
27508
|
+
args,
|
|
26444
27509
|
description: body.description,
|
|
26445
|
-
transport
|
|
27510
|
+
transport,
|
|
26446
27511
|
url: body.url
|
|
26447
27512
|
});
|
|
26448
27513
|
return json(entry, 200, port);
|
|
@@ -26459,7 +27524,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26459
27524
|
if (!entry)
|
|
26460
27525
|
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26461
27526
|
const tools = getCachedTools(id);
|
|
26462
|
-
return json({ ...entry, toolCount: tools.length, tools }, 200, port);
|
|
27527
|
+
return json({ ...redactServer(entry), toolCount: tools.length, tools }, 200, port);
|
|
26463
27528
|
}
|
|
26464
27529
|
if (singleMatch && method === "DELETE") {
|
|
26465
27530
|
const id = singleMatch[1];
|
|
@@ -26477,6 +27542,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26477
27542
|
if (!isValidId(id))
|
|
26478
27543
|
return json({ error: "Invalid server ID" }, 400, port);
|
|
26479
27544
|
try {
|
|
27545
|
+
const existing = getServer(id);
|
|
27546
|
+
if (!existing)
|
|
27547
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26480
27548
|
enableServer(id);
|
|
26481
27549
|
return json({ success: true }, 200, port);
|
|
26482
27550
|
} catch (e) {
|
|
@@ -26489,29 +27557,277 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26489
27557
|
if (!isValidId(id))
|
|
26490
27558
|
return json({ error: "Invalid server ID" }, 400, port);
|
|
26491
27559
|
try {
|
|
27560
|
+
const existing = getServer(id);
|
|
27561
|
+
if (!existing)
|
|
27562
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
26492
27563
|
disableServer(id);
|
|
26493
27564
|
return json({ success: true }, 200, port);
|
|
26494
27565
|
} catch (e) {
|
|
26495
27566
|
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
26496
27567
|
}
|
|
26497
27568
|
}
|
|
27569
|
+
if (singleMatch && method === "PATCH") {
|
|
27570
|
+
const id = singleMatch[1];
|
|
27571
|
+
if (!isValidId(id))
|
|
27572
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27573
|
+
const existing = getServer(id);
|
|
27574
|
+
if (!existing)
|
|
27575
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
27576
|
+
try {
|
|
27577
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
27578
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
27579
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
27580
|
+
let body;
|
|
27581
|
+
try {
|
|
27582
|
+
body = await req.json();
|
|
27583
|
+
} catch {
|
|
27584
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27585
|
+
}
|
|
27586
|
+
const fields = {};
|
|
27587
|
+
if (body.name !== undefined)
|
|
27588
|
+
fields.name = body.name;
|
|
27589
|
+
if (body.description !== undefined)
|
|
27590
|
+
fields.description = body.description;
|
|
27591
|
+
if (body.command !== undefined)
|
|
27592
|
+
fields.command = body.command;
|
|
27593
|
+
if (body.transport !== undefined)
|
|
27594
|
+
fields.transport = body.transport;
|
|
27595
|
+
if (body.url !== undefined)
|
|
27596
|
+
fields.url = body.url;
|
|
27597
|
+
if (body.args !== undefined) {
|
|
27598
|
+
if (!Array.isArray(body.args) || body.args.some((a) => typeof a !== "string")) {
|
|
27599
|
+
return json({ error: "Invalid 'args' format" }, 400, port);
|
|
27600
|
+
}
|
|
27601
|
+
fields.args = body.args;
|
|
27602
|
+
}
|
|
27603
|
+
const updated = updateServer(id, fields);
|
|
27604
|
+
return json(redactServer(updated), 200, port);
|
|
27605
|
+
} catch (e) {
|
|
27606
|
+
return json({ error: e instanceof Error ? e.message : "Failed to update server" }, 500, port);
|
|
27607
|
+
}
|
|
27608
|
+
}
|
|
27609
|
+
const serverEnvMatch = path.match(/^\/api\/servers\/([^/]+)\/env$/);
|
|
27610
|
+
if (serverEnvMatch && method === "POST") {
|
|
27611
|
+
const id = serverEnvMatch[1];
|
|
27612
|
+
if (!isValidId(id))
|
|
27613
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27614
|
+
try {
|
|
27615
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
27616
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
27617
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
27618
|
+
let body;
|
|
27619
|
+
try {
|
|
27620
|
+
body = await req.json();
|
|
27621
|
+
} catch {
|
|
27622
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27623
|
+
}
|
|
27624
|
+
if (!body.key || typeof body.key !== "string")
|
|
27625
|
+
return json({ error: "Missing 'key'" }, 400, port);
|
|
27626
|
+
if (typeof body.value !== "string")
|
|
27627
|
+
return json({ error: "Missing 'value'" }, 400, port);
|
|
27628
|
+
setServerEnv(id, body.key, body.value);
|
|
27629
|
+
return json({ ok: true }, 200, port);
|
|
27630
|
+
} catch (e) {
|
|
27631
|
+
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27632
|
+
}
|
|
27633
|
+
}
|
|
27634
|
+
const serverEnvKeyMatch = path.match(/^\/api\/servers\/([^/]+)\/env\/([^/]+)$/);
|
|
27635
|
+
if (serverEnvKeyMatch && method === "DELETE") {
|
|
27636
|
+
const id = serverEnvKeyMatch[1];
|
|
27637
|
+
const key = decodeURIComponent(serverEnvKeyMatch[2]);
|
|
27638
|
+
if (!isValidId(id))
|
|
27639
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27640
|
+
try {
|
|
27641
|
+
unsetServerEnv(id, key);
|
|
27642
|
+
return json({ ok: true }, 200, port);
|
|
27643
|
+
} catch (e) {
|
|
27644
|
+
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27645
|
+
}
|
|
27646
|
+
}
|
|
27647
|
+
const serverToolsMatch = path.match(/^\/api\/servers\/([^/]+)\/tools$/);
|
|
27648
|
+
if (serverToolsMatch && method === "GET") {
|
|
27649
|
+
const id = serverToolsMatch[1];
|
|
27650
|
+
if (!isValidId(id))
|
|
27651
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27652
|
+
const entry = getServer(id);
|
|
27653
|
+
if (!entry)
|
|
27654
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
27655
|
+
const tools = getCachedTools(id);
|
|
27656
|
+
return json(tools, 200, port);
|
|
27657
|
+
}
|
|
27658
|
+
const serverCallMatch = path.match(/^\/api\/servers\/([^/]+)\/call$/);
|
|
27659
|
+
if (serverCallMatch && method === "POST") {
|
|
27660
|
+
const id = serverCallMatch[1];
|
|
27661
|
+
if (!isValidId(id))
|
|
27662
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27663
|
+
const entry = getServer(id);
|
|
27664
|
+
if (!entry)
|
|
27665
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
27666
|
+
try {
|
|
27667
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
27668
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
27669
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
27670
|
+
let body;
|
|
27671
|
+
try {
|
|
27672
|
+
body = await req.json();
|
|
27673
|
+
} catch {
|
|
27674
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27675
|
+
}
|
|
27676
|
+
if (!body.tool || typeof body.tool !== "string")
|
|
27677
|
+
return json({ error: "Missing 'tool'" }, 400, port);
|
|
27678
|
+
await connectToServer(entry);
|
|
27679
|
+
const toolName = `${id}__${body.tool}`;
|
|
27680
|
+
const result = await callTool(toolName, body.args || {});
|
|
27681
|
+
await disconnectServer(id).catch(() => {
|
|
27682
|
+
return;
|
|
27683
|
+
});
|
|
27684
|
+
return json({ content: result.content }, 200, port);
|
|
27685
|
+
} catch (e) {
|
|
27686
|
+
await disconnectServer(id).catch(() => {
|
|
27687
|
+
return;
|
|
27688
|
+
});
|
|
27689
|
+
return json({ error: e instanceof Error ? e.message : "Failed to call tool" }, 500, port);
|
|
27690
|
+
}
|
|
27691
|
+
}
|
|
27692
|
+
const serverDoctorMatch = path.match(/^\/api\/servers\/([^/]+)\/doctor$/);
|
|
27693
|
+
if (serverDoctorMatch && method === "GET") {
|
|
27694
|
+
const id = serverDoctorMatch[1];
|
|
27695
|
+
if (!isValidId(id))
|
|
27696
|
+
return json({ error: "Invalid server ID" }, 400, port);
|
|
27697
|
+
const entry = getServer(id);
|
|
27698
|
+
if (!entry)
|
|
27699
|
+
return json({ error: `Server '${id}' not found` }, 404, port);
|
|
27700
|
+
try {
|
|
27701
|
+
const report = await diagnoseServer(entry);
|
|
27702
|
+
return json(report, 200, port);
|
|
27703
|
+
} catch (e) {
|
|
27704
|
+
return json({ error: e instanceof Error ? e.message : "Failed to diagnose server" }, 500, port);
|
|
27705
|
+
}
|
|
27706
|
+
}
|
|
27707
|
+
if (path === "/api/sources" && method === "GET") {
|
|
27708
|
+
return json(listSources(), 200, port);
|
|
27709
|
+
}
|
|
27710
|
+
if (path === "/api/sources" && method === "POST") {
|
|
27711
|
+
try {
|
|
27712
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
27713
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
27714
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
27715
|
+
let body;
|
|
27716
|
+
try {
|
|
27717
|
+
body = await req.json();
|
|
27718
|
+
} catch {
|
|
27719
|
+
return json({ error: "Invalid JSON body" }, 400, port);
|
|
27720
|
+
}
|
|
27721
|
+
if (!body.name)
|
|
27722
|
+
return json({ error: "Missing 'name'" }, 400, port);
|
|
27723
|
+
if (!body.type)
|
|
27724
|
+
return json({ error: "Missing 'type'" }, 400, port);
|
|
27725
|
+
if (!body.url)
|
|
27726
|
+
return json({ error: "Missing 'url'" }, 400, port);
|
|
27727
|
+
const source = addSource({
|
|
27728
|
+
name: body.name,
|
|
27729
|
+
type: body.type,
|
|
27730
|
+
url: body.url,
|
|
27731
|
+
description: body.description
|
|
27732
|
+
});
|
|
27733
|
+
return json(source, 200, port);
|
|
27734
|
+
} catch (e) {
|
|
27735
|
+
return json({ error: e instanceof Error ? e.message : "Failed to add source" }, 500, port);
|
|
27736
|
+
}
|
|
27737
|
+
}
|
|
27738
|
+
const singleSourceMatch = path.match(/^\/api\/sources\/([^/]+)$/);
|
|
27739
|
+
if (singleSourceMatch && method === "DELETE") {
|
|
27740
|
+
const id = singleSourceMatch[1];
|
|
27741
|
+
try {
|
|
27742
|
+
removeSource(id);
|
|
27743
|
+
return json({ ok: true }, 200, port);
|
|
27744
|
+
} catch (e) {
|
|
27745
|
+
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27746
|
+
}
|
|
27747
|
+
}
|
|
27748
|
+
const sourceEnableMatch = path.match(/^\/api\/sources\/([^/]+)\/enable$/);
|
|
27749
|
+
if (sourceEnableMatch && method === "POST") {
|
|
27750
|
+
const id = sourceEnableMatch[1];
|
|
27751
|
+
try {
|
|
27752
|
+
enableSource(id);
|
|
27753
|
+
return json({ ok: true }, 200, port);
|
|
27754
|
+
} catch (e) {
|
|
27755
|
+
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27756
|
+
}
|
|
27757
|
+
}
|
|
27758
|
+
const sourceDisableMatch = path.match(/^\/api\/sources\/([^/]+)\/disable$/);
|
|
27759
|
+
if (sourceDisableMatch && method === "POST") {
|
|
27760
|
+
const id = sourceDisableMatch[1];
|
|
27761
|
+
try {
|
|
27762
|
+
disableSource(id);
|
|
27763
|
+
return json({ ok: true }, 200, port);
|
|
27764
|
+
} catch (e) {
|
|
27765
|
+
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27766
|
+
}
|
|
27767
|
+
}
|
|
27768
|
+
if (path === "/api/find" && method === "GET") {
|
|
27769
|
+
try {
|
|
27770
|
+
const q = url2.searchParams.get("q") || "";
|
|
27771
|
+
const sourcesParam = url2.searchParams.get("sources");
|
|
27772
|
+
const limitParam = url2.searchParams.get("limit");
|
|
27773
|
+
const sources = sourcesParam ? sourcesParam.split(",").filter(Boolean) : undefined;
|
|
27774
|
+
const limit = limitParam ? parseInt(limitParam, 10) : undefined;
|
|
27775
|
+
const results = await findServers(q, { sources, limit });
|
|
27776
|
+
return json(results, 200, port);
|
|
27777
|
+
} catch (e) {
|
|
27778
|
+
return json({ error: e instanceof Error ? e.message : "Find failed" }, 500, port);
|
|
27779
|
+
}
|
|
27780
|
+
}
|
|
27781
|
+
if (path === "/api/update" && method === "POST") {
|
|
27782
|
+
if (!isLoopbackHost(host)) {
|
|
27783
|
+
return json({ error: "Update only allowed on loopback host" }, 403, port);
|
|
27784
|
+
}
|
|
27785
|
+
try {
|
|
27786
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
27787
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
27788
|
+
const currentVersion = pkg.version;
|
|
27789
|
+
const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
|
|
27790
|
+
encoding: "utf-8"
|
|
27791
|
+
}).trim();
|
|
27792
|
+
if (latest === currentVersion) {
|
|
27793
|
+
return json({ success: true, current: currentVersion, latest, upToDate: true }, 200, port);
|
|
27794
|
+
}
|
|
27795
|
+
execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "pipe" });
|
|
27796
|
+
return json({ success: true, current: currentVersion, latest, upToDate: false, updated: true }, 200, port);
|
|
27797
|
+
} catch (e) {
|
|
27798
|
+
return json({ error: e instanceof Error ? e.message : "Update failed" }, 500, port);
|
|
27799
|
+
}
|
|
27800
|
+
}
|
|
27801
|
+
if (path === "/api/version" && method === "GET") {
|
|
27802
|
+
try {
|
|
27803
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
27804
|
+
return json({ version: pkg.version }, 200, port);
|
|
27805
|
+
} catch {
|
|
27806
|
+
return json({ version: "unknown" }, 200, port);
|
|
27807
|
+
}
|
|
27808
|
+
}
|
|
26498
27809
|
if (method === "OPTIONS") {
|
|
27810
|
+
if (!isAllowedOrigin(req, port, host)) {
|
|
27811
|
+
return json({ error: "Forbidden" }, 403, port);
|
|
27812
|
+
}
|
|
26499
27813
|
return new Response(null, {
|
|
26500
27814
|
headers: {
|
|
26501
27815
|
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
26502
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
27816
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
26503
27817
|
"Access-Control-Allow-Headers": "Content-Type"
|
|
26504
27818
|
}
|
|
26505
27819
|
});
|
|
26506
27820
|
}
|
|
26507
27821
|
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
26508
27822
|
if (path !== "/") {
|
|
26509
|
-
const
|
|
26510
|
-
|
|
26511
|
-
|
|
26512
|
-
|
|
27823
|
+
const safePath = resolveStaticPath(dashboardDir, path);
|
|
27824
|
+
if (safePath) {
|
|
27825
|
+
const res2 = serveStaticFile(safePath);
|
|
27826
|
+
if (res2)
|
|
27827
|
+
return res2;
|
|
27828
|
+
}
|
|
26513
27829
|
}
|
|
26514
|
-
const indexPath =
|
|
27830
|
+
const indexPath = join5(dashboardDir, "index.html");
|
|
26515
27831
|
const res = serveStaticFile(indexPath);
|
|
26516
27832
|
if (res)
|
|
26517
27833
|
return res;
|
|
@@ -26526,19 +27842,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
26526
27842
|
};
|
|
26527
27843
|
process.on("SIGINT", shutdown);
|
|
26528
27844
|
process.on("SIGTERM", shutdown);
|
|
26529
|
-
const
|
|
27845
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
27846
|
+
const serverUrl = `http://${formatHostForUrl(displayHost)}:${port}`;
|
|
26530
27847
|
console.log(`MCPs Dashboard running at ${serverUrl}`);
|
|
26531
27848
|
if (shouldOpen) {
|
|
26532
27849
|
try {
|
|
26533
|
-
const {
|
|
26534
|
-
|
|
26535
|
-
|
|
27850
|
+
const { execFile } = await import("child_process");
|
|
27851
|
+
if (process.platform === "win32") {
|
|
27852
|
+
execFile("cmd", ["/c", "start", "", serverUrl]);
|
|
27853
|
+
} else {
|
|
27854
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
27855
|
+
execFile(openCmd, [serverUrl]);
|
|
27856
|
+
}
|
|
26536
27857
|
} catch {}
|
|
26537
27858
|
}
|
|
26538
27859
|
}
|
|
26539
27860
|
|
|
26540
27861
|
// src/cli/components/App.tsx
|
|
26541
|
-
import { useState as useState7 } from "react";
|
|
27862
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
26542
27863
|
import { Box as Box7, Text as Text9, useApp, useInput as useInput4 } from "ink";
|
|
26543
27864
|
|
|
26544
27865
|
// src/cli/components/ServerList.tsx
|
|
@@ -26951,6 +28272,7 @@ var SelectInput_default = SelectInput;
|
|
|
26951
28272
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
26952
28273
|
function ServerList({ onSelect, onSearch }) {
|
|
26953
28274
|
const servers = listServers();
|
|
28275
|
+
const toolCounts = getToolCounts();
|
|
26954
28276
|
useInput2((input) => {
|
|
26955
28277
|
if (input === "s") {
|
|
26956
28278
|
onSearch();
|
|
@@ -26972,10 +28294,9 @@ function ServerList({ onSelect, onSearch }) {
|
|
|
26972
28294
|
}, undefined, true, undefined, this);
|
|
26973
28295
|
}
|
|
26974
28296
|
const items = servers.map((s) => {
|
|
26975
|
-
const
|
|
28297
|
+
const cachedCount = toolCounts.get(s.id) ?? 0;
|
|
26976
28298
|
const status = s.enabled ? "\u25CF" : "\u25CB";
|
|
26977
|
-
const
|
|
26978
|
-
const toolInfo = cached2.length > 0 ? ` (${cached2.length} tools)` : "";
|
|
28299
|
+
const toolInfo = cachedCount > 0 ? ` (${cachedCount} tools)` : "";
|
|
26979
28300
|
return {
|
|
26980
28301
|
label: `${status} ${s.name} [${s.id}]${toolInfo}`,
|
|
26981
28302
|
value: s.id,
|
|
@@ -27033,7 +28354,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
|
|
|
27033
28354
|
const [loading, setLoading] = useState3(false);
|
|
27034
28355
|
const [error2, setError] = useState3(null);
|
|
27035
28356
|
const cachedTools = getCachedTools(server2.id);
|
|
28357
|
+
const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
|
|
27036
28358
|
useEffect3(() => {
|
|
28359
|
+
setLoading(false);
|
|
28360
|
+
setError(null);
|
|
27037
28361
|
if (cachedTools.length > 0) {
|
|
27038
28362
|
setTools(cachedTools.map((t) => ({
|
|
27039
28363
|
server_id: server2.id,
|
|
@@ -27041,8 +28365,10 @@ function ServerDetail({ server: server2, onSelectTool, onBack }) {
|
|
|
27041
28365
|
description: t.description,
|
|
27042
28366
|
input_schema: t.input_schema
|
|
27043
28367
|
})));
|
|
28368
|
+
} else {
|
|
28369
|
+
setTools([]);
|
|
27044
28370
|
}
|
|
27045
|
-
}, [server2.id]);
|
|
28371
|
+
}, [server2.id, cachedKey]);
|
|
27046
28372
|
const handleConnect = async () => {
|
|
27047
28373
|
setLoading(true);
|
|
27048
28374
|
setError(null);
|
|
@@ -27366,6 +28692,7 @@ function SearchView({ onBack }) {
|
|
|
27366
28692
|
// src/cli/components/ToolCall.tsx
|
|
27367
28693
|
import { useState as useState6 } from "react";
|
|
27368
28694
|
import { Box as Box6, Text as Text8 } from "ink";
|
|
28695
|
+
init_config();
|
|
27369
28696
|
import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
|
|
27370
28697
|
function ToolCall({ server: server2, tool, onBack }) {
|
|
27371
28698
|
const [argsInput, setArgsInput] = useState6("{}");
|
|
@@ -27496,6 +28823,7 @@ function ToolCall({ server: server2, tool, onBack }) {
|
|
|
27496
28823
|
}
|
|
27497
28824
|
|
|
27498
28825
|
// src/cli/components/App.tsx
|
|
28826
|
+
init_db();
|
|
27499
28827
|
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
27500
28828
|
function App() {
|
|
27501
28829
|
const { exit } = useApp();
|
|
@@ -27503,7 +28831,7 @@ function App() {
|
|
|
27503
28831
|
const [selectedServer, setSelectedServer] = useState7(null);
|
|
27504
28832
|
const [selectedTool, setSelectedTool] = useState7(null);
|
|
27505
28833
|
useInput4((input, key) => {
|
|
27506
|
-
if (input === "q" && view
|
|
28834
|
+
if (input === "q" && view !== "search" && view !== "call") {
|
|
27507
28835
|
exit();
|
|
27508
28836
|
}
|
|
27509
28837
|
if (key.escape) {
|
|
@@ -27520,6 +28848,13 @@ function App() {
|
|
|
27520
28848
|
}
|
|
27521
28849
|
}
|
|
27522
28850
|
});
|
|
28851
|
+
useEffect5(() => {
|
|
28852
|
+
return () => {
|
|
28853
|
+
disconnectAll().catch(() => {
|
|
28854
|
+
return;
|
|
28855
|
+
}).finally(() => closeDb());
|
|
28856
|
+
};
|
|
28857
|
+
}, []);
|
|
27523
28858
|
const handleSelectServer = (server2) => {
|
|
27524
28859
|
setSelectedServer(server2);
|
|
27525
28860
|
setView("detail");
|
|
@@ -27581,7 +28916,7 @@ function App() {
|
|
|
27581
28916
|
marginTop: 1,
|
|
27582
28917
|
children: /* @__PURE__ */ jsxDEV5(Text9, {
|
|
27583
28918
|
dimColor: true,
|
|
27584
|
-
children: view === "servers" ? "\u2191\u2193 navigate \xB7 enter select \xB7 s search \xB7 q quit" : "esc back \xB7 q quit"
|
|
28919
|
+
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
28920
|
}, undefined, false, undefined, this)
|
|
27586
28921
|
}, undefined, false, undefined, this)
|
|
27587
28922
|
]
|
|
@@ -27589,23 +28924,62 @@ function App() {
|
|
|
27589
28924
|
}
|
|
27590
28925
|
|
|
27591
28926
|
// src/cli/index.tsx
|
|
28927
|
+
var VERSION2 = (() => {
|
|
28928
|
+
try {
|
|
28929
|
+
const pkgPath = join6(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
|
|
28930
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
28931
|
+
return pkg.version || "0.0.1";
|
|
28932
|
+
} catch {
|
|
28933
|
+
return "0.0.1";
|
|
28934
|
+
}
|
|
28935
|
+
})();
|
|
27592
28936
|
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(() => {
|
|
28937
|
+
program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
|
|
28938
|
+
program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").option("--verbose", "Show detailed info including health, command, and transport").action((opts) => {
|
|
27595
28939
|
const servers = listServers();
|
|
28940
|
+
if (opts.json) {
|
|
28941
|
+
const toolCounts2 = getToolCounts();
|
|
28942
|
+
console.log(JSON.stringify(servers.map((s) => ({ ...s, toolCount: toolCounts2.get(s.id) ?? 0 })), null, 2));
|
|
28943
|
+
closeDb();
|
|
28944
|
+
return;
|
|
28945
|
+
}
|
|
27596
28946
|
if (servers.length === 0) {
|
|
27597
28947
|
console.log(chalk2.dim("No servers registered. Use `mcps add` or `mcps search` to get started."));
|
|
27598
28948
|
closeDb();
|
|
27599
28949
|
return;
|
|
27600
28950
|
}
|
|
28951
|
+
const toolCounts = getToolCounts();
|
|
27601
28952
|
for (const s of servers) {
|
|
27602
28953
|
const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
|
|
27603
|
-
const
|
|
27604
|
-
const toolCount =
|
|
27605
|
-
|
|
28954
|
+
const cachedCount = toolCounts.get(s.id) ?? 0;
|
|
28955
|
+
const toolCount = cachedCount > 0 ? chalk2.dim(` (${cachedCount} tools)`) : "";
|
|
28956
|
+
const errorWarning = s.last_error ? chalk2.red(" \u26A0") : "";
|
|
28957
|
+
console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
|
|
27606
28958
|
if (s.description)
|
|
27607
28959
|
console.log(` ${chalk2.dim(s.description)}`);
|
|
27608
|
-
|
|
28960
|
+
if (opts.verbose) {
|
|
28961
|
+
console.log(` Command: ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
|
|
28962
|
+
console.log(` Transport: ${chalk2.dim(s.transport)}`);
|
|
28963
|
+
const now = Date.now();
|
|
28964
|
+
if (s.last_connected_at) {
|
|
28965
|
+
const connectedAt = new Date(s.last_connected_at).getTime();
|
|
28966
|
+
const daysDiff = Math.floor((now - connectedAt) / (1000 * 60 * 60 * 24));
|
|
28967
|
+
const connectedLabel = daysDiff === 0 ? "today" : daysDiff === 1 ? "1 day ago" : `${daysDiff} days ago`;
|
|
28968
|
+
const connectedColor = !s.last_error && daysDiff < 7 ? chalk2.green : chalk2.yellow;
|
|
28969
|
+
console.log(` Connected: ${connectedColor(connectedLabel)}`);
|
|
28970
|
+
} else {
|
|
28971
|
+
console.log(` Connected: ${chalk2.dim("never")}`);
|
|
28972
|
+
}
|
|
28973
|
+
if (s.last_error) {
|
|
28974
|
+
console.log(` Error: ${chalk2.red(s.last_error)}`);
|
|
28975
|
+
}
|
|
28976
|
+
const hasError = !!s.last_error;
|
|
28977
|
+
const daysSinceConnect = s.last_connected_at ? Math.floor((Date.now() - new Date(s.last_connected_at).getTime()) / (1000 * 60 * 60 * 24)) : Infinity;
|
|
28978
|
+
const healthIcon = hasError ? chalk2.red("\u2717 unhealthy") : daysSinceConnect < 7 ? chalk2.green("\u2713 healthy") : chalk2.yellow("\u26A0 stale");
|
|
28979
|
+
console.log(` Health: ${healthIcon}`);
|
|
28980
|
+
} else {
|
|
28981
|
+
console.log(` ${chalk2.dim(`${s.command} ${s.args.join(" ")}`)}`);
|
|
28982
|
+
}
|
|
27609
28983
|
}
|
|
27610
28984
|
closeDb();
|
|
27611
28985
|
});
|
|
@@ -27630,10 +29004,25 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
|
|
|
27630
29004
|
} catch (err) {
|
|
27631
29005
|
console.error(chalk2.red(`Search failed: ${err.message}`));
|
|
27632
29006
|
process.exit(1);
|
|
29007
|
+
} finally {
|
|
29008
|
+
closeDb();
|
|
27633
29009
|
}
|
|
27634
|
-
closeDb();
|
|
27635
29010
|
});
|
|
27636
|
-
|
|
29011
|
+
async function promptReadline(rl, question) {
|
|
29012
|
+
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
29013
|
+
}
|
|
29014
|
+
function detectSourceType(url2) {
|
|
29015
|
+
if (url2.includes("raw.githubusercontent.com") || url2.endsWith(".md"))
|
|
29016
|
+
return "awesome-list";
|
|
29017
|
+
if (url2.includes("registry.npmjs.org"))
|
|
29018
|
+
return "npm-search";
|
|
29019
|
+
if (url2.includes("api.github.com/search"))
|
|
29020
|
+
return "github-topic";
|
|
29021
|
+
if (url2.includes("/v0/servers") || url2.includes("/servers"))
|
|
29022
|
+
return "mcp-registry";
|
|
29023
|
+
return null;
|
|
29024
|
+
}
|
|
29025
|
+
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").option("--wizard", "Interactive setup wizard").option("--force", "Register even if duplicate command exists").description("Add a local MCP server").action(async (command, args, opts) => {
|
|
27637
29026
|
try {
|
|
27638
29027
|
if (opts.fromRegistry) {
|
|
27639
29028
|
console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
|
|
@@ -27643,14 +29032,84 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
27643
29032
|
closeDb();
|
|
27644
29033
|
return;
|
|
27645
29034
|
}
|
|
29035
|
+
if (opts.wizard) {
|
|
29036
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29037
|
+
const transport = await promptReadline(rl, "Transport [stdio/sse/http] (default: stdio): ") || "stdio";
|
|
29038
|
+
const wizardCommand = await promptReadline(rl, "Command (e.g. npx, node, bunx): ");
|
|
29039
|
+
if (!wizardCommand) {
|
|
29040
|
+
console.error(chalk2.red("Command is required"));
|
|
29041
|
+
rl.close();
|
|
29042
|
+
closeDb();
|
|
29043
|
+
process.exit(1);
|
|
29044
|
+
}
|
|
29045
|
+
const argsStr = await promptReadline(rl, "Arguments (space-separated, e.g. -y @pkg/name): ");
|
|
29046
|
+
const wizardArgs = argsStr.trim() ? argsStr.trim().split(/\s+/) : [];
|
|
29047
|
+
const wizardName = await promptReadline(rl, "Display name (optional, press enter to skip): ");
|
|
29048
|
+
const wizardDescription = await promptReadline(rl, "Description (optional): ");
|
|
29049
|
+
const env = {};
|
|
29050
|
+
console.log(chalk2.dim("Add env vars (KEY=VALUE). Press enter with empty key to skip."));
|
|
29051
|
+
while (true) {
|
|
29052
|
+
const pair = await promptReadline(rl, " Env var (KEY=VALUE or empty to done): ");
|
|
29053
|
+
if (!pair.trim())
|
|
29054
|
+
break;
|
|
29055
|
+
const eqIdx = pair.indexOf("=");
|
|
29056
|
+
if (eqIdx > 0)
|
|
29057
|
+
env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
29058
|
+
}
|
|
29059
|
+
rl.close();
|
|
29060
|
+
console.log(chalk2.bold(`
|
|
29061
|
+
Server to add:`));
|
|
29062
|
+
console.log(` Command: ${wizardCommand} ${wizardArgs.join(" ")}`);
|
|
29063
|
+
console.log(` Transport: ${transport}`);
|
|
29064
|
+
if (wizardName)
|
|
29065
|
+
console.log(` Name: ${wizardName}`);
|
|
29066
|
+
if (Object.keys(env).length)
|
|
29067
|
+
console.log(` Env: ${Object.keys(env).join(", ")}`);
|
|
29068
|
+
const confirm = await new Promise((resolve2) => {
|
|
29069
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29070
|
+
rl2.question(chalk2.bold("Add this server? [Y/n]: "), (ans) => {
|
|
29071
|
+
rl2.close();
|
|
29072
|
+
resolve2(ans);
|
|
29073
|
+
});
|
|
29074
|
+
});
|
|
29075
|
+
if (confirm.toLowerCase() === "n") {
|
|
29076
|
+
console.log("Aborted.");
|
|
29077
|
+
closeDb();
|
|
29078
|
+
return;
|
|
29079
|
+
}
|
|
29080
|
+
const server3 = addServer({
|
|
29081
|
+
command: wizardCommand,
|
|
29082
|
+
args: wizardArgs,
|
|
29083
|
+
name: wizardName || undefined,
|
|
29084
|
+
description: wizardDescription || undefined,
|
|
29085
|
+
transport,
|
|
29086
|
+
env
|
|
29087
|
+
});
|
|
29088
|
+
console.log(chalk2.green(`Added: ${server3.name} [${server3.id}]`));
|
|
29089
|
+
closeDb();
|
|
29090
|
+
return;
|
|
29091
|
+
}
|
|
27646
29092
|
if (!command) {
|
|
27647
|
-
console.error(chalk2.red("Error: command is required (or use --from-registry)"));
|
|
29093
|
+
console.error(chalk2.red("Error: command is required (or use --from-registry or --wizard)"));
|
|
29094
|
+
closeDb();
|
|
27648
29095
|
process.exit(1);
|
|
27649
29096
|
}
|
|
29097
|
+
const existing = listServers();
|
|
29098
|
+
const duplicate = existing.find((s) => s.command === command && JSON.stringify(s.args) === JSON.stringify(args));
|
|
29099
|
+
if (duplicate) {
|
|
29100
|
+
console.log(chalk2.yellow(`Warning: server "${duplicate.name}" [${duplicate.id}] already uses this command.`));
|
|
29101
|
+
if (!opts.force) {
|
|
29102
|
+
console.log(chalk2.dim("Use --force to register anyway."));
|
|
29103
|
+
closeDb();
|
|
29104
|
+
return;
|
|
29105
|
+
}
|
|
29106
|
+
}
|
|
27650
29107
|
const envMap = {};
|
|
27651
29108
|
if (opts.env) {
|
|
27652
29109
|
for (const pair of opts.env) {
|
|
27653
29110
|
const [key, ...rest] = pair.split("=");
|
|
29111
|
+
if (!key)
|
|
29112
|
+
continue;
|
|
27654
29113
|
envMap[key] = rest.join("=");
|
|
27655
29114
|
}
|
|
27656
29115
|
}
|
|
@@ -27671,6 +29130,43 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
27671
29130
|
} else {
|
|
27672
29131
|
console.error(chalk2.red(`Failed to add server: ${err.message}`));
|
|
27673
29132
|
}
|
|
29133
|
+
closeDb();
|
|
29134
|
+
process.exit(1);
|
|
29135
|
+
}
|
|
29136
|
+
closeDb();
|
|
29137
|
+
});
|
|
29138
|
+
program2.command("update-server").argument("<id>", "Server ID to update").description("Update fields of a registered server").option("--name <name>", "New display name").option("--description <desc>", "New description").option("--command <cmd>", "New command").option("--args <args...>", "New args list").option("--transport <type>", "New transport type").option("--url <url>", "New URL").action((id, opts) => {
|
|
29139
|
+
const server2 = getServer(id);
|
|
29140
|
+
if (!server2) {
|
|
29141
|
+
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29142
|
+
closeDb();
|
|
29143
|
+
process.exit(1);
|
|
29144
|
+
}
|
|
29145
|
+
const fields = {};
|
|
29146
|
+
if (opts.name !== undefined)
|
|
29147
|
+
fields.name = opts.name;
|
|
29148
|
+
if (opts.description !== undefined)
|
|
29149
|
+
fields.description = opts.description;
|
|
29150
|
+
if (opts.command !== undefined)
|
|
29151
|
+
fields.command = opts.command;
|
|
29152
|
+
if (opts.args !== undefined)
|
|
29153
|
+
fields.args = opts.args;
|
|
29154
|
+
if (opts.transport !== undefined)
|
|
29155
|
+
fields.transport = opts.transport;
|
|
29156
|
+
if (opts.url !== undefined)
|
|
29157
|
+
fields.url = opts.url;
|
|
29158
|
+
const updated = updateServer(id, fields);
|
|
29159
|
+
console.log(chalk2.green(`Updated server: ${updated.name} [${updated.id}]`));
|
|
29160
|
+
closeDb();
|
|
29161
|
+
});
|
|
29162
|
+
program2.command("clone").argument("<id>", "Server ID to clone").argument("<new-name>", "Name for the cloned server").description("Clone a server with a new name").action((id, newName) => {
|
|
29163
|
+
try {
|
|
29164
|
+
const cloned = cloneServer(id, newName);
|
|
29165
|
+
console.log(chalk2.green(`Cloned server: ${cloned.name} [${cloned.id}]`));
|
|
29166
|
+
console.log(chalk2.dim(` ${cloned.command} ${cloned.args.join(" ")}`));
|
|
29167
|
+
} catch (err) {
|
|
29168
|
+
console.error(chalk2.red(err.message));
|
|
29169
|
+
closeDb();
|
|
27674
29170
|
process.exit(1);
|
|
27675
29171
|
}
|
|
27676
29172
|
closeDb();
|
|
@@ -27679,6 +29175,7 @@ program2.command("remove").argument("<id>", "Server ID to remove").description("
|
|
|
27679
29175
|
const server2 = getServer(id);
|
|
27680
29176
|
if (!server2) {
|
|
27681
29177
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29178
|
+
closeDb();
|
|
27682
29179
|
process.exit(1);
|
|
27683
29180
|
}
|
|
27684
29181
|
removeServer(id);
|
|
@@ -27689,6 +29186,7 @@ program2.command("enable").argument("<id>", "Server ID to enable").description("
|
|
|
27689
29186
|
const server2 = getServer(id);
|
|
27690
29187
|
if (!server2) {
|
|
27691
29188
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29189
|
+
closeDb();
|
|
27692
29190
|
process.exit(1);
|
|
27693
29191
|
}
|
|
27694
29192
|
enableServer(id);
|
|
@@ -27699,6 +29197,7 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
|
|
|
27699
29197
|
const server2 = getServer(id);
|
|
27700
29198
|
if (!server2) {
|
|
27701
29199
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29200
|
+
closeDb();
|
|
27702
29201
|
process.exit(1);
|
|
27703
29202
|
}
|
|
27704
29203
|
disableServer(id);
|
|
@@ -27778,6 +29277,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
|
|
|
27778
29277
|
}
|
|
27779
29278
|
}
|
|
27780
29279
|
}
|
|
29280
|
+
let exitCode = 0;
|
|
27781
29281
|
try {
|
|
27782
29282
|
console.log(chalk2.dim(`Connecting to servers...`));
|
|
27783
29283
|
await connectAllEnabled();
|
|
@@ -27786,17 +29286,22 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
|
|
|
27786
29286
|
for (const c of result.content) {
|
|
27787
29287
|
console.log(c.text);
|
|
27788
29288
|
}
|
|
27789
|
-
await disconnectAll();
|
|
27790
29289
|
} catch (err) {
|
|
27791
29290
|
console.error(chalk2.red(`Call failed: ${err.message}`));
|
|
27792
|
-
|
|
29291
|
+
exitCode = 1;
|
|
27793
29292
|
}
|
|
29293
|
+
await disconnectAll().catch(() => {
|
|
29294
|
+
return;
|
|
29295
|
+
});
|
|
27794
29296
|
closeDb();
|
|
29297
|
+
if (exitCode !== 0)
|
|
29298
|
+
process.exit(exitCode);
|
|
27795
29299
|
});
|
|
27796
29300
|
program2.command("info").argument("<id>", "Server ID").description("Show server details & tools").action((id) => {
|
|
27797
29301
|
const server2 = getServer(id);
|
|
27798
29302
|
if (!server2) {
|
|
27799
29303
|
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29304
|
+
closeDb();
|
|
27800
29305
|
process.exit(1);
|
|
27801
29306
|
}
|
|
27802
29307
|
console.log(chalk2.bold(server2.name) + " " + chalk2.dim(`[${server2.id}]`));
|
|
@@ -27825,21 +29330,553 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
|
|
|
27825
29330
|
}
|
|
27826
29331
|
closeDb();
|
|
27827
29332
|
});
|
|
27828
|
-
program2.command("status").description("Show registry stats").action(() => {
|
|
29333
|
+
program2.command("status").description("Show registry stats").option("--json", "Output as JSON").action((opts) => {
|
|
27829
29334
|
const servers = listServers();
|
|
27830
29335
|
const enabled = servers.filter((s) => s.enabled).length;
|
|
27831
29336
|
const disabled = servers.length - enabled;
|
|
29337
|
+
const toolCounts = getToolCounts();
|
|
27832
29338
|
let totalTools = 0;
|
|
27833
|
-
for (const s of servers)
|
|
27834
|
-
totalTools +=
|
|
29339
|
+
for (const s of servers)
|
|
29340
|
+
totalTools += toolCounts.get(s.id) ?? 0;
|
|
29341
|
+
if (opts.json) {
|
|
29342
|
+
console.log(JSON.stringify({ total: servers.length, enabled, disabled, totalTools }, null, 2));
|
|
29343
|
+
closeDb();
|
|
29344
|
+
return;
|
|
27835
29345
|
}
|
|
27836
29346
|
console.log(chalk2.bold("Registry Status"));
|
|
27837
29347
|
console.log(` Servers: ${servers.length} (${chalk2.green(`${enabled} enabled`)}, ${chalk2.red(`${disabled} disabled`)})`);
|
|
27838
29348
|
console.log(` Tools: ${totalTools} (cached)`);
|
|
27839
29349
|
closeDb();
|
|
27840
29350
|
});
|
|
27841
|
-
program2.command("
|
|
27842
|
-
|
|
29351
|
+
program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").option("--fix", "Attempt to fix issues automatically").action(async (id, opts) => {
|
|
29352
|
+
const { execFileSync: execFileSync22 } = await import("child_process");
|
|
29353
|
+
const servers = id ? [getServer(id)].filter(Boolean) : listServers();
|
|
29354
|
+
if (servers.length === 0) {
|
|
29355
|
+
console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
|
|
29356
|
+
closeDb();
|
|
29357
|
+
return;
|
|
29358
|
+
}
|
|
29359
|
+
let allHealthy = true;
|
|
29360
|
+
for (const server2 of servers) {
|
|
29361
|
+
console.log(chalk2.bold(`
|
|
29362
|
+
${server2.name} [${server2.id}]`));
|
|
29363
|
+
const report = await diagnoseServer(server2);
|
|
29364
|
+
for (const check2 of report.checks) {
|
|
29365
|
+
const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
|
|
29366
|
+
console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
|
|
29367
|
+
if (!check2.pass && opts.fix && check2.fixable && check2.fixHint) {
|
|
29368
|
+
console.log(chalk2.dim(` Attempting fix: ${check2.fixHint}`));
|
|
29369
|
+
try {
|
|
29370
|
+
execFileSync22("npm", ["install", "-g", check2.fixHint.replace("npm install -g ", "")], { stdio: "inherit" });
|
|
29371
|
+
console.log(chalk2.green(` Fixed!`));
|
|
29372
|
+
} catch {
|
|
29373
|
+
console.log(chalk2.red(` Fix failed`));
|
|
29374
|
+
}
|
|
29375
|
+
}
|
|
29376
|
+
}
|
|
29377
|
+
if (!report.healthy)
|
|
29378
|
+
allHealthy = false;
|
|
29379
|
+
}
|
|
29380
|
+
console.log("");
|
|
29381
|
+
if (allHealthy) {
|
|
29382
|
+
console.log(chalk2.green("All checks passed."));
|
|
29383
|
+
} else {
|
|
29384
|
+
console.log(chalk2.red("Some checks failed. Fix issues above."));
|
|
29385
|
+
}
|
|
29386
|
+
closeDb();
|
|
29387
|
+
});
|
|
29388
|
+
program2.command("completion").argument("<shell>", "Shell type: bash, zsh, fish").description("Generate shell completion script").action((shell) => {
|
|
29389
|
+
const commands = ["list", "search", "find", "add", "remove", "enable", "disable", "info", "status", "tools", "call", "doctor", "install", "export", "import", "env", "sources", "clone", "update-server", "serve", "update", "mcp", "completion"];
|
|
29390
|
+
if (shell === "bash") {
|
|
29391
|
+
console.log(`# Add to ~/.bashrc: eval "$(mcps completion bash)"
|
|
29392
|
+
_mcps_complete() {
|
|
29393
|
+
local cur prev words
|
|
29394
|
+
COMPREPLY=()
|
|
29395
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
29396
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
29397
|
+
local cmds="${commands.join(" ")}"
|
|
29398
|
+
if [ $COMP_CWORD -eq 1 ]; then
|
|
29399
|
+
COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
|
|
29400
|
+
fi
|
|
29401
|
+
}
|
|
29402
|
+
complete -F _mcps_complete mcps`);
|
|
29403
|
+
} else if (shell === "zsh") {
|
|
29404
|
+
console.log(`# Add to ~/.zshrc: eval "$(mcps completion zsh)"
|
|
29405
|
+
_mcps() {
|
|
29406
|
+
local -a cmds
|
|
29407
|
+
cmds=(${commands.map((c) => `'${c}'`).join(" ")})
|
|
29408
|
+
_describe 'commands' cmds
|
|
29409
|
+
}
|
|
29410
|
+
compdef _mcps mcps`);
|
|
29411
|
+
} else if (shell === "fish") {
|
|
29412
|
+
const lines = commands.map((c) => `complete -c mcps -f -a '${c}'`).join(`
|
|
29413
|
+
`);
|
|
29414
|
+
console.log(`# Add to ~/.config/fish/completions/mcps.fish
|
|
29415
|
+
${lines}`);
|
|
29416
|
+
} else {
|
|
29417
|
+
console.error(chalk2.red(`Unknown shell: ${shell}. Use bash, zsh, or fish.`));
|
|
29418
|
+
process.exit(1);
|
|
29419
|
+
}
|
|
29420
|
+
closeDb();
|
|
29421
|
+
});
|
|
29422
|
+
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) => {
|
|
29423
|
+
await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
|
|
29424
|
+
});
|
|
29425
|
+
program2.command("update").description("Update mcps to the latest version").action(async () => {
|
|
29426
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
29427
|
+
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
29428
|
+
const currentVersion = pkg.version;
|
|
29429
|
+
console.log(chalk2.dim(`Current version: ${currentVersion}`));
|
|
29430
|
+
console.log(chalk2.dim("Checking for updates..."));
|
|
29431
|
+
try {
|
|
29432
|
+
const latest = execFileSync3("npm", ["view", "@hasna/mcps", "version"], {
|
|
29433
|
+
encoding: "utf-8"
|
|
29434
|
+
}).trim();
|
|
29435
|
+
if (latest === currentVersion) {
|
|
29436
|
+
console.log(chalk2.green(`Already on the latest version (${currentVersion}).`));
|
|
29437
|
+
return;
|
|
29438
|
+
}
|
|
29439
|
+
console.log(chalk2.dim(`New version available: ${latest}`));
|
|
29440
|
+
console.log(chalk2.dim("Updating..."));
|
|
29441
|
+
execFileSync3("bun", ["install", "-g", "@hasna/mcps@latest"], { stdio: "inherit" });
|
|
29442
|
+
console.log(chalk2.green(`Updated to ${latest}`));
|
|
29443
|
+
} catch (err) {
|
|
29444
|
+
console.error(chalk2.red(`Update failed: ${err.message}`));
|
|
29445
|
+
closeDb();
|
|
29446
|
+
process.exit(1);
|
|
29447
|
+
}
|
|
29448
|
+
});
|
|
29449
|
+
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) => {
|
|
29450
|
+
try {
|
|
29451
|
+
if (opts.awesome) {
|
|
29452
|
+
console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
|
|
29453
|
+
const results2 = await listAwesomeServers();
|
|
29454
|
+
if (opts.json) {
|
|
29455
|
+
console.log(JSON.stringify(results2, null, 2));
|
|
29456
|
+
closeDb();
|
|
29457
|
+
return;
|
|
29458
|
+
}
|
|
29459
|
+
const allSources = listSources();
|
|
29460
|
+
const sourceNameMap2 = new Map(allSources.map((s) => [s.id, s.name]));
|
|
29461
|
+
for (const r of results2) {
|
|
29462
|
+
const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
|
|
29463
|
+
console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
|
|
29464
|
+
if (r.description)
|
|
29465
|
+
console.log(` ${chalk2.dim(r.description)}`);
|
|
29466
|
+
if (r.url)
|
|
29467
|
+
console.log(` ${chalk2.cyan(r.url)}`);
|
|
29468
|
+
}
|
|
29469
|
+
console.log(chalk2.dim(`
|
|
29470
|
+
${results2.length} servers in awesome list.`));
|
|
29471
|
+
closeDb();
|
|
29472
|
+
return;
|
|
29473
|
+
}
|
|
29474
|
+
const q = query || "";
|
|
29475
|
+
const sources = opts.source;
|
|
29476
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
29477
|
+
const noCache = opts.cache === false;
|
|
29478
|
+
if (!q) {
|
|
29479
|
+
console.log(chalk2.dim("Tip: provide a query to search, or use --awesome to browse the curated list."));
|
|
29480
|
+
} else {
|
|
29481
|
+
console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
|
|
29482
|
+
}
|
|
29483
|
+
const t0 = Date.now();
|
|
29484
|
+
const results = await findServers(q, { sources, limit, noCache });
|
|
29485
|
+
const elapsed = Date.now() - t0;
|
|
29486
|
+
if (opts.json) {
|
|
29487
|
+
console.log(JSON.stringify(results, null, 2));
|
|
29488
|
+
closeDb();
|
|
29489
|
+
return;
|
|
29490
|
+
}
|
|
29491
|
+
if (results.length === 0) {
|
|
29492
|
+
console.log(chalk2.dim("No servers found."));
|
|
29493
|
+
closeDb();
|
|
29494
|
+
return;
|
|
29495
|
+
}
|
|
29496
|
+
const countBySource = new Map;
|
|
29497
|
+
for (const r of results) {
|
|
29498
|
+
const key = r.sourceId ?? r.source;
|
|
29499
|
+
countBySource.set(key, (countBySource.get(key) ?? 0) + 1);
|
|
29500
|
+
}
|
|
29501
|
+
const allSourcesList = listSources();
|
|
29502
|
+
const sourceNameMap = new Map(allSourcesList.map((s) => [s.id, s.name]));
|
|
29503
|
+
const sourcesUsed = countBySource.size;
|
|
29504
|
+
const breakdownParts = Array.from(countBySource.entries()).map(([k, n]) => `${sourceNameMap.get(k) ?? k}: ${n}`).join(", ");
|
|
29505
|
+
const sourceColors = {
|
|
29506
|
+
registry: chalk2.blue,
|
|
29507
|
+
npm: chalk2.red,
|
|
29508
|
+
awesome: chalk2.yellow,
|
|
29509
|
+
github: chalk2.magenta
|
|
29510
|
+
};
|
|
29511
|
+
for (let i = 0;i < results.length; i++) {
|
|
29512
|
+
const r = results[i];
|
|
29513
|
+
const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
|
|
29514
|
+
const sourceLabel = (sourceColors[r.source] ?? chalk2.dim)(`[${sourceName}]`);
|
|
29515
|
+
const stars = r.stars ? chalk2.dim(` \u2605${r.stars}`) : "";
|
|
29516
|
+
const idx = opts.install ? chalk2.dim(`${i + 1}. `) : " ";
|
|
29517
|
+
console.log(`${idx}${chalk2.bold(r.name)} ${sourceLabel}${stars}`);
|
|
29518
|
+
if (r.description)
|
|
29519
|
+
console.log(` ${chalk2.dim(r.description)}`);
|
|
29520
|
+
if (r.installCmd)
|
|
29521
|
+
console.log(` ${chalk2.green(`Install: ${r.installCmd}`)}`);
|
|
29522
|
+
else if (r.url)
|
|
29523
|
+
console.log(` ${chalk2.cyan(r.url)}`);
|
|
29524
|
+
}
|
|
29525
|
+
console.log(chalk2.dim(`
|
|
29526
|
+
Found ${results.length} results across ${sourcesUsed} source${sourcesUsed === 1 ? "" : "s"} (${elapsed}ms)`));
|
|
29527
|
+
if (breakdownParts)
|
|
29528
|
+
console.log(chalk2.dim(` Breakdown: ${breakdownParts}`));
|
|
29529
|
+
console.log(chalk2.dim(`Use \`mcps add --from-registry <id>\` or \`mcps add npx -y <pkg>\` to install.`));
|
|
29530
|
+
if (opts.install) {
|
|
29531
|
+
let chosen = results[0];
|
|
29532
|
+
if (results.length === 1 && opts.yes) {} else {
|
|
29533
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29534
|
+
const answer = await new Promise((resolve2) => {
|
|
29535
|
+
rl.question(chalk2.cyan(`
|
|
29536
|
+
Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
|
|
29537
|
+
});
|
|
29538
|
+
rl.close();
|
|
29539
|
+
const num = parseInt(answer, 10);
|
|
29540
|
+
if (!num || num < 1 || num > results.length) {
|
|
29541
|
+
console.log(chalk2.dim("Installation cancelled."));
|
|
29542
|
+
closeDb();
|
|
29543
|
+
return;
|
|
29544
|
+
}
|
|
29545
|
+
chosen = results[num - 1];
|
|
29546
|
+
}
|
|
29547
|
+
if (!chosen.npmPackage && !chosen.installCmd) {
|
|
29548
|
+
console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
|
|
29549
|
+
closeDb();
|
|
29550
|
+
return;
|
|
29551
|
+
}
|
|
29552
|
+
const pkg = chosen.npmPackage ?? chosen.installCmd?.replace(/^npx -y /, "");
|
|
29553
|
+
if (!pkg) {
|
|
29554
|
+
console.log(chalk2.yellow(`No install command available for ${chosen.name}, visit ${chosen.url ?? "(no URL)"}`));
|
|
29555
|
+
closeDb();
|
|
29556
|
+
return;
|
|
29557
|
+
}
|
|
29558
|
+
console.log(chalk2.dim(`Installing ${chosen.name}...`));
|
|
29559
|
+
const server2 = addServer({
|
|
29560
|
+
command: "npx",
|
|
29561
|
+
args: ["-y", pkg],
|
|
29562
|
+
name: chosen.name,
|
|
29563
|
+
description: chosen.description,
|
|
29564
|
+
transport: "stdio"
|
|
29565
|
+
});
|
|
29566
|
+
const results2 = installToAgents(server2, ["claude", "codex", "gemini"]);
|
|
29567
|
+
for (const r of results2) {
|
|
29568
|
+
if (r.success) {
|
|
29569
|
+
console.log(chalk2.green(` \u2713 ${r.agent}`));
|
|
29570
|
+
} else {
|
|
29571
|
+
console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
|
|
29572
|
+
}
|
|
29573
|
+
}
|
|
29574
|
+
console.log(chalk2.green(`
|
|
29575
|
+
Installed ${server2.name} [${server2.id}]`));
|
|
29576
|
+
}
|
|
29577
|
+
} catch (err) {
|
|
29578
|
+
console.error(chalk2.red(`Find failed: ${err.message}`));
|
|
29579
|
+
process.exit(1);
|
|
29580
|
+
} finally {
|
|
29581
|
+
closeDb();
|
|
29582
|
+
}
|
|
29583
|
+
});
|
|
29584
|
+
var sourcesCmd = program2.command("sources").description("Manage search sources");
|
|
29585
|
+
sourcesCmd.command("list").description("List all search sources").action(() => {
|
|
29586
|
+
const sources = listSources();
|
|
29587
|
+
if (sources.length === 0) {
|
|
29588
|
+
console.log(chalk2.dim("No sources configured."));
|
|
29589
|
+
closeDb();
|
|
29590
|
+
return;
|
|
29591
|
+
}
|
|
29592
|
+
for (const s of sources) {
|
|
29593
|
+
const status = s.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
|
|
29594
|
+
console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${chalk2.dim(s.type)} \u2014 ${status}`);
|
|
29595
|
+
if (s.description)
|
|
29596
|
+
console.log(` ${chalk2.dim(s.description)}`);
|
|
29597
|
+
console.log(` ${chalk2.cyan(s.url)}`);
|
|
29598
|
+
}
|
|
29599
|
+
closeDb();
|
|
29600
|
+
});
|
|
29601
|
+
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").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) => {
|
|
29602
|
+
if (!opts.name || !opts.url) {
|
|
29603
|
+
console.error(chalk2.red("Error: --name and --url are required"));
|
|
29604
|
+
closeDb();
|
|
29605
|
+
process.exit(1);
|
|
29606
|
+
}
|
|
29607
|
+
const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
|
|
29608
|
+
let sourceType = opts.type;
|
|
29609
|
+
if (!sourceType) {
|
|
29610
|
+
const detected = detectSourceType(opts.url);
|
|
29611
|
+
if (detected) {
|
|
29612
|
+
console.log(chalk2.dim(`Auto-detected type: ${detected}`));
|
|
29613
|
+
sourceType = detected;
|
|
29614
|
+
} else {
|
|
29615
|
+
console.error(chalk2.red(`Error: could not auto-detect --type. Please specify one of: ${validTypes.join(", ")}`));
|
|
29616
|
+
closeDb();
|
|
29617
|
+
process.exit(1);
|
|
29618
|
+
}
|
|
29619
|
+
}
|
|
29620
|
+
if (!validTypes.includes(sourceType)) {
|
|
29621
|
+
console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
|
|
29622
|
+
closeDb();
|
|
29623
|
+
process.exit(1);
|
|
29624
|
+
}
|
|
29625
|
+
try {
|
|
29626
|
+
const source = addSource({
|
|
29627
|
+
name: opts.name,
|
|
29628
|
+
type: sourceType,
|
|
29629
|
+
url: opts.url,
|
|
29630
|
+
description: opts.description
|
|
29631
|
+
});
|
|
29632
|
+
console.log(chalk2.green(`Added source: ${source.name} [${source.id}]`));
|
|
29633
|
+
if (opts.test) {
|
|
29634
|
+
console.log(chalk2.dim("Testing source..."));
|
|
29635
|
+
const testResults = await searchSource(source, "");
|
|
29636
|
+
console.log(chalk2.dim(` Found ${testResults.length} results`));
|
|
29637
|
+
if (testResults.length === 0)
|
|
29638
|
+
console.log(chalk2.yellow(" Warning: source returned no results"));
|
|
29639
|
+
}
|
|
29640
|
+
} catch (err) {
|
|
29641
|
+
console.error(chalk2.red(`Failed to add source: ${err.message}`));
|
|
29642
|
+
closeDb();
|
|
29643
|
+
process.exit(1);
|
|
29644
|
+
}
|
|
29645
|
+
closeDb();
|
|
29646
|
+
});
|
|
29647
|
+
sourcesCmd.command("remove").argument("<id>", "Source ID to remove").description("Remove a search source").action((id) => {
|
|
29648
|
+
const source = getSource(id);
|
|
29649
|
+
if (!source) {
|
|
29650
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29651
|
+
closeDb();
|
|
29652
|
+
process.exit(1);
|
|
29653
|
+
}
|
|
29654
|
+
removeSource(id);
|
|
29655
|
+
console.log(chalk2.green(`Removed source: ${source.name} [${id}]`));
|
|
29656
|
+
closeDb();
|
|
29657
|
+
});
|
|
29658
|
+
sourcesCmd.command("enable").argument("<id>", "Source ID to enable").description("Enable a search source").action((id) => {
|
|
29659
|
+
const source = getSource(id);
|
|
29660
|
+
if (!source) {
|
|
29661
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29662
|
+
closeDb();
|
|
29663
|
+
process.exit(1);
|
|
29664
|
+
}
|
|
29665
|
+
enableSource(id);
|
|
29666
|
+
console.log(chalk2.green(`Enabled source: ${source.name}`));
|
|
29667
|
+
closeDb();
|
|
29668
|
+
});
|
|
29669
|
+
sourcesCmd.command("disable").argument("<id>", "Source ID to disable").description("Disable a search source").action((id) => {
|
|
29670
|
+
const source = getSource(id);
|
|
29671
|
+
if (!source) {
|
|
29672
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29673
|
+
closeDb();
|
|
29674
|
+
process.exit(1);
|
|
29675
|
+
}
|
|
29676
|
+
disableSource(id);
|
|
29677
|
+
console.log(chalk2.yellow(`Disabled source: ${source.name}`));
|
|
29678
|
+
closeDb();
|
|
29679
|
+
});
|
|
29680
|
+
sourcesCmd.command("refresh").argument("[id]", "Source ID to refresh (omit to refresh all)").description("Clear cached results for a source").action((id) => {
|
|
29681
|
+
clearCache(id);
|
|
29682
|
+
if (id) {
|
|
29683
|
+
console.log(chalk2.green(`Cleared cache for source: ${id}`));
|
|
29684
|
+
} else {
|
|
29685
|
+
console.log(chalk2.green("Cleared all source caches."));
|
|
29686
|
+
}
|
|
29687
|
+
closeDb();
|
|
29688
|
+
});
|
|
29689
|
+
sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Test a source by running a sample search").action(async (id) => {
|
|
29690
|
+
const source = getSource(id);
|
|
29691
|
+
if (!source) {
|
|
29692
|
+
console.error(chalk2.red(`Source "${id}" not found.`));
|
|
29693
|
+
closeDb();
|
|
29694
|
+
process.exit(1);
|
|
29695
|
+
}
|
|
29696
|
+
console.log(chalk2.dim(`Testing source "${source.name}"...`));
|
|
29697
|
+
try {
|
|
29698
|
+
const results = await searchSource(source, "", true);
|
|
29699
|
+
console.log(chalk2.green(`\u2713 Source returned ${results.length} results`));
|
|
29700
|
+
if (results.length > 0) {
|
|
29701
|
+
console.log(chalk2.dim(" Sample results:"));
|
|
29702
|
+
for (const r of results.slice(0, 3)) {
|
|
29703
|
+
console.log(` ${chalk2.bold(r.name)}: ${chalk2.dim(r.description?.slice(0, 60) || "no description")}`);
|
|
29704
|
+
}
|
|
29705
|
+
}
|
|
29706
|
+
} catch (err) {
|
|
29707
|
+
console.error(chalk2.red(`\u2717 Source test failed: ${err.message}`));
|
|
29708
|
+
}
|
|
29709
|
+
closeDb();
|
|
29710
|
+
});
|
|
29711
|
+
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) => {
|
|
29712
|
+
const targets = [];
|
|
29713
|
+
if (opts.to) {
|
|
29714
|
+
for (const t of opts.to) {
|
|
29715
|
+
if (t === "claude" || t === "codex" || t === "gemini")
|
|
29716
|
+
targets.push(t);
|
|
29717
|
+
}
|
|
29718
|
+
}
|
|
29719
|
+
if (opts.claude && !targets.includes("claude"))
|
|
29720
|
+
targets.push("claude");
|
|
29721
|
+
if (opts.codex && !targets.includes("codex"))
|
|
29722
|
+
targets.push("codex");
|
|
29723
|
+
if (opts.gemini && !targets.includes("gemini"))
|
|
29724
|
+
targets.push("gemini");
|
|
29725
|
+
if (opts.all || targets.length === 0) {
|
|
29726
|
+
if (!targets.includes("claude"))
|
|
29727
|
+
targets.push("claude");
|
|
29728
|
+
if (!targets.includes("codex"))
|
|
29729
|
+
targets.push("codex");
|
|
29730
|
+
if (!targets.includes("gemini"))
|
|
29731
|
+
targets.push("gemini");
|
|
29732
|
+
}
|
|
29733
|
+
let server2;
|
|
29734
|
+
if (opts.fromRegistry) {
|
|
29735
|
+
console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
|
|
29736
|
+
try {
|
|
29737
|
+
server2 = await installFromRegistry(opts.fromRegistry);
|
|
29738
|
+
console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
|
|
29739
|
+
} catch (err) {
|
|
29740
|
+
console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
|
|
29741
|
+
closeDb();
|
|
29742
|
+
process.exit(1);
|
|
29743
|
+
}
|
|
29744
|
+
} else if (opts.npm) {
|
|
29745
|
+
const pkg = opts.npm;
|
|
29746
|
+
server2 = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
|
|
29747
|
+
console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
|
|
29748
|
+
} else {
|
|
29749
|
+
if (!id) {
|
|
29750
|
+
console.error(chalk2.red("Error: server ID is required (or use --from-registry or --npm)"));
|
|
29751
|
+
closeDb();
|
|
29752
|
+
process.exit(1);
|
|
29753
|
+
}
|
|
29754
|
+
server2 = getServer(id);
|
|
29755
|
+
if (!server2) {
|
|
29756
|
+
console.error(chalk2.red(`Server "${id}" not found. Use \`mcps add\` first.`));
|
|
29757
|
+
closeDb();
|
|
29758
|
+
process.exit(1);
|
|
29759
|
+
}
|
|
29760
|
+
}
|
|
29761
|
+
console.log(chalk2.dim(`Installing "${server2.name}" to: ${targets.join(", ")}...`));
|
|
29762
|
+
const results = installToAgents(server2, targets);
|
|
29763
|
+
for (const r of results) {
|
|
29764
|
+
if (r.success) {
|
|
29765
|
+
console.log(chalk2.green(` \u2713 ${r.agent}`));
|
|
29766
|
+
} else {
|
|
29767
|
+
console.log(chalk2.red(` \u2717 ${r.agent}: ${r.error}`));
|
|
29768
|
+
}
|
|
29769
|
+
}
|
|
29770
|
+
closeDb();
|
|
29771
|
+
});
|
|
29772
|
+
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) => {
|
|
29773
|
+
const servers = listServers();
|
|
29774
|
+
const sources = listSources();
|
|
29775
|
+
const payload = {
|
|
29776
|
+
version: 1,
|
|
29777
|
+
exported_at: new Date().toISOString(),
|
|
29778
|
+
servers,
|
|
29779
|
+
sources
|
|
29780
|
+
};
|
|
29781
|
+
const json2 = JSON.stringify(payload, null, 2);
|
|
29782
|
+
if (opts.stdout) {
|
|
29783
|
+
console.log(json2);
|
|
29784
|
+
} else {
|
|
29785
|
+
writeFileSync3(opts.file, json2, "utf-8");
|
|
29786
|
+
console.log(chalk2.green(`Exported ${servers.length} server(s) and ${sources.length} source(s) to ${opts.file}`));
|
|
29787
|
+
}
|
|
29788
|
+
closeDb();
|
|
29789
|
+
});
|
|
29790
|
+
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) => {
|
|
29791
|
+
let payload;
|
|
29792
|
+
try {
|
|
29793
|
+
payload = JSON.parse(readFileSync4(file, "utf-8"));
|
|
29794
|
+
} catch (err) {
|
|
29795
|
+
console.error(chalk2.red(`Failed to read file: ${err.message}`));
|
|
29796
|
+
closeDb();
|
|
29797
|
+
process.exit(1);
|
|
29798
|
+
}
|
|
29799
|
+
if (!Array.isArray(payload.servers) || !Array.isArray(payload.sources)) {
|
|
29800
|
+
console.error(chalk2.red("Invalid export file format."));
|
|
29801
|
+
closeDb();
|
|
29802
|
+
process.exit(1);
|
|
29803
|
+
}
|
|
29804
|
+
const db2 = getDb();
|
|
29805
|
+
const overwrite = opts.overwrite;
|
|
29806
|
+
const orReplace = overwrite ? "OR REPLACE" : "OR IGNORE";
|
|
29807
|
+
let serversImported = 0;
|
|
29808
|
+
let serversSkipped = 0;
|
|
29809
|
+
for (const s of payload.servers) {
|
|
29810
|
+
const existing = getServer(s.id);
|
|
29811
|
+
if (existing && !overwrite) {
|
|
29812
|
+
serversSkipped++;
|
|
29813
|
+
continue;
|
|
29814
|
+
}
|
|
29815
|
+
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]);
|
|
29816
|
+
serversImported++;
|
|
29817
|
+
}
|
|
29818
|
+
let sourcesImported = 0;
|
|
29819
|
+
let sourcesSkipped = 0;
|
|
29820
|
+
for (const s of payload.sources) {
|
|
29821
|
+
const existing = getSource(s.id);
|
|
29822
|
+
if (existing && !overwrite) {
|
|
29823
|
+
sourcesSkipped++;
|
|
29824
|
+
continue;
|
|
29825
|
+
}
|
|
29826
|
+
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]);
|
|
29827
|
+
sourcesImported++;
|
|
29828
|
+
}
|
|
29829
|
+
console.log(chalk2.green(`Servers: ${serversImported} imported, ${serversSkipped} skipped.`));
|
|
29830
|
+
console.log(chalk2.green(`Sources: ${sourcesImported} imported, ${sourcesSkipped} skipped.`));
|
|
29831
|
+
closeDb();
|
|
29832
|
+
});
|
|
29833
|
+
var envCmd = program2.command("env").description("Manage server environment variables");
|
|
29834
|
+
envCmd.command("list").argument("<id>").description("List env vars for a server").action((id) => {
|
|
29835
|
+
const server2 = getServer(id);
|
|
29836
|
+
if (!server2) {
|
|
29837
|
+
console.error(chalk2.red(`Server "${id}" not found.`));
|
|
29838
|
+
closeDb();
|
|
29839
|
+
process.exit(1);
|
|
29840
|
+
}
|
|
29841
|
+
const entries = Object.entries(server2.env);
|
|
29842
|
+
if (entries.length === 0) {
|
|
29843
|
+
console.log(chalk2.dim("No env vars set."));
|
|
29844
|
+
closeDb();
|
|
29845
|
+
return;
|
|
29846
|
+
}
|
|
29847
|
+
for (const [k, v] of entries)
|
|
29848
|
+
console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
|
|
29849
|
+
closeDb();
|
|
29850
|
+
});
|
|
29851
|
+
envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
|
|
29852
|
+
const eqIdx = pair.indexOf("=");
|
|
29853
|
+
if (eqIdx === -1) {
|
|
29854
|
+
console.error(chalk2.red("Format: KEY=VALUE"));
|
|
29855
|
+
closeDb();
|
|
29856
|
+
process.exit(1);
|
|
29857
|
+
}
|
|
29858
|
+
const key = pair.slice(0, eqIdx);
|
|
29859
|
+
const value = pair.slice(eqIdx + 1);
|
|
29860
|
+
try {
|
|
29861
|
+
setServerEnv(id, key, value);
|
|
29862
|
+
console.log(chalk2.green(`Set ${key} on ${id}`));
|
|
29863
|
+
} catch (err) {
|
|
29864
|
+
console.error(chalk2.red(err.message));
|
|
29865
|
+
closeDb();
|
|
29866
|
+
process.exit(1);
|
|
29867
|
+
}
|
|
29868
|
+
closeDb();
|
|
29869
|
+
});
|
|
29870
|
+
envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
|
|
29871
|
+
try {
|
|
29872
|
+
unsetServerEnv(id, key);
|
|
29873
|
+
console.log(chalk2.green(`Unset ${key} on ${id}`));
|
|
29874
|
+
} catch (err) {
|
|
29875
|
+
console.error(chalk2.red(err.message));
|
|
29876
|
+
closeDb();
|
|
29877
|
+
process.exit(1);
|
|
29878
|
+
}
|
|
29879
|
+
closeDb();
|
|
27843
29880
|
});
|
|
27844
29881
|
program2.command("mcp").description("Start meta-MCP server (stdio)").action(async () => {
|
|
27845
29882
|
await startMcpServer();
|