@hasna/mcps 0.0.3 → 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 +519 -20
- package/bin/mcp.js +76 -3
- 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 +1 -1
- package/dist/index.js +40 -3
- package/dist/lib/doctor.d.ts +2 -0
- package/dist/lib/registry.d.ts +1 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/index-BHsa5YXH.js +0 -229
- package/dashboard/dist/assets/index-C7n__Rq8.css +0 -1
package/bin/index.js
CHANGED
|
@@ -9183,7 +9183,7 @@ var init_sources = __esm(() => {
|
|
|
9183
9183
|
var require_package = __commonJS((exports, module) => {
|
|
9184
9184
|
module.exports = {
|
|
9185
9185
|
name: "@hasna/mcps",
|
|
9186
|
-
version: "0.0.
|
|
9186
|
+
version: "0.0.4",
|
|
9187
9187
|
description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
|
|
9188
9188
|
type: "module",
|
|
9189
9189
|
main: "dist/index.js",
|
|
@@ -11095,6 +11095,21 @@ function getToolCounts() {
|
|
|
11095
11095
|
const rows = db2.prepare("SELECT server_id, COUNT(*) as count FROM tool_cache GROUP BY server_id").all();
|
|
11096
11096
|
return new Map(rows.map((row) => [row.server_id, Number(row.count)]));
|
|
11097
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
|
+
});
|
|
11112
|
+
}
|
|
11098
11113
|
function getCachedTools(serverId) {
|
|
11099
11114
|
const db2 = getDb();
|
|
11100
11115
|
const rows = db2.prepare("SELECT name, description, input_schema FROM tool_cache WHERE server_id = ? ORDER BY name").all(serverId);
|
|
@@ -25521,10 +25536,21 @@ async function diagnoseServer(server) {
|
|
|
25521
25536
|
const checks4 = [];
|
|
25522
25537
|
if (server.transport === "stdio") {
|
|
25523
25538
|
try {
|
|
25524
|
-
execFileSync("which", [server.command], { stdio: "pipe" });
|
|
25525
|
-
|
|
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})` : ""}` });
|
|
25526
25546
|
} catch {
|
|
25527
|
-
checks4.push({
|
|
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
|
+
});
|
|
25528
25554
|
}
|
|
25529
25555
|
}
|
|
25530
25556
|
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
@@ -27047,6 +27073,59 @@ server.tool("disable_server", "Disable a registered MCP server", { id: exports_e
|
|
|
27047
27073
|
content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
|
|
27048
27074
|
};
|
|
27049
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
|
+
});
|
|
27050
27129
|
server.tool("get_server_info", "Get detailed information about a registered MCP server", { id: exports_external.string().describe("Server ID") }, async ({ id }) => {
|
|
27051
27130
|
const entry = getServer(id);
|
|
27052
27131
|
if (!entry) {
|
|
@@ -27221,6 +27300,7 @@ if (isDirectRun) {
|
|
|
27221
27300
|
import { existsSync as existsSync3 } from "fs";
|
|
27222
27301
|
import { join as join5, dirname as dirname3, extname, resolve, relative, sep } from "path";
|
|
27223
27302
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
27303
|
+
init_sources();
|
|
27224
27304
|
init_db();
|
|
27225
27305
|
function redactServer(server2) {
|
|
27226
27306
|
return { ...server2, env: {} };
|
|
@@ -27486,6 +27566,218 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
27486
27566
|
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
27487
27567
|
}
|
|
27488
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
|
+
}
|
|
27489
27781
|
if (path === "/api/update" && method === "POST") {
|
|
27490
27782
|
if (!isLoopbackHost(host)) {
|
|
27491
27783
|
return json({ error: "Update only allowed on loopback host" }, 403, port);
|
|
@@ -27521,7 +27813,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
27521
27813
|
return new Response(null, {
|
|
27522
27814
|
headers: {
|
|
27523
27815
|
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
27524
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
27816
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
27525
27817
|
"Access-Control-Allow-Headers": "Content-Type"
|
|
27526
27818
|
}
|
|
27527
27819
|
});
|
|
@@ -28643,7 +28935,7 @@ var VERSION2 = (() => {
|
|
|
28643
28935
|
})();
|
|
28644
28936
|
var program2 = new Command;
|
|
28645
28937
|
program2.name("mcps").description("Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers").version(VERSION2).enablePositionalOptions();
|
|
28646
|
-
program2.command("list").description("List registered MCP servers").option("--json", "Output as JSON").action((opts) => {
|
|
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) => {
|
|
28647
28939
|
const servers = listServers();
|
|
28648
28940
|
if (opts.json) {
|
|
28649
28941
|
const toolCounts2 = getToolCounts();
|
|
@@ -28665,7 +28957,29 @@ program2.command("list").description("List registered MCP servers").option("--js
|
|
|
28665
28957
|
console.log(` ${chalk2.bold(s.name)} ${chalk2.dim(`[${s.id}]`)} \u2014 ${status}${toolCount}${errorWarning}`);
|
|
28666
28958
|
if (s.description)
|
|
28667
28959
|
console.log(` ${chalk2.dim(s.description)}`);
|
|
28668
|
-
|
|
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
|
+
}
|
|
28669
28983
|
}
|
|
28670
28984
|
closeDb();
|
|
28671
28985
|
});
|
|
@@ -28694,7 +29008,21 @@ ${results.length} result(s). Use \`mcps add --from-registry <id>\` to install.`)
|
|
|
28694
29008
|
closeDb();
|
|
28695
29009
|
}
|
|
28696
29010
|
});
|
|
28697
|
-
|
|
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) => {
|
|
28698
29026
|
try {
|
|
28699
29027
|
if (opts.fromRegistry) {
|
|
28700
29028
|
console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
|
|
@@ -28704,11 +29032,78 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
28704
29032
|
closeDb();
|
|
28705
29033
|
return;
|
|
28706
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
|
+
}
|
|
28707
29092
|
if (!command) {
|
|
28708
|
-
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)"));
|
|
28709
29094
|
closeDb();
|
|
28710
29095
|
process.exit(1);
|
|
28711
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
|
+
}
|
|
28712
29107
|
const envMap = {};
|
|
28713
29108
|
if (opts.env) {
|
|
28714
29109
|
for (const pair of opts.env) {
|
|
@@ -28740,6 +29135,42 @@ program2.command("add").passThroughOptions().argument("[command]", "Command to r
|
|
|
28740
29135
|
}
|
|
28741
29136
|
closeDb();
|
|
28742
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();
|
|
29170
|
+
process.exit(1);
|
|
29171
|
+
}
|
|
29172
|
+
closeDb();
|
|
29173
|
+
});
|
|
28743
29174
|
program2.command("remove").argument("<id>", "Server ID to remove").description("Remove a registered server").action((id) => {
|
|
28744
29175
|
const server2 = getServer(id);
|
|
28745
29176
|
if (!server2) {
|
|
@@ -28917,7 +29348,8 @@ program2.command("status").description("Show registry stats").option("--json", "
|
|
|
28917
29348
|
console.log(` Tools: ${totalTools} (cached)`);
|
|
28918
29349
|
closeDb();
|
|
28919
29350
|
});
|
|
28920
|
-
program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").action(async (id) => {
|
|
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");
|
|
28921
29353
|
const servers = id ? [getServer(id)].filter(Boolean) : listServers();
|
|
28922
29354
|
if (servers.length === 0) {
|
|
28923
29355
|
console.log(chalk2.dim(id ? `Server "${id}" not found.` : "No servers registered."));
|
|
@@ -28932,6 +29364,15 @@ ${server2.name} [${server2.id}]`));
|
|
|
28932
29364
|
for (const check2 of report.checks) {
|
|
28933
29365
|
const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
|
|
28934
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
|
+
}
|
|
28935
29376
|
}
|
|
28936
29377
|
if (!report.healthy)
|
|
28937
29378
|
allHealthy = false;
|
|
@@ -28944,6 +29385,40 @@ ${server2.name} [${server2.id}]`));
|
|
|
28944
29385
|
}
|
|
28945
29386
|
closeDb();
|
|
28946
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
|
+
});
|
|
28947
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) => {
|
|
28948
29423
|
await startServer(parseInt(opts.port, 10), { open: opts.open, host: opts.host });
|
|
28949
29424
|
});
|
|
@@ -28981,8 +29456,8 @@ program2.command("find").argument("[query]", "Search query (omit to list all fro
|
|
|
28981
29456
|
closeDb();
|
|
28982
29457
|
return;
|
|
28983
29458
|
}
|
|
28984
|
-
const
|
|
28985
|
-
const sourceNameMap2 = new Map(
|
|
29459
|
+
const allSources = listSources();
|
|
29460
|
+
const sourceNameMap2 = new Map(allSources.map((s) => [s.id, s.name]));
|
|
28986
29461
|
for (const r of results2) {
|
|
28987
29462
|
const sourceName = r.sourceId ? sourceNameMap2.get(r.sourceId) ?? r.source : r.source;
|
|
28988
29463
|
console.log(` ${chalk2.bold(r.name)} ${chalk2.yellow(`[${sourceName}]`)}`);
|
|
@@ -29005,7 +29480,9 @@ ${results2.length} servers in awesome list.`));
|
|
|
29005
29480
|
} else {
|
|
29006
29481
|
console.log(chalk2.dim(`Searching for "${q}" across ${sources ? sources.join(", ") : "all enabled sources"}...`));
|
|
29007
29482
|
}
|
|
29483
|
+
const t0 = Date.now();
|
|
29008
29484
|
const results = await findServers(q, { sources, limit, noCache });
|
|
29485
|
+
const elapsed = Date.now() - t0;
|
|
29009
29486
|
if (opts.json) {
|
|
29010
29487
|
console.log(JSON.stringify(results, null, 2));
|
|
29011
29488
|
closeDb();
|
|
@@ -29016,14 +29493,21 @@ ${results2.length} servers in awesome list.`));
|
|
|
29016
29493
|
closeDb();
|
|
29017
29494
|
return;
|
|
29018
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(", ");
|
|
29019
29505
|
const sourceColors = {
|
|
29020
29506
|
registry: chalk2.blue,
|
|
29021
29507
|
npm: chalk2.red,
|
|
29022
29508
|
awesome: chalk2.yellow,
|
|
29023
29509
|
github: chalk2.magenta
|
|
29024
29510
|
};
|
|
29025
|
-
const allSources = listSources();
|
|
29026
|
-
const sourceNameMap = new Map(allSources.map((s) => [s.id, s.name]));
|
|
29027
29511
|
for (let i = 0;i < results.length; i++) {
|
|
29028
29512
|
const r = results[i];
|
|
29029
29513
|
const sourceName = r.sourceId ? sourceNameMap.get(r.sourceId) ?? r.source : r.source;
|
|
@@ -29039,7 +29523,10 @@ ${results2.length} servers in awesome list.`));
|
|
|
29039
29523
|
console.log(` ${chalk2.cyan(r.url)}`);
|
|
29040
29524
|
}
|
|
29041
29525
|
console.log(chalk2.dim(`
|
|
29042
|
-
${results.length}
|
|
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.`));
|
|
29043
29530
|
if (opts.install) {
|
|
29044
29531
|
let chosen = results[0];
|
|
29045
29532
|
if (results.length === 1 && opts.yes) {} else {
|
|
@@ -29111,14 +29598,26 @@ sourcesCmd.command("list").description("List all search sources").action(() => {
|
|
|
29111
29598
|
}
|
|
29112
29599
|
closeDb();
|
|
29113
29600
|
});
|
|
29114
|
-
sourcesCmd.command("add").description("Add a new search source").option("--name <name>", "Source name (required)").option("--type <type>", "Source type: mcp-registry, awesome-list, npm-search, github-topic
|
|
29115
|
-
if (!opts.name || !opts.
|
|
29116
|
-
console.error(chalk2.red("Error: --name
|
|
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"));
|
|
29117
29604
|
closeDb();
|
|
29118
29605
|
process.exit(1);
|
|
29119
29606
|
}
|
|
29120
29607
|
const validTypes = ["mcp-registry", "awesome-list", "npm-search", "github-topic"];
|
|
29121
|
-
|
|
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)) {
|
|
29122
29621
|
console.error(chalk2.red(`Error: --type must be one of: ${validTypes.join(", ")}`));
|
|
29123
29622
|
closeDb();
|
|
29124
29623
|
process.exit(1);
|
|
@@ -29126,7 +29625,7 @@ sourcesCmd.command("add").description("Add a new search source").option("--name
|
|
|
29126
29625
|
try {
|
|
29127
29626
|
const source = addSource({
|
|
29128
29627
|
name: opts.name,
|
|
29129
|
-
type:
|
|
29628
|
+
type: sourceType,
|
|
29130
29629
|
url: opts.url,
|
|
29131
29630
|
description: opts.description
|
|
29132
29631
|
});
|