Skip to main content
Glama
manager-cli.ts26.4 kB
import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import { existsSync } from "node:fs"; import { chmodSync, copyFileSync, mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { z } from "zod"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { readRegistry, writeRegistry, type Upstream } from "./registry/registry.js"; type Cmd = | "doctor" | "install" | "status" | "help" | "build-gateway" | "centralize" | "decentralize"; const argsSchema = z.object({ cmd: z.enum(["doctor", "install", "status", "help", "build-gateway", "centralize", "decentralize"]), fix: z.boolean().default(false), apply: z.boolean().default(false), verbose: z.boolean().default(false), }); function parseArgs(argv: string[]): z.infer<typeof argsSchema> { const cmd = (argv[2] as Cmd | undefined) ?? "help"; const fix = argv.includes("--fix"); const apply = argv.includes("--apply"); const verbose = argv.includes("--verbose") || argv.includes("-v"); return argsSchema.parse({ cmd, fix, apply, verbose }); } function homeDir(): string { return process.env.HOME ?? os.homedir(); } function gatewayInstallPath(): string { return path.join(homeDir(), ".mcpmanager", "bin", "mcpmanager-gateway"); } function gatewayBuiltPath(): string { const here = path.dirname(fileURLToPath(import.meta.url)); return path.join(here, "..", "dist", "mcpmanager-gateway"); } function codexConfigPath(): string { return path.join(homeDir(), ".codex", "config.toml"); } function claudeDesktopConfigPath(): string { // macOS only for now. return path.join( homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json", ); } function backupPath(original: string): string { return `${original}.bak.${Math.floor(Date.now() / 1000)}`; } async function writeFileWithBackup(filePath: string, contents: string) { await fs.mkdir(path.dirname(filePath), { recursive: true }); if (existsSync(filePath)) { await fs.copyFile(filePath, backupPath(filePath)); } await fs.writeFile(filePath, contents, "utf8"); } async function run(cmd: string, cmdArgs: string[], cwd?: string) { const proc = Bun.spawn([cmd, ...cmdArgs], { cwd, stdout: "pipe", stderr: "pipe", env: process.env, }); const [stdout, stderr, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); return { stdout, stderr, exitCode }; } async function buildGateway(verbose: boolean) { const here = path.dirname(fileURLToPath(import.meta.url)); const gatewayDir = path.join(here, ".."); const { stdout, stderr, exitCode } = await run("bun", ["run", "build:exe"], gatewayDir); if (verbose) { process.stdout.write(stdout); process.stderr.write(stderr); } if (exitCode !== 0) throw new Error("Failed to build gateway (bun run build:exe)."); if (!existsSync(gatewayBuiltPath())) throw new Error("Gateway binary missing after build."); } async function installGatewayBinary() { const src = gatewayBuiltPath(); if (!existsSync(src)) { throw new Error(`Gateway binary not found at ${src}. Run: bun run build:exe`); } const dest = gatewayInstallPath(); mkdirSync(path.dirname(dest), { recursive: true }); copyFileSync(src, dest); try { chmodSync(dest, 0o755); } catch { // ignore on non-unix } return dest; } function tomlString(value: string): string { // Minimal TOML string escaping for our use-case. return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`; } function renderCodexMcpManagerSection(gatewayPath: string) { const env: Record<string, string> = {}; for (const key of [ "DAYTONA_API_KEY", "DAYTONA_API_URL", "DAYTONA_SERVER_URL", "DAYTONA_TARGET", "TAILSCALE_API_KEY", "TAILSCALE_TAILNET", "MCPMANAGER_REGISTRY_PATH", "MCPMANAGER_POLICY_MODE", "MCPMANAGER_TAILSCALE_ALLOW_KEYS", "MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS", "MCPMANAGER_TAILSCALE_ALLOW_REUSABLE", "MCPMANAGER_TAILSCALE_TAILNET_LOCK", "MCPMANAGER_TAILSCALE_TAGS_ALLOW", ] as const) { const v = process.env[key]; if (v && v.length > 0) env[key] = v; } const envInline = Object.keys(env).length > 0 ? `env = { ${Object.entries(env) .map(([k, v]) => `${k} = ${tomlString(v)}`) .join(", ")} }` : null; const envVars = [ "DAYTONA_API_KEY", "DAYTONA_API_URL", "DAYTONA_SERVER_URL", "DAYTONA_TARGET", "TAILSCALE_API_KEY", "TAILSCALE_TAILNET", "MCPMANAGER_REGISTRY_PATH", "MCPMANAGER_POLICY_MODE", "MCPMANAGER_TAILSCALE_ALLOW_KEYS", "MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS", "MCPMANAGER_TAILSCALE_ALLOW_REUSABLE", "MCPMANAGER_TAILSCALE_TAILNET_LOCK", "MCPMANAGER_TAILSCALE_TAGS_ALLOW", ]; return ( [ "[mcp_servers.mcpmanager]", `command = ${tomlString(gatewayPath)}`, "args = []", "enabled = true", envInline, "env_vars = [", ...envVars.map((v) => ` ${tomlString(v)},`), "]", "", ] .filter(Boolean) .join("\n") + "\n" ); } function upsertTomlTable(text: string, tableHeader: string, replacement: string): string { const headerRe = new RegExp(`^\\[${tableHeader.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\]\\s*$`, "m"); const match = headerRe.exec(text); if (!match) { const prefix = text.trimEnd().length === 0 ? "" : "\n\n"; return text.replace(/\s*$/, "") + prefix + replacement; } const start = match.index; const afterHeader = start + match[0].length; const nextHeaderRe = /^\[.+\]\s*$/gm; nextHeaderRe.lastIndex = afterHeader; const next = nextHeaderRe.exec(text); const end = next ? next.index : text.length; const before = text.slice(0, start).replace(/\s*$/, ""); const after = text.slice(end).replace(/^\s*/, ""); const joinerBefore = before.length === 0 ? "" : "\n\n"; const joinerAfter = after.length === 0 ? "" : "\n\n"; return before + joinerBefore + replacement.replace(/\s*$/, "\n") + joinerAfter + after; } async function installCodexConfig(gatewayPath: string) { const filePath = codexConfigPath(); const current = existsSync(filePath) ? await fs.readFile(filePath, "utf8") : ""; const section = renderCodexMcpManagerSection(gatewayPath); const next = upsertTomlTable(current, "mcp_servers.mcpmanager", section); await writeFileWithBackup(filePath, next); return filePath; } function removeTomlTable(text: string, tableHeader: string): string { const headerRe = new RegExp( `^\\[${tableHeader.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\]\\s*$`, "m", ); const match = headerRe.exec(text); if (!match) return text; const start = match.index; const afterHeader = start + match[0].length; const nextHeaderRe = /^\[.+\]\s*$/gm; nextHeaderRe.lastIndex = afterHeader; const next = nextHeaderRe.exec(text); const end = next ? next.index : text.length; const before = text.slice(0, start).replace(/\s*$/, ""); const after = text.slice(end).replace(/^\s*/, ""); if (!before && !after) return ""; if (!before) return after; if (!after) return before + "\n"; return before + "\n\n" + after; } function setTomlEnabledInTable(text: string, tableHeader: string, enabled: boolean): string { const headerRe = new RegExp(`^\\[${tableHeader.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\]\\s*$`, "m"); const match = headerRe.exec(text); if (!match) return text; const start = match.index; const afterHeader = start + match[0].length; const nextHeaderRe = /^\[.+\]\s*$/gm; nextHeaderRe.lastIndex = afterHeader; const next = nextHeaderRe.exec(text); const end = next ? next.index : text.length; const block = text.slice(start, end); const lines = block.split("\n"); const enabledLine = `enabled = ${enabled ? "true" : "false"}`; let found = false; for (let i = 0; i < lines.length; i++) { if (/^\s*enabled\s*=/.test(lines[i] ?? "")) { lines[i] = enabledLine; found = true; break; } } if (!found) { lines.splice(1, 0, enabledLine); } const nextBlock = lines.join("\n"); return text.slice(0, start) + nextBlock + text.slice(end); } function normalizeStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.filter((v) => typeof v === "string"); } function normalizeStringMap(value: unknown): Record<string, string> { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const out: Record<string, string> = {}; for (const [k, v] of Object.entries(value as Record<string, unknown>)) { if (typeof v === "string") out[k] = v; } return out; } function splitCommandLine(commandLine: string): string[] { const out: string[] = []; let cur = ""; let quote: "'" | '"' | null = null; for (let i = 0; i < commandLine.length; i++) { const ch = commandLine[i] ?? ""; if (quote) { if (ch === quote) { quote = null; continue; } if (ch === "\\" && quote === '"') { const next = commandLine[i + 1]; if (next) { cur += next; i++; continue; } } cur += ch; continue; } if (ch === "'" || ch === '"') { quote = ch as any; continue; } if (/\s/.test(ch)) { if (cur.length > 0) { out.push(cur); cur = ""; } continue; } if (ch === "\\") { const next = commandLine[i + 1]; if (next) { cur += next; i++; continue; } } cur += ch; } if (cur.length > 0) out.push(cur); return out; } async function importUpstreamsFromCodex(): Promise<Upstream[]> { const p = codexConfigPath(); if (!existsSync(p)) return []; const text = await fs.readFile(p, "utf8"); let doc: any = {}; try { doc = Bun.TOML.parse(text) as any; } catch { return []; } const servers: any = doc?.mcp_servers ?? {}; if (!servers || typeof servers !== "object") return []; const upstreams: Upstream[] = []; for (const [id, cfg] of Object.entries(servers)) { if (id === "mcpmanager") continue; if (!cfg || typeof cfg !== "object") continue; const c = cfg as any; if (typeof c.command !== "string") continue; upstreams.push({ id, enabled: c.enabled !== false, command: c.command, args: normalizeStringArray(c.args), env: normalizeStringMap(c.env), env_vars: normalizeStringArray(c.env_vars), }); } return upstreams; } async function importUpstreamsFromClaudeDesktop(): Promise<Upstream[]> { const p = claudeDesktopConfigPath(); if (!existsSync(p)) return []; let doc: any = {}; try { doc = JSON.parse(await fs.readFile(p, "utf8")); } catch { return []; } const servers: any = doc?.mcpServers ?? {}; if (!servers || typeof servers !== "object") return []; const upstreams: Upstream[] = []; for (const [id, cfg] of Object.entries(servers)) { if (id === "mcpmanager") continue; if (!cfg || typeof cfg !== "object") continue; const c = cfg as any; if (typeof c.command !== "string") continue; const env = normalizeStringMap(c.env); const env_vars: string[] = []; const explicitEnv: Record<string, string> = {}; for (const [k, v] of Object.entries(env)) { if (v === `\${${k}}`) env_vars.push(k); else explicitEnv[k] = v; } upstreams.push({ id, enabled: true, command: c.command, args: normalizeStringArray(c.args), env: explicitEnv, env_vars, }); } return upstreams; } async function importUpstreamsFromClaudeCodeList(): Promise<Upstream[]> { const { exitCode } = await run("claude", ["--version"]); if (exitCode !== 0) return []; const list = await run("claude", ["mcp", "list"]); const upstreams: Upstream[] = []; for (const line of (list.stdout ?? "").split("\n")) { const trimmed = line.trim(); if (!trimmed) continue; // Example: exa: npx -y exa-mcp-server - ✓ Connected const m = /^([A-Za-z0-9._-]+):\s+(.+?)\s+-\s+/.exec(trimmed); if (!m) continue; const id = m[1]!; const commandLine = m[2]!; if (id === "mcpmanager") continue; const parts = splitCommandLine(commandLine); if (parts.length === 0) continue; upstreams.push({ id, enabled: true, command: parts[0]!, args: parts.slice(1), env: {}, env_vars: [], }); } return upstreams; } async function centralize({ apply, verbose }: { apply: boolean; verbose: boolean }) { const fromCodex = await importUpstreamsFromCodex(); const fromDesktop = await importUpstreamsFromClaudeDesktop(); const fromClaudeCode = await importUpstreamsFromClaudeCodeList(); // Merge by id, preferring Codex definitions. const byId = new Map<string, Upstream>(); for (const u of fromClaudeCode) byId.set(u.id, u); for (const u of fromDesktop) byId.set(u.id, u); for (const u of fromCodex) byId.set(u.id, u); const reg = await readRegistry(); const existing = new Map(reg.upstreams.map((u) => [u.id, u] as const)); for (const [id, u] of byId.entries()) { existing.set(id, u); } const next = { version: 1 as const, upstreams: Array.from(existing.values()).sort((a, b) => a.id.localeCompare(b.id)) }; await writeRegistry(next); const ids = Array.from(byId.keys()).sort(); let codexUpdated: string | null = null; let desktopUpdated: string | null = null; let claudeRemoved: Array<{ name: string; ok: boolean; reason?: string }> = []; if (apply) { // Codex: disable direct MCP servers now routed via mcpmanager. const codexPath = codexConfigPath(); if (existsSync(codexPath)) { let text = await fs.readFile(codexPath, "utf8"); for (const id of ids) { text = setTomlEnabledInTable(text, `mcp_servers.${id}`, false); } await writeFileWithBackup(codexPath, text); codexUpdated = codexPath; } // Claude Desktop: remove other servers, stash under mcpServersDisabled. const desktopPath = claudeDesktopConfigPath(); if (existsSync(desktopPath)) { const doc = JSON.parse(await fs.readFile(desktopPath, "utf8")); if (!doc.mcpServers) doc.mcpServers = {}; if (!doc.mcpServersDisabled) doc.mcpServersDisabled = {}; for (const id of ids) { if (doc.mcpServers?.[id]) { doc.mcpServersDisabled[id] = doc.mcpServers[id]; delete doc.mcpServers[id]; } } await writeFileWithBackup(desktopPath, JSON.stringify(doc, null, 2) + "\n"); desktopUpdated = desktopPath; } // Claude Code: remove user-scoped direct servers (if present), since they're now routed. const { exitCode } = await run("claude", ["--version"]); if (exitCode === 0) { const list = await run("claude", ["mcp", "list"]); const names = (list.stdout ?? "") .split("\n") .map((l) => l.trim()) .map((l) => /^([A-Za-z0-9._-]+):\s+/.exec(l)?.[1]) .filter((v): v is string => Boolean(v)) .filter((n) => n !== "mcpmanager"); for (const name of names) { // Only remove if we imported it (or it conflicts by id). if (!ids.includes(name)) continue; const res = await run("claude", ["mcp", "remove", "--scope", "user", name]); claudeRemoved.push({ name, ok: res.exitCode === 0, reason: res.exitCode === 0 ? undefined : (res.stderr || res.stdout).trim(), }); if (verbose && res.exitCode !== 0) { process.stderr.write(res.stderr); } } } } return { imported: ids, registryPath: process.env.MCPMANAGER_REGISTRY_PATH ?? "(default)", apply, codexUpdated, desktopUpdated, claudeRemoved }; } function renderClaudeDesktopServer(u: Upstream) { const env: Record<string, string> = {}; for (const k of u.env_vars ?? []) env[k] = `\${${k}}`; for (const [k, v] of Object.entries(u.env ?? {})) env[k] = v; return { command: u.command, args: u.args ?? [], env }; } async function decentralize({ apply, verbose }: { apply: boolean; verbose: boolean }) { const reg = await readRegistry(); const enabledUpstreams = reg.upstreams.filter((u) => u.enabled).filter((u) => u.id !== "mcpmanager"); let codexUpdated: string | null = null; let desktopUpdated: string | null = null; let claudeUpdated: Array<{ name: string; action: string; ok: boolean; reason?: string }> = []; if (apply) { // Codex: remove mcpmanager and re-enable upstreams. const codexPath = codexConfigPath(); if (existsSync(codexPath)) { let text = await fs.readFile(codexPath, "utf8"); text = removeTomlTable(text, "mcp_servers.mcpmanager"); for (const u of enabledUpstreams) { // best-effort: enable existing tables; don't add new ones here. text = setTomlEnabledInTable(text, `mcp_servers.${u.id}`, true); } await writeFileWithBackup(codexPath, text); codexUpdated = codexPath; } // Claude Desktop: remove mcpmanager and restore upstreams. const desktopPath = claudeDesktopConfigPath(); if (existsSync(desktopPath)) { const doc = JSON.parse(await fs.readFile(desktopPath, "utf8")); if (!doc.mcpServers) doc.mcpServers = {}; delete doc.mcpServers.mcpmanager; for (const u of enabledUpstreams) { doc.mcpServers[u.id] = renderClaudeDesktopServer(u); } await writeFileWithBackup(desktopPath, JSON.stringify(doc, null, 2) + "\n"); desktopUpdated = desktopPath; } // Claude Code: remove mcpmanager and restore upstreams. const { exitCode } = await run("claude", ["--version"]); if (exitCode === 0) { const rm = await run("claude", ["mcp", "remove", "--scope", "user", "mcpmanager"]); claudeUpdated.push({ name: "mcpmanager", action: "remove", ok: rm.exitCode === 0 || /not found/i.test(rm.stderr) || /does not exist/i.test(rm.stderr), reason: rm.exitCode === 0 ? undefined : (rm.stderr || rm.stdout).trim(), }); for (const u of enabledUpstreams) { const cfg: any = { command: u.command, args: u.args ?? [] }; if (u.env_vars?.length || Object.keys(u.env ?? {}).length) { const env: Record<string, string> = {}; for (const k of u.env_vars ?? []) env[k] = `\${${k}}`; for (const [k, v] of Object.entries(u.env ?? {})) env[k] = v; cfg.env = env; } const res = await run("claude", ["mcp", "add-json", u.id, "--scope", "user", JSON.stringify(cfg)]); const stderr = (res.stderr || "").trim(); const ok = res.exitCode === 0 || /already exists/i.test(stderr); claudeUpdated.push({ name: u.id, action: "add-json", ok, reason: ok ? undefined : stderr || (res.stdout || "").trim(), }); if (verbose && !ok) process.stderr.write(res.stderr); } } } return { restored: enabledUpstreams.map((u) => u.id), apply, codexUpdated, desktopUpdated, claudeUpdated, note: "Restart Codex/Claude apps after config changes.", }; } async function installClaudeDesktopConfig(gatewayPath: string) { const filePath = claudeDesktopConfigPath(); let doc: any = {}; if (existsSync(filePath)) { doc = JSON.parse(await fs.readFile(filePath, "utf8")); } if (!doc.mcpServers) doc.mcpServers = {}; const envValue = (key: string) => { const v = process.env[key]; return v && v.length > 0 ? v : `\${${key}}`; }; doc.mcpServers.mcpmanager = { command: gatewayPath, args: [], env: { DAYTONA_API_KEY: envValue("DAYTONA_API_KEY"), DAYTONA_API_URL: envValue("DAYTONA_API_URL"), DAYTONA_SERVER_URL: envValue("DAYTONA_SERVER_URL"), DAYTONA_TARGET: envValue("DAYTONA_TARGET"), TAILSCALE_API_KEY: envValue("TAILSCALE_API_KEY"), TAILSCALE_TAILNET: envValue("TAILSCALE_TAILNET"), MCPMANAGER_REGISTRY_PATH: envValue("MCPMANAGER_REGISTRY_PATH"), MCPMANAGER_POLICY_MODE: envValue("MCPMANAGER_POLICY_MODE"), MCPMANAGER_TAILSCALE_ALLOW_KEYS: envValue("MCPMANAGER_TAILSCALE_ALLOW_KEYS"), MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS: envValue("MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS"), MCPMANAGER_TAILSCALE_ALLOW_REUSABLE: envValue("MCPMANAGER_TAILSCALE_ALLOW_REUSABLE"), MCPMANAGER_TAILSCALE_TAILNET_LOCK: envValue("MCPMANAGER_TAILSCALE_TAILNET_LOCK"), MCPMANAGER_TAILSCALE_TAGS_ALLOW: envValue("MCPMANAGER_TAILSCALE_TAGS_ALLOW"), }, }; await writeFileWithBackup(filePath, JSON.stringify(doc, null, 2) + "\n"); return filePath; } async function installClaudeCodeConfig(gatewayPath: string) { const { exitCode } = await run("claude", ["--version"]); if (exitCode !== 0) return { ok: false, reason: "claude CLI not found" }; const config = JSON.stringify({ command: gatewayPath, args: [] }); const res = await run("claude", ["mcp", "add-json", "mcpmanager", "--scope", "user", config]); if (res.exitCode !== 0) { const err = res.stderr.trim() || "claude mcp add-json failed"; if (/already exists/i.test(err)) return { ok: true, reason: err }; return { ok: false, reason: err }; } return { ok: true, reason: res.stdout.trim() }; } async function smokePingGateway(gatewayPath: string) { const transport = new StdioClientTransport({ command: gatewayPath }); const client = new Client({ name: "mcpmanager-doctor", version: "0.1.0" }, { capabilities: {} }); await client.connect(transport); const tools = await client.listTools(); const ping = await client.callTool({ name: "health.ping", arguments: {} }); await client.close(); const first = (ping as any)?.content?.[0]; return { tools: tools.tools?.map((t) => t.name) ?? [], pingText: typeof first?.text === "string" ? first.text : "", }; } async function status() { const gwInstalled = gatewayInstallPath(); const gwExists = existsSync(gwInstalled); const codex = codexConfigPath(); const claudeDesktop = claudeDesktopConfigPath(); const codexInstalled = await (async () => { if (!existsSync(codex)) return false; try { const text = await Bun.file(codex).text(); return /^\[mcp_servers\.mcpmanager\]\s*$/m.test(text); } catch { return false; } })(); const claudeDesktopInstalled = await (async () => { if (!existsSync(claudeDesktop)) return false; try { const doc = JSON.parse(await Bun.file(claudeDesktop).text()); return Boolean(doc?.mcpServers?.mcpmanager); } catch { return false; } })(); return { gateway: { path: gwInstalled, exists: gwExists }, codex: { path: codex, exists: existsSync(codex), installed: codexInstalled }, claudeDesktop: { path: claudeDesktop, exists: existsSync(claudeDesktop), installed: claudeDesktopInstalled, }, }; } function printHelp() { console.log( [ "mcpManager (dev CLI)", "", "Usage:", " bun run manager help", " bun run manager status", " bun run manager build-gateway", " bun run manager install", " bun run manager doctor [--fix]", " bun run manager centralize [--apply]", " bun run manager decentralize --apply", "", "Flags:", " --fix Run install steps during doctor", " --apply Apply config changes (centralize)", " -v,--verbose Show command output", "", "Environment:", " DAYTONA_API_KEY, DAYTONA_SERVER_URL (or DAYTONA_API_URL), DAYTONA_TARGET", " TAILSCALE_API_KEY, TAILSCALE_TAILNET", " MCPMANAGER_POLICY_MODE, MCPMANAGER_TAILSCALE_ALLOW_KEYS, MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS", ].join("\n"), ); } async function main() { const args = parseArgs(process.argv); if (args.cmd === "help") { printHelp(); return; } if (args.cmd === "status") { console.log(JSON.stringify(await status(), null, 2)); return; } if (args.cmd === "build-gateway") { await buildGateway(args.verbose); console.log(`Built gateway: ${gatewayBuiltPath()}`); return; } if (args.cmd === "install") { await buildGateway(args.verbose); const gw = await installGatewayBinary(); const codex = await installCodexConfig(gw); const desktop = await installClaudeDesktopConfig(gw); const claude = await installClaudeCodeConfig(gw); console.log( JSON.stringify( { gateway: gw, codex, claudeDesktop: desktop, claudeCode: claude, note: "Restart Codex/Claude apps after config changes.", }, null, 2, ), ); return; } if (args.cmd === "doctor") { const before = await status(); if (args.fix) { await mainInstall(args.verbose); } const after = await status(); const gwPath = gatewayInstallPath(); const gatewayOk = existsSync(gwPath); const ping = gatewayOk ? await smokePingGateway(gwPath) : null; console.log( JSON.stringify( { before, after, gatewayPing: ping, env: { DAYTONA_API_KEY: Boolean(process.env.DAYTONA_API_KEY), DAYTONA_SERVER_URL: Boolean(process.env.DAYTONA_SERVER_URL || process.env.DAYTONA_API_URL), DAYTONA_TARGET: process.env.DAYTONA_TARGET ?? null, TAILSCALE_API_KEY: Boolean(process.env.TAILSCALE_API_KEY), TAILSCALE_TAILNET: process.env.TAILSCALE_TAILNET ?? null, }, }, null, 2, ), ); return; } if (args.cmd === "centralize") { const res = await centralize({ apply: args.apply, verbose: args.verbose }); console.log(JSON.stringify(res, null, 2)); return; } if (args.cmd === "decentralize") { const res = await decentralize({ apply: args.apply, verbose: args.verbose }); console.log(JSON.stringify(res, null, 2)); return; } } async function mainInstall(verbose: boolean) { await buildGateway(verbose); const gw = await installGatewayBinary(); await installCodexConfig(gw); await installClaudeDesktopConfig(gw); await installClaudeCodeConfig(gw); } await main();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/maxtheman/mcpManager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server