// core/mcphub.js — the MCP connection HUB: manages servers (substrate roster · user-added · // registry finds), their lifecycles and tool lists, and exposes the flat enabled-tool set the // generation loop consumes. Server configs persist in the boot index (substrate-local). // // Discovery rings: (1) the substrate's own /.well-known/mcp.json (the OS's 30 verified tools), // (2) servers the user added by URL, (3) the official public MCP registry. One hub, the // whole universe reachable from a tab. import { makeMcpClient, contentToText, discoverSubstrate, searchRegistry, discoverAgents } from "./mcp.js"; export function makeMcpHub({ chatStore, bus }) { const servers = new Map(); // id → { id, name, url, headers, enabled, client, tools, status, error } let loaded = false; const INDEX_KEY = "index:org.hologram.HoloQ"; async function persist() { const idx = await chatStore.getIndex(); idx.mcpServers = [...servers.values()].map((s) => ({ id: s.id, name: s.name, url: s.url, headers: s.headers || {}, enabled: !!s.enabled, source: s.source || "user" })); const b = new TextEncoder().encode(JSON.stringify(idx)); await (chatStore.store.backend.putRaw ? chatStore.store.backend.putRaw(INDEX_KEY, b) : chatStore.store.backend.put(INDEX_KEY, b)); } async function restore() { if (loaded) return; loaded = true; const idx = await chatStore.getIndex(); for (const s of idx.mcpServers || []) if (!servers.has(s.id)) servers.set(s.id, { ...s, client: null, tools: [], status: "saved" }); // never clobber a live entry bus.emit("mcp-changed"); } const norm = (url) => { try { return new URL(url, location.origin).href; } catch { return url; } }; const idOf = (url) => "mcp-" + norm(url).replace(/[^\w]+/g, "-").slice(0, 60); async function add({ name, url, headers = {}, source = "user", enabled = true }) { const u = norm(url); const id = idOf(u); if (!servers.has(id)) servers.set(id, { id, name: name || u, url: u, headers, source, enabled, client: null, tools: [], status: "saved" }); const s = servers.get(id); s.enabled = enabled; // adding is an explicit intent — (re)enable even if a saved entry existed await connect(s.id).catch(() => {}); await persist(); bus.emit("mcp-changed"); return s; } async function connect(id) { const s = servers.get(id); if (!s) return null; s.status = "connecting"; s.error = null; bus.emit("mcp-changed"); try { s.client = makeMcpClient({ url: s.url, headers: s.headers, name: s.name }); await s.client.initialize(); s.tools = await s.client.listTools(); s.name = s.client.name() || s.name; s.status = "connected"; } catch (e) { s.client = null; s.tools = []; s.status = "error"; s.error = String(e && e.message || e); } bus.emit("mcp-changed"); return s; } async function remove(id) { servers.delete(id); await persist(); bus.emit("mcp-changed"); } async function setEnabled(id, on) { const s = servers.get(id); if (s) { s.enabled = !!on; if (on && !s.client) await connect(id); await persist(); bus.emit("mcp-changed"); } } // The flat ARMED tool set the loop consumes: [{ def, serverName, call(args)→{text,isError} }]. // Per-server `toolFilter` (a name allowlist, set from the UI) selects which tools arm; without // one, the first DEFAULT_ARM tools arm. A hard ceiling keeps the system block inside the model's // context (a local model can't juggle 30 schemas — pick, like LibreChat's per-server selection). const DEFAULT_ARM = 4, MAX_ARM = 8; // the substrate's own roster is curated to its core verbs by default const SUBSTRATE_DEFAULT = ["search_web", "resolve_object", "answer", "verify_object"]; function enabledTools() { const out = []; for (const s of servers.values()) { if (!s.enabled || s.status !== "connected") continue; const names = s.toolFilter && s.toolFilter.length ? s.toolFilter : (s.source === "substrate" ? SUBSTRATE_DEFAULT.filter((n) => s.tools.some((t) => t.name === n)) : s.tools.slice(0, DEFAULT_ARM).map((t) => t.name)); for (const t of s.tools) { if (!names.includes(t.name)) continue; out.push({ def: { name: t.name, description: t.description, inputSchema: t.inputSchema }, serverName: s.name, serverId: s.id, call: async (args) => { const r = await s.client.callTool(t.name, args); return { text: contentToText(r), isError: !!(r && r.isError) }; }, }); } } // de-dup by tool name (first server wins) — small models address tools by bare name const seen = new Set(); return out.filter((t) => (seen.has(t.def.name) ? false : (seen.add(t.def.name), true))).slice(0, MAX_ARM); } async function setToolFilter(id, names) { const s = servers.get(id); if (s) { s.toolFilter = names && names.length ? names : null; bus.emit("mcp-changed"); } } // Substrate ring: read the OS roster; candidate endpoints = declared, same-origin /mcp, the // conventional localhost MCP port. First reachable wins. async function discoverSubstrateServers() { const entries = await discoverSubstrate(); const candidates = new Set(); for (const e of entries) if (e.url) candidates.add(norm(e.url)); candidates.add(norm("/mcp")); candidates.add("http://127.0.0.1:8787/mcp"); return { entries, candidates: [...candidates] }; } return { restore, add, connect, remove, setEnabled, setToolFilter, enabledTools, persist, list: () => [...servers.values()], discoverSubstrateServers, searchRegistry, discoverAgents, }; }