// Registers the connector with the Claude apps on this computer, so the user // clicks "Connect" instead of copying a command into a terminal. // // Claude Code: through its own CLI (`claude mcp add-json … --scope user`), so // Claude Code writes its own config and nothing here edits ~/.claude.json, // which a running Claude Code rewrites at will. That file is only READ, to // show whether the entry is there and current. // // Claude Desktop: its claude_desktop_config.json is the file users are told // to edit by hand. Only the `theseus` entry is added, changed or removed; the // rest is kept as it was, a copy of the previous file is kept beside it, and // a file that does not parse is left alone. const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const { execFile } = require("node:child_process"); const NAME = "theseus"; function env(opts) { const e = opts.env || process.env; const platform = opts.platform || process.platform; const home = opts.home || os.homedir(); return { e, platform, home }; } // ---- Claude Code --------------------------------------------------------- // The CLI as a native binary only: an npm `claude.cmd` would need a shell, // and a shell would have to re-quote the JSON argument. function findClaudeCli(opts = {}) { const { e, platform, home } = env(opts); if (opts.cli) return fs.existsSync(opts.cli) ? opts.cli : null; const names = platform === "win32" ? ["claude.exe"] : ["claude"]; const dirs = [ path.join(home, ".local", "bin"), path.join(home, ".claude", "local"), ...(platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin"]), ...String(e.PATH || e.Path || "").split(path.delimiter).filter(Boolean), ]; for (const d of dirs) { for (const n of names) { const p = path.join(d, n); try { if (fs.statSync(p).isFile()) return p; } catch {} } } return null; } function claudeCodeConfigPath(opts = {}) { const { e, home } = env(opts); return e.CLAUDE_CONFIG_DIR ? path.join(e.CLAUDE_CONFIG_DIR, ".claude.json") : path.join(home, ".claude.json"); } function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } } function httpEntry(url, token) { return { type: "http", url, headers: { Authorization: `Bearer ${token}` } }; } function sameHttp(entry, url, token) { return !!entry && entry.url === url && entry.headers && entry.headers.Authorization === `Bearer ${token}`; } function run(cli, args, opts) { return new Promise((resolve) => { execFile(cli, args, { timeout: 30000, windowsHide: true, env: opts.env || process.env }, (err, stdout, stderr) => { resolve({ ok: !err, code: err ? (err.code ?? 1) : 0, out: `${stdout || ""}${stderr || ""}`.trim() }); }); }); } const claudeCode = { status({ url, token }, opts = {}) { const cli = findClaudeCli(opts); const cfg = readJson(claudeCodeConfigPath(opts)); const entry = cfg && cfg.mcpServers ? cfg.mcpServers[NAME] : null; return { installed: !!cli, configured: !!entry, current: !!url && sameHttp(entry, url, token), }; }, async register({ url, token }, opts = {}) { const cli = findClaudeCli(opts); if (!cli) throw new Error("Claude Code was not found on this computer (looked for the claude command)"); // add-json refuses an existing name, so an update is remove + add. await run(cli, ["mcp", "remove", NAME, "--scope", "user"], opts); const r = await run(cli, ["mcp", "add-json", NAME, JSON.stringify(httpEntry(url, token)), "--scope", "user"], opts); if (!r.ok) throw new Error(`Claude Code refused the entry: ${r.out || `exit ${r.code}`}`); return { ok: true }; }, async unregister(opts = {}) { const cli = findClaudeCli(opts); if (!cli) throw new Error("Claude Code was not found on this computer"); const r = await run(cli, ["mcp", "remove", NAME, "--scope", "user"], opts); if (!r.ok && !/not found|no .*server/i.test(r.out)) throw new Error(`Claude Code: ${r.out || `exit ${r.code}`}`); return { ok: true }; }, }; // ---- Claude Desktop ------------------------------------------------------ function desktopConfigPath(opts = {}) { const { e, platform, home } = env(opts); if (platform === "win32") return path.join(e.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"); if (platform === "darwin") return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"); return path.join(e.XDG_CONFIG_HOME || path.join(home, ".config"), "Claude", "claude_desktop_config.json"); } // Claude Desktop starts stdio servers only: it runs Theseus itself in Node // mode with the relay, which reads the address from Theseus's endpoint file. function desktopEntry({ exe, shim, url, token }) { return { command: exe, args: [shim], env: { ELECTRON_RUN_AS_NODE: "1", THESEUS_MCP_URL: url, THESEUS_MCP_TOKEN: token } }; } function sameDesktop(entry, want) { return !!entry && entry.command === want.command && JSON.stringify(entry.args) === JSON.stringify(want.args) && entry.env && entry.env.THESEUS_MCP_URL === want.env.THESEUS_MCP_URL && entry.env.THESEUS_MCP_TOKEN === want.env.THESEUS_MCP_TOKEN; } function editDesktopConfig(file, edit) { let cfg = {}; if (fs.existsSync(file)) { const text = fs.readFileSync(file, "utf8"); try { cfg = text.trim() ? JSON.parse(text) : {}; } catch { throw new Error(`${file} is not valid JSON; fix or remove it in Claude Desktop first`); } fs.copyFileSync(file, file + ".theseus-backup"); } if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) throw new Error(`${file} does not hold a settings object`); edit(cfg); const tmp = file + ".tmp-" + process.pid; fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n"); fs.renameSync(tmp, file); } const claudeDesktop = { status(params, opts = {}) { const file = desktopConfigPath(opts); const cfg = readJson(file); const entry = cfg && cfg.mcpServers ? cfg.mcpServers[NAME] : null; return { installed: fs.existsSync(path.dirname(file)), configured: !!entry, current: !!params.url && sameDesktop(entry, desktopEntry(params)), }; }, async register(params, opts = {}) { const file = desktopConfigPath(opts); if (!fs.existsSync(path.dirname(file))) throw new Error("Claude Desktop was not found on this computer (no Claude settings folder)"); editDesktopConfig(file, (cfg) => { if (!cfg.mcpServers || typeof cfg.mcpServers !== "object") cfg.mcpServers = {}; cfg.mcpServers[NAME] = desktopEntry(params); }); return { ok: true, restart: true }; }, async unregister(opts = {}) { const file = desktopConfigPath(opts); if (!fs.existsSync(file)) return { ok: true }; editDesktopConfig(file, (cfg) => { if (cfg.mcpServers) delete cfg.mcpServers[NAME]; }); return { ok: true, restart: true }; }, }; module.exports = { claudeCode, claudeDesktop, findClaudeCli, claudeCodeConfigPath, desktopConfigPath, httpEntry, desktopEntry };