feat(soul): MCP tool-bridge — suspend agentic loop for client-executed tools #5

Closed
tim.lingo wants to merge 0 commits from feat/mcp-tool-bridge into main
Member

What

"Option B" MCP tool-bridge on the soul side. The agentic chat loop
(handle_chat_agentic) used to return the literal string "unknown tool: <name>"
for any tool it could not run in-process. It now suspends and asks the client
(the Kotlin desktop app) to execute the tool — this is what lets Neuron run MCP
connectors/plugins, matching Claude's Connectors.

Built-in tools (read_file, write_file, web_get, search_memory, run_command)
and Anthropic's native server-side web_search are unchanged. There was no
pre-existing tool-approval / tool_pending round-trip in the soul, so this PR
defines the contract below as a new, minimal tool_result endpoint.

Client contract (for the Kotlin MCP-client lane)

1. Soul → client: pending signal

When the model calls a tool the soul cannot run in-process, the agentic loop
suspends and /api/chat (agentic) returns, with HTTP 200:

{
  "tool_pending": true,
  "session_id": "br-<time>-<seq>",
  "call_id": "<tool_use_id>",
  "tool_name": "<mcp tool name>",
  "tool_input": { ... },        // raw JSON object the model passed
  "model": "claude-...",
  "agentic": true,
  "tools_used": ["..."]
}

The client detects tool_pending == true, executes the named MCP tool with
tool_input, and captures the output as a string.

2. Client → soul: result, to resume

POST /api/sessions/{session_id}/tool_result

Body:

{
  "call_id": "<the call_id from the pending envelope>",
  "content": "<MCP tool output as a string>"
}
  • session_id comes from the URL path (the session_id field of the envelope).
  • call_id should echo the envelope's call_id. If omitted/mismatched, the soul
    falls back to the tool_use_id it suspended on, so a partial client still resumes.
  • content is the tool's textual result (truncated to 6000 chars upstream, like
    local tool results).

3. Soul resumes

The soul appends the result as a tool_result block bound to the original
tool_use_id, re-enters the agentic loop, and returns the same envelope shape:

  • a final answer:
    { "reply": "...", "model": "...", "agentic": true, "tools_used": ["..."] }
    
  • or another tool_pending envelope if the continuation hits a further MCP tool.
    The bridge is fully chainable across multiple MCP calls in one turn.

The saved continuation is one-shot: it is cleared on resume, so a session_id
cannot be replayed.

Implementation notes

  • chat.el: is_builtin_tool, next_bridge_id, refactor of the agentic loop into a
    resumable agentic_loop(session_id, model, safe_sys, tools_json, messages, h, tools_log),
    plus bridge_save, agentic_resume, handle_tool_result. The suspended turn's full
    message history (including the assistant turn that requested the tool) is persisted in
    soul state under mcp_bridge:<session_id>.
  • routes.el: routes POST /api/sessions/{id}/tool_result to handle_tool_result.
  • Scope: this PR bridges the chat agentic loop (handle_chat_agentic) used by the
    desktop app. The DHARMA-room agentic loop (handle_dharma_room_turn_agentic) is left
    unchanged for now; it still runs only built-ins.

Verification

  • elc-verified: ~/el-sdk/elc --target=c <file> exits 0 with no stderr for chat.el,
    routes.el, and the full soul.el import graph.
  • The soul cannot be run locally (local builds can't network), so this is elc-verified
    only and needs Will's build
    to ship and test end-to-end.

🤖 Generated with Claude Code

## What "Option B" MCP tool-bridge on the soul side. The agentic chat loop (`handle_chat_agentic`) used to return the literal string `"unknown tool: <name>"` for any tool it could not run in-process. It now **suspends** and asks the **client** (the Kotlin desktop app) to execute the tool — this is what lets Neuron run MCP connectors/plugins, matching Claude's Connectors. Built-in tools (`read_file`, `write_file`, `web_get`, `search_memory`, `run_command`) and Anthropic's native server-side `web_search` are unchanged. There was no pre-existing tool-approval / `tool_pending` round-trip in the soul, so this PR defines the contract below as a new, minimal `tool_result` endpoint. ## Client contract (for the Kotlin MCP-client lane) ### 1. Soul → client: pending signal When the model calls a tool the soul cannot run in-process, the agentic loop suspends and `/api/chat` (agentic) returns, with HTTP 200: ```json { "tool_pending": true, "session_id": "br-<time>-<seq>", "call_id": "<tool_use_id>", "tool_name": "<mcp tool name>", "tool_input": { ... }, // raw JSON object the model passed "model": "claude-...", "agentic": true, "tools_used": ["..."] } ``` The client detects `tool_pending == true`, executes the named MCP tool with `tool_input`, and captures the output as a string. ### 2. Client → soul: result, to resume ``` POST /api/sessions/{session_id}/tool_result ``` Body: ```json { "call_id": "<the call_id from the pending envelope>", "content": "<MCP tool output as a string>" } ``` - `session_id` comes from the URL path (the `session_id` field of the envelope). - `call_id` should echo the envelope's `call_id`. If omitted/mismatched, the soul falls back to the `tool_use_id` it suspended on, so a partial client still resumes. - `content` is the tool's textual result (truncated to 6000 chars upstream, like local tool results). ### 3. Soul resumes The soul appends the result as a `tool_result` block bound to the original `tool_use_id`, re-enters the agentic loop, and returns the **same envelope shape**: - a final answer: ```json { "reply": "...", "model": "...", "agentic": true, "tools_used": ["..."] } ``` - or another `tool_pending` envelope if the continuation hits a further MCP tool. The bridge is fully **chainable** across multiple MCP calls in one turn. The saved continuation is **one-shot**: it is cleared on resume, so a `session_id` cannot be replayed. ## Implementation notes - `chat.el`: `is_builtin_tool`, `next_bridge_id`, refactor of the agentic loop into a resumable `agentic_loop(session_id, model, safe_sys, tools_json, messages, h, tools_log)`, plus `bridge_save`, `agentic_resume`, `handle_tool_result`. The suspended turn's full message history (including the assistant turn that requested the tool) is persisted in soul state under `mcp_bridge:<session_id>`. - `routes.el`: routes `POST /api/sessions/{id}/tool_result` to `handle_tool_result`. - Scope: this PR bridges the **chat** agentic loop (`handle_chat_agentic`) used by the desktop app. The DHARMA-room agentic loop (`handle_dharma_room_turn_agentic`) is left unchanged for now; it still runs only built-ins. ## Verification - elc-verified: `~/el-sdk/elc --target=c <file>` exits 0 with **no stderr** for `chat.el`, `routes.el`, and the full `soul.el` import graph. - The soul cannot be run locally (local builds can't network), so this is **elc-verified only and needs Will's build** to ship and test end-to-end. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tim.lingo added 1 commit 2026-06-11 02:32:02 +00:00
feat(soul): MCP tool-bridge — suspend agentic loop for client-executed tools
Neuron Soul CI / build (pull_request) Failing after 4m8s
c3f39a949d
When handle_chat_agentic hits a tool the soul cannot run in-process (an MCP
connector/plugin surfaced by the Kotlin desktop app), instead of returning
"unknown tool" it now suspends the agentic loop and returns a tool_pending
envelope so the CLIENT executes the tool and posts the result back. Built-in
tools (read_file/write_file/web_get/search_memory/run_command) and Anthropic's
native web_search are unchanged.

Client contract:
- Soul returns (HTTP 200) on an unknown tool:
    { "tool_pending": true, "session_id": "br-...", "call_id": "<tool_use_id>",
      "tool_name": "...", "tool_input": { ... }, "model": "...",
      "agentic": true, "tools_used": [...] }
- Client runs the MCP tool, then POSTs to
    /api/sessions/{session_id}/tool_result
  with body:
    { "call_id": "<the call_id from the envelope>",
      "content": "<MCP tool output as a string>" }
- Soul resumes the loop and returns the same envelope shape: either a final
    { "reply": ..., "tools_used": [...] }
  or another tool_pending if the continuation needs a further MCP tool
  (fully chainable). Saved continuation is one-shot (cleared on resume).

elc-verified (--target=c, exit 0, no stderr) on chat.el, routes.el, and the
full soul.el import graph. Needs Will's build to ship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tim.lingo changed title from test to feat(soul): MCP tool-bridge — suspend agentic loop for client-executed tools 2026-06-11 02:32:11 +00:00
Author
Member

Handoff (auto) — prereq #16.

⚠ OVERLAPS #9 by ~80%: identical changes to awareness.el (+402 -9), mcp-wrapper/src/main.el (+831), neuron-api.el (+173), mcp-proxy, and the same docs. #5 and #9 are the same underlying work in two giant diffs — they will conflict. RECONCILE with #9 before merging either (see #9; likely #9 is canonical).

WHAT real: awareness.el +402, chat.el +184, mcp-wrapper +831, neuron-api.el +173, soul.el +94 -25, routes.el +25.
REAL SOURCE: ~2,181 lines (not 70k — the rest was generated dist/*.c).
RISK: higher — touches soul.el + awareness.el (cognitive core).
ORDER: last, as part of the #5/#9 reconciliation.

Handoff (auto) — prereq #16. ⚠ OVERLAPS #9 by ~80%: identical changes to awareness.el (+402 -9), mcp-wrapper/src/main.el (+831), neuron-api.el (+173), mcp-proxy, and the same docs. #5 and #9 are the same underlying work in two giant diffs — they will conflict. RECONCILE with #9 before merging either (see #9; likely #9 is canonical). WHAT real: awareness.el +402, chat.el +184, mcp-wrapper +831, neuron-api.el +173, soul.el +94 -25, routes.el +25. REAL SOURCE: ~2,181 lines (not 70k — the rest was generated dist/*.c). RISK: higher — touches soul.el + awareness.el (cognitive core). ORDER: last, as part of the #5/#9 reconciliation.
will.anderson closed this pull request 2026-06-15 16:38:16 +00:00
will.anderson deleted branch feat/mcp-tool-bridge 2026-06-15 16:38:21 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: neuron-technologies/neuron#5