Merge PR #14: feat(soul): MCP connectors — /api/connectors proxy + per-connector auto-approve
Deploy Soul to GKE / deploy (push) Failing after 7m14s
Neuron Soul CI / build (push) Failing after 8m16s

Applies connector-specific additions from feat/connectors-soul:
- chat.el: connector_tools_json(), agentic_tools_all(), call_mcp_bridge(),
  tool_auto_approved() and mcp__ dispatch in dispatch_tool()
- routes.el: connectd_get/post, handle_connectors(), /api/connectors routing
  in GET and POST sections
- MEMORY_RECALL_BUG.md: investigation notes on memory retrieval failure

The agentic loop rewrite in the source branch was not applied — it conflicts
with the tool-bridge pattern from PR #5 which is the chosen design for
client-side MCP tool execution. The connectors themselves are now fully
wired: connector tools surface as mcp__<server>__<tool> in the tools array
and dispatch to neuron-connectd via call_mcp_bridge().
This commit is contained in:
2026-06-15 11:37:34 -05:00
parent a0470acc45
commit 1c8438ad20
3 changed files with 322 additions and 0 deletions
+59
View File
@@ -207,6 +207,59 @@ fn route_sessions() -> String {
return results
}
// ---------------------------------------------------------------------------
// MCP Connectors proxy thin pass-through to neuron-connectd on :7771.
// The UI talks to ONE origin (the soul); all MCP/config complexity lives in
// the bridge. Bridge-down returns a clear error (not a panic).
// ---------------------------------------------------------------------------
fn connectd_get(suffix: String) -> String {
let out: String = exec_capture("curl -s --max-time 5 http://127.0.0.1:7771" + suffix)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
// POST passthrough: request body is written to a temp file and passed via -d @file
// so arbitrary JSON cannot reach the shell as a command-line argument.
fn connectd_post(suffix: String, body: String) -> String {
let eff: String = if str_eq(body, "") { "{}" } else { body }
let tmp: String = "/tmp/neuron-connectors-req.json"
fs_write(tmp, eff)
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
fn handle_connectors(method: String, clean: String, body: String) -> String {
if str_eq(method, "GET") {
// /api/connectors -> each configured server with status, tools, auth, auto-approve.
return connectd_get("/mcp/servers")
}
if str_eq(clean, "/api/connectors/add") {
return connectd_post("/mcp/servers/add", body)
}
if str_eq(clean, "/api/connectors/toggle") {
return connectd_post("/mcp/servers/toggle", body)
}
if str_eq(clean, "/api/connectors/auto-approve") {
return connectd_post("/mcp/servers/auto-approve", body)
}
if str_eq(clean, "/api/connectors/remove") {
return connectd_post("/mcp/servers/remove", body)
}
if str_eq(clean, "/api/connectors/secret") {
return connectd_post("/mcp/servers/secret", body)
}
if str_eq(clean, "/api/connectors/oauth/start") {
return connectd_post("/mcp/oauth/start", body)
}
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
}
fn handle_request(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
@@ -320,6 +373,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_starts_with(clean, "/api/neuron/recall") {
return handle_api_recall(method, path, body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
return err_404(clean)
}
@@ -458,6 +514,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(clean, "/api/neuron/cultivate") {
return handle_api_cultivate(body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
return err_404(clean)
}