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
+48
@@ -0,0 +1,48 @@
|
||||
import { execFile } from "node:child_process";
|
||||
// macOS Keychain store for connector secrets. Mirrors the pattern Backend.kt
|
||||
// uses for the soul's provider key (service "ai.neuron.soul"); connectors live
|
||||
// under their own service so they never collide with the LLM key.
|
||||
//
|
||||
// Account convention:
|
||||
// <serverId> → static bearer token (auth: "token")
|
||||
// <serverId>:oauth → JSON-encoded OAuthTokens blob (auth: "oauth")
|
||||
// <serverId>:verifier → transient PKCE code_verifier during a flow
|
||||
// <serverId>:client → JSON OAuthClientInformation from dynamic registration
|
||||
const SERVICE = "ai.neuron.connect";
|
||||
function run(args) {
|
||||
return new Promise((resolve) => {
|
||||
execFile("/usr/bin/security", args, (err, stdout) => {
|
||||
resolve({ code: err ? (err.code ?? 1) : 0, stdout: stdout ?? "" });
|
||||
});
|
||||
});
|
||||
}
|
||||
export async function kcGet(account) {
|
||||
const { code, stdout } = await run(["find-generic-password", "-s", SERVICE, "-a", account, "-w"]);
|
||||
if (code !== 0)
|
||||
return undefined;
|
||||
const v = stdout.replace(/\n$/, "");
|
||||
return v.length ? v : undefined;
|
||||
}
|
||||
export async function kcSet(account, secret) {
|
||||
// -U updates in place if the item already exists.
|
||||
const { code } = await run(["add-generic-password", "-U", "-s", SERVICE, "-a", account, "-w", secret]);
|
||||
return code === 0;
|
||||
}
|
||||
export async function kcDelete(account) {
|
||||
await run(["delete-generic-password", "-s", SERVICE, "-a", account]);
|
||||
}
|
||||
// JSON helpers for the OAuth blobs.
|
||||
export async function kcGetJson(account) {
|
||||
const raw = await kcGet(account);
|
||||
if (!raw)
|
||||
return undefined;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
export async function kcSetJson(account, value) {
|
||||
return kcSet(account, JSON.stringify(value));
|
||||
}
|
||||
Reference in New Issue
Block a user