import { createServer } from "node:http"; import { HOST, PORT } from "./config.js"; function sendJson(res, status, body) { const payload = JSON.stringify(body); res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload), }); res.end(payload); } function sendHtml(res, status, html) { res.writeHead(status, { "content-type": "text/html; charset=utf-8" }); res.end(html); } function authResultPage(ok, detail) { const msg = ok ? "Connected. You can close this tab." : `Sign-in failed: ${detail}`; return `Neuron Connectors` + `` + `
${ok ? "✓" : "✗"}

${msg}

`; } function readBody(req) { return new Promise((resolve, reject) => { let data = ""; req.on("data", (c) => { data += c; if (data.length > 5_000_000) reject(new Error("body too large")); }); req.on("end", () => resolve(data)); req.on("error", reject); }); } // Read + JSON-parse a request body; returns null on empty/invalid JSON. async function parseJsonBody(req) { const raw = await readBody(req); try { return JSON.parse(raw || "{}"); } catch { return null; } } export function startServer(bridge) { const server = createServer(async (req, res) => { const url = req.url ?? "/"; const method = req.method ?? "GET"; try { // GET /mcp/tools — merged, namespaced tool schemas for the soul. if (method === "GET" && url === "/mcp/tools") { return sendJson(res, 200, { tools: bridge.allTools() }); } // GET /mcp/servers — per-server health for the UI. if (method === "GET" && url === "/mcp/servers") { return sendJson(res, 200, { servers: bridge.serverStatuses() }); } // GET /healthz — liveness. if (method === "GET" && url === "/healthz") { return sendJson(res, 200, { ok: true }); } // POST /mcp/oauth/start { id } — begin OAuth sign-in, return the auth URL. if (method === "POST" && url === "/mcp/oauth/start") { const raw = await readBody(req); let parsed; try { parsed = JSON.parse(raw || "{}"); } catch { return sendJson(res, 400, { ok: false, error: "invalid JSON body" }); } if (!parsed.id) return sendJson(res, 400, { ok: false, error: "missing id" }); const result = await bridge.startOAuth(parsed.id); return sendJson(res, result.ok ? 200 : 400, result); } // GET /mcp/oauth/callback?id=..&code=.. — provider redirects here after consent. if (method === "GET" && url.startsWith("/mcp/oauth/callback")) { const q = new URL(url, `http://${HOST}:${PORT}`).searchParams; const id = q.get("id"); const code = q.get("code"); const err = q.get("error"); if (err) return sendHtml(res, 400, authResultPage(false, err)); if (!id || !code) return sendHtml(res, 400, authResultPage(false, "missing id or code")); const result = await bridge.finishOAuth(id, code); return sendHtml(res, result.ok ? 200 : 400, authResultPage(result.ok, result.error ?? "")); } // GET /mcp/auto-approved — the soul reads this to decide which mcp__* calls skip the // approval card (per-connector opt-in, off by default). if (method === "GET" && url === "/mcp/auto-approved") { return sendJson(res, 200, { tools: bridge.autoApprovedTools() }); } // POST /mcp/servers/add { id, config } — add or replace a connector, connect it. if (method === "POST" && url === "/mcp/servers/add") { const parsed = await parseJsonBody(req); if (!parsed || !parsed.id || !parsed.config) return sendJson(res, 400, { ok: false, error: "missing id or config" }); const result = await bridge.addServer(parsed.id, parsed.config); return sendJson(res, result.ok ? 200 : 400, result); } // POST /mcp/servers/toggle { id, enabled } — enable/disable a connector. if (method === "POST" && url === "/mcp/servers/toggle") { const parsed = await parseJsonBody(req); if (!parsed || !parsed.id || typeof parsed.enabled !== "boolean") return sendJson(res, 400, { ok: false, error: "missing id or enabled" }); const result = await bridge.setEnabled(parsed.id, parsed.enabled); return sendJson(res, result.ok ? 200 : 400, result); } // POST /mcp/servers/auto-approve { id, autoApprove } — toggle approval-skip for a connector. if (method === "POST" && url === "/mcp/servers/auto-approve") { const parsed = await parseJsonBody(req); if (!parsed || !parsed.id || typeof parsed.autoApprove !== "boolean") return sendJson(res, 400, { ok: false, error: "missing id or autoApprove" }); const result = await bridge.setAutoApprove(parsed.id, parsed.autoApprove); return sendJson(res, result.ok ? 200 : 400, result); } // POST /mcp/servers/secret { id, secret } — store an API token in the Keychain. if (method === "POST" && url === "/mcp/servers/secret") { const parsed = await parseJsonBody(req); if (!parsed || !parsed.id || !parsed.secret) return sendJson(res, 400, { ok: false, error: "missing id or secret" }); const result = await bridge.setSecret(parsed.id, parsed.secret); return sendJson(res, result.ok ? 200 : 400, result); } // POST /mcp/servers/remove { id } — remove a connector + its stored secret. if (method === "POST" && url === "/mcp/servers/remove") { const parsed = await parseJsonBody(req); if (!parsed || !parsed.id) return sendJson(res, 400, { ok: false, error: "missing id" }); const result = await bridge.removeServer(parsed.id); return sendJson(res, result.ok ? 200 : 400, result); } // POST /mcp/call { name, input } — proxy tools/call to the owning server. if (method === "POST" && url === "/mcp/call") { const raw = await readBody(req); let parsed; try { parsed = JSON.parse(raw || "{}"); } catch { return sendJson(res, 400, { ok: false, error: "invalid JSON body" }); } if (!parsed.name) { return sendJson(res, 400, { ok: false, error: "missing tool name" }); } const result = await bridge.call(parsed.name, parsed.input ?? {}); return sendJson(res, result.ok ? 200 : 502, result); } sendJson(res, 404, { ok: false, error: "not found" }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); sendJson(res, 500, { ok: false, error: msg }); } }); server.listen(PORT, HOST, () => { console.error(`[connectd] listening on http://${HOST}:${PORT}`); }); }