import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs"; import { homedir } from "node:os"; import { join, dirname } from "node:path"; // A single configured MCP server. Phase 1 supports stdio only; `http` transport // (remote MCP + OAuth) lands in Phase 3. export interface ServerConfig { transport: "stdio" | "http"; enabled: boolean; // stdio command?: string; args?: string[]; env?: Record; // http (Phase 3) url?: string; auth?: "none" | "oauth" | "token"; scope?: string; // OAuth scope string, when auth === "oauth" // Phase 5: per-connector opt-in to skip the soul's approval card (read-only-leaning, // off by default). The soul reads this from connectors.json at approval time. autoApprove?: boolean; // Phase 5: tool-poisoning guard. Last-known hash of this server's tool schemas; if the // server silently changes a tool description on reconnect, the bridge flags it in the UI. schemaHash?: string; } export interface ConnectorsConfig { servers: Record; } export const CONFIG_PATH = join(homedir(), ".neuron", "connectors.json"); const SANDBOX = join(homedir(), "neuron-connectd-sandbox"); // Phase-1 default: one zero-auth filesystem server scoped to the sandbox dir. // Once ~/.neuron/connectors.json exists, it wins. function defaultConfig(): ConnectorsConfig { return { servers: { filesystem: { transport: "stdio", enabled: true, command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", SANDBOX], }, }, }; } export function loadConfig(): ConnectorsConfig { try { const raw = readFileSync(CONFIG_PATH, "utf8"); const parsed = JSON.parse(raw) as ConnectorsConfig; if (!parsed.servers || typeof parsed.servers !== "object") { return defaultConfig(); } return parsed; } catch { // No config file yet — run the Phase-1 default so the bridge is useful out of the box. return defaultConfig(); } } // Atomic write of connectors.json (temp + rename), 0600. The bridge owns all writes so the // soul stays simple and El never has to manipulate JSON. UI edits flow: UI → soul route → bridge. export function saveConfig(config: ConnectorsConfig): void { mkdirSync(dirname(CONFIG_PATH), { recursive: true }); const tmp = `${CONFIG_PATH}.tmp`; writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 }); renameSync(tmp, CONFIG_PATH); } export const PORT = Number(process.env.NEURON_CONNECTD_PORT ?? 7771); export const HOST = "127.0.0.1"; // loopback only — never bind 0.0.0.0