Files
neuron-connectd/dist/server.js
Tim Lingo a01236753a feat(connectors): clean-install bundling + Google OAuth foundation
Make connectors work on a clean Mac with no Node/npm installed, and lay
the foundation for one-click Google sign-in.

Clean-install bundling:
- bundle scripts: `npm run bundle` -> self-contained dist/connectd.cjs (CJS,
  no node_modules); `bundle:servers` -> pre-bundled MCP servers (fs-server.mjs
  for "Your files", runs without npx); `bundle:all`.
- runtime resolution in the bridge: a connector config can use command "@node"
  (the bundled runtime, process.execPath) and arg "@bundled/<server>" (a server
  shipped alongside the bridge), resolved at spawn time via
  NEURON_CONNECTD_BUNDLE_DIR. So remote connectors + "Your files" work offline,
  zero deps.
- graceful ENOENT -> needs_setup ("coming soon") instead of a cryptic spawn
  crash for not-yet-bundled npx connectors on a clean machine.

Google OAuth foundation (google-oauth.ts):
- desktop-style authorization-code + PKCE flow, refresh, Keychain storage,
  gated on GOOGLE_OAUTH_CLIENT_ID (honest needs_setup until configured).
- new auth mode "google" for stdio connectors + /google/oauth/start and
  /google/callback endpoints. NOTE: pending the Neuron Google OAuth client +
  Google verification; not certified end-to-end yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:27:04 -05:00

181 lines
9.1 KiB
JavaScript

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 `<!doctype html><meta charset="utf-8"><title>Neuron Connectors</title>` +
`<body style="font:15px -apple-system,system-ui;margin:18% auto;max-width:420px;text-align:center;color:#222">` +
`<div style="font-size:42px">${ok ? "&#10003;" : "&#10007;"}</div><p>${msg}</p></body>`;
}
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 ?? ""));
}
// POST /google/oauth/start { id } — begin a Google one-click sign-in, return the consent URL.
if (method === "POST" && url === "/google/oauth/start") {
const parsed = await parseJsonBody(req);
if (!parsed || !parsed.id)
return sendJson(res, 400, { ok: false, error: "missing id" });
const result = await bridge.startGoogleOAuth(parsed.id);
return sendJson(res, result.ok ? 200 : 400, result);
}
// GET /google/callback?code=..&state=.. — Google redirects here after consent.
if (method === "GET" && url.startsWith("/google/callback")) {
const q = new URL(url, `http://${HOST}:${PORT}`).searchParams;
const id = q.get("state"); // we pass the connector id as `state`
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.finishGoogleOAuth(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}`);
});
}