feat: neuron-connectd — MCP connector bridge (Accessor sidecar)
The sidecar that isolates all MCP wire complexity from the soul. Binds
loopback 127.0.0.1:7771 only. The soul reaches it over flat HTTP; the bridge
owns stdio/streamable-HTTP transports, OAuth (PKCE), Keychain secrets, server
lifecycle, config, and a tool-schema-hash poisoning guard.
HTTP contract: GET /mcp/tools, /mcp/servers, /mcp/auto-approved, /healthz;
POST /mcp/call, /mcp/oauth/start, /mcp/servers/{add,toggle,auto-approve,
remove,secret}; GET /mcp/oauth/callback.
Config: ~/.neuron/connectors.json (servers, no secrets). Secrets in macOS
Keychain (service ai.neuron.connect, account = serverId). Spec:
docs/research/mcp-connectors-adoption-spec.md.
Phases 1-3 verified end to end (stdio + HTTP transport, Keychain token auth,
OAuth round-trip); Phase 4/5 (CRUD + auto-approve + schema-hash) added for the
ConnectorsView UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
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() {
|
||||
return {
|
||||
servers: {
|
||||
filesystem: {
|
||||
transport: "stdio",
|
||||
enabled: true,
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", SANDBOX],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
export function loadConfig() {
|
||||
try {
|
||||
const raw = readFileSync(CONFIG_PATH, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
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) {
|
||||
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
|
||||
Reference in New Issue
Block a user