claude-bridge-cli 2.0.24 → 2.0.25
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/lib/bridge.js +44 -0
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -64,6 +64,8 @@ function dataDir() {
|
|
|
64
64
|
return d;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
let mcpHealth = { at: 0, data: {} }; // cached `claude mcp list` health
|
|
68
|
+
|
|
67
69
|
// Where always-on personal skills live for THIS machine (a plugin dir, since
|
|
68
70
|
// ~/.claude/skills is not discovered by the CLI). Shared by the turn builder
|
|
69
71
|
// (--plugin-dir) and the /skills endpoint so they can never disagree.
|
|
@@ -1212,6 +1214,48 @@ function startBridge(config) {
|
|
|
1212
1214
|
// machine is active. Without it the request 404s, the host list comes back
|
|
1213
1215
|
// empty, and the menu silently shows ONLY the CLI built-ins — which reads
|
|
1214
1216
|
// as "this machine has no skills". Mirrors the Python bridge's contract.
|
|
1217
|
+
// ── /mcp — configured MCP servers for THIS machine ───────────────────────
|
|
1218
|
+
// Config read is instant; ?health=1 shells out to `claude mcp list`, which
|
|
1219
|
+
// actually connects to each server (seconds), so it's cached — but it's the
|
|
1220
|
+
// only way to distinguish Connected from needs-authentication.
|
|
1221
|
+
if (url.pathname === "/mcp") {
|
|
1222
|
+
const servers = {};
|
|
1223
|
+
const bases = new Set([process.env.CLAUDE_CONFIG_DIR || homeDir(), homeDir()]);
|
|
1224
|
+
for (const base of bases) {
|
|
1225
|
+
let cfg; try { cfg = JSON.parse(fs.readFileSync(path.join(base, ".claude.json"), "utf8")); }
|
|
1226
|
+
catch { continue; }
|
|
1227
|
+
for (const [n, spec] of Object.entries(cfg.mcpServers || {}))
|
|
1228
|
+
if (!(n in servers)) servers[n] = spec || {};
|
|
1229
|
+
for (const proj of Object.values(cfg.projects || {}))
|
|
1230
|
+
for (const [n, spec] of Object.entries((proj && proj.mcpServers) || {}))
|
|
1231
|
+
if (!(n in servers)) servers[n] = spec || {};
|
|
1232
|
+
}
|
|
1233
|
+
const out = Object.keys(servers).sort().map((n) => ({
|
|
1234
|
+
name: n,
|
|
1235
|
+
transport: servers[n].type || (servers[n].url ? "http" : "stdio"),
|
|
1236
|
+
target: servers[n].url || servers[n].command || "",
|
|
1237
|
+
}));
|
|
1238
|
+
const want = url.searchParams.get("health");
|
|
1239
|
+
if (out.length && want && want !== "0" && want !== "false") {
|
|
1240
|
+
const now = Date.now();
|
|
1241
|
+
if (now - mcpHealth.at > 120000) {
|
|
1242
|
+
const states = {};
|
|
1243
|
+
try {
|
|
1244
|
+
const p = execSync(`${JSON.stringify(config.claudeBin)} mcp list`,
|
|
1245
|
+
{ encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1246
|
+
for (const line of String(p).split("\n")) {
|
|
1247
|
+
const m = /^\s*([A-Za-z0-9._-]+):\s*(.*?)\s*-\s*(.+?)\s*$/.exec(line);
|
|
1248
|
+
if (m) states[m[1]] = m[3].replace(/[✔⚠○⏸]/g, "").trim();
|
|
1249
|
+
}
|
|
1250
|
+
} catch {}
|
|
1251
|
+
mcpHealth = { at: now, data: states };
|
|
1252
|
+
}
|
|
1253
|
+
for (const s of out) s.state = mcpHealth.data[s.name];
|
|
1254
|
+
}
|
|
1255
|
+
send(200, { servers: out });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1215
1259
|
if (url.pathname === "/skills" || url.pathname.startsWith("/skills/")) {
|
|
1216
1260
|
const skillsRoot = path.join(globalSkillsDir(), "skills");
|
|
1217
1261
|
const nameOf = decodeURIComponent(url.pathname.slice("/skills/".length) || "");
|
package/package.json
CHANGED