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:
2026-06-13 18:45:55 -05:00
commit fdf8fb5cda
19 changed files with 3378 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
// A minimal remote MCP server over Streamable HTTP that REQUIRES a bearer token.
// Used to verify neuron-connectd's http transport + Keychain-backed auth end to
// end, with no external dependency. Wrong/missing token → 401.
//
// TEST_MCP_TOKEN=secret-123 TEST_MCP_PORT=7795 node test/auth-http-mcp-server.mjs
import http from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const TOKEN = process.env.TEST_MCP_TOKEN || "secret-123";
const PORT = Number(process.env.TEST_MCP_PORT || 7795);
function buildServer() {
const server = new McpServer({ name: "auth-test", version: "0.1.0" });
server.registerTool(
"whoami",
{ description: "Return the authenticated identity.", inputSchema: {} },
async () => ({ content: [{ type: "text", text: "authenticated-ok: bearer token accepted" }] })
);
return server;
}
http
.createServer(async (req, res) => {
// Bearer auth gate — the whole point of the test.
const auth = req.headers["authorization"] || "";
if (auth !== `Bearer ${TOKEN}`) {
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
res.end(JSON.stringify({ error: "unauthorized" }));
return;
}
let body = "";
for await (const chunk of req) body += chunk;
const parsed = body ? JSON.parse(body) : undefined;
const server = buildServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, parsed);
})
.listen(PORT, "127.0.0.1", () => {
console.error(`[auth-test-mcp] listening on http://127.0.0.1:${PORT} (token gated)`);
});
+49
View File
@@ -0,0 +1,49 @@
// Local verification of the OAuth provider's durable mechanics (no external IdP):
// the KeychainOAuthProvider must round-trip tokens, client info, and the PKCE
// verifier through the macOS Keychain so a sign-in survives bridge restarts.
import { KeychainOAuthProvider } from "../src/oauth.js";
import { kcDelete } from "../src/keychain.js";
const ID = "oauth-selftest";
const p = new KeychainOAuthProvider(ID, "drive.readonly");
let failures = 0;
const check = (label: string, cond: boolean) => {
console.log(`${cond ? "ok " : "FAIL"} ${label}`);
if (!cond) failures++;
};
// metadata + redirect shape
check("redirectUrl points at loopback callback with id",
p.redirectUrl.includes("/mcp/oauth/callback?id=oauth-selftest"));
check("clientMetadata is PKCE public client",
p.clientMetadata.token_endpoint_auth_method === "none" &&
p.clientMetadata.response_types.includes("code"));
check("state carries serverId", p.state() === ID);
// tokens round-trip through Keychain
await p.saveTokens({ access_token: "tok-abc", token_type: "bearer", expires_in: 3600 });
const t = await p.tokens();
check("tokens persisted + read back", t?.access_token === "tok-abc");
check("hasTokens true after save", await p.hasTokens());
// client info (dynamic registration) round-trip
await p.saveClientInformation({ client_id: "cid-1", redirect_uris: [p.redirectUrl] } as any);
const ci = await p.clientInformation();
check("client info persisted + read back", (ci as any)?.client_id === "cid-1");
// PKCE verifier round-trip
await p.saveCodeVerifier("verifier-xyz");
check("code verifier persisted + read back", (await p.codeVerifier()) === "verifier-xyz");
// invalidate clears everything
await p.invalidateCredentials("all");
check("invalidate(all) clears tokens", !(await p.hasTokens()));
// cleanup any stragglers
await kcDelete(`${ID}:oauth`);
await kcDelete(`${ID}:client`);
await kcDelete(`${ID}:verifier`);
console.log(failures === 0 ? "\nALL OAUTH MECHANICS OK" : `\n${failures} FAILURE(S)`);
process.exit(failures === 0 ? 0 : 1);