Skip to content

Per-server typed bridge tools for MCP (replace generic mcp_invoke_tool) #420

@rockfordlhotka

Description

@rockfordlhotka

Problem

The current generic mcp_invoke_tool(server_name, tool_name, arguments) surface is unreliable when LLMs need to call MCP tools with non-trivial arguments. Observed failure mode (rockbot-agent-5b48d44999-kk9sq, patrol run 2026-05-19):

  • Subagent loaded calendar-mcp/send_email schema via mcp_get_service_details (which lists to/subject/body as required).
  • 30 seconds later, called mcp_invoke_tool for send_email with only server_name and tool_name — no nested arguments, no flat fields, nothing.
  • MCP server returned 'to' was not provided, which sounds like the tool is broken rather than the call shape is wrong.
  • Subagent rationalized: "mcp_invoke_tool itself doesn't carry the nested email arguments in this tool surface" and pivoted to a spawn_wisps Direct/Mcp step — which succeeded because the wisp JSON literal hard-codes the params shape.

This is the self-deceptive failure loop called out in design/self-repair.md. The agent saved its rationalization to working memory ("prior direct mcp_invoke_tool calls lost/ignored parameters"), so future runs treat the lie as fact and pre-emptively detour through wisps. We've now seen this on calendar-mcp/get_calendar_events, calendar-mcp/get_email_details, and calendar-mcp/send_email.

Note: the framework's mcp_invoke_tool path is not actually broken — list_accounts (no-arg) succeeded through the exact same surface in the same trace. The problem is that LLMs struggle to author the nested arguments payload for the generic wrapper.

Proposed direction

Replace the generic mcp_invoke_tool surface with dynamically-registered per-tool wrapper tools. After mcp_get_service_details (or eagerly at server registration), the framework registers tools shaped like:

calendar-mcp__send_email(to: string[], subject: string, body: string, accountId?: string, ...)
calendar-mcp__list_accounts()
calendar-mcp__get_calendar_events(timeZone: string, startDate: string, ...)

Each wrapper carries the inner MCP tool's JSON schema as its own parameter schema, flattened. The LLM sees typed, named parameters — same shape that Claude Desktop, Cursor, Continue, and other working MCP hosts expose. It becomes nearly impossible to emit an empty call: the model has to fill the declared parameters or omit only optional ones.

Open design questions

  1. Tool-count blow-up. Each MCP server exposes 5–30 tools; 5 servers = 50–150 wrappers in the tool list. Need a story for what gets registered eagerly vs. lazily — possibly mcp_list_services returns the wrappers per-server and mcp_get_service_details activates them in the session's tool registry.
  2. Schema freshness. MCP tool schemas can change when the server is upgraded. Need cache invalidation on server reconnect.
  3. Naming. calendar-mcp__send_email vs. mcp_calendar_mcp_send_email vs. some other separator. Has to be stable, parseable, and not collide with built-in tools.
  4. Backwards compat. Keep mcp_invoke_tool as a fallback escape hatch (for tools we haven't pre-registered, or for prompt-text-driven cases), or rip it out entirely.
  5. Wisp interaction. Wisp Direct/Mcp gateway currently routes through mcp_invoke_tool under the hood. Probably keep routing wisps through the generic path since wisp JSON already carries the shape correctly; or rewrite the router to call the typed wrapper.
  6. Recovery. McpRecoveryExecutor currently runs on mcp_invoke_tool results. Confirm it still triggers per-wrapper or needs refactor.
  7. MCP changelog/tool discovery race. When a new MCP server is registered mid-session, the wrappers need to appear in the LLM's tool list before the next iteration — currently the tool list is built once per turn.
  8. Token budget. 50–150 extra tool descriptions in every prompt is a real cost. The eager-vs-lazy decision dominates this.

Related

  • #(this PR series) — pending typed schema-error nudge that addresses the symptom inside McpManagementExecutor without a full refactor. That's a fine stopgap; this issue covers the durable fix.
  • design/self-repair.md — documents the rationalization-loop pattern this issue eliminates.
  • Patrol-directive working memory entry warning that direct mcp_invoke_tool "loses arguments" needs eviction once the typed wrappers ship.

Acceptance criteria (draft)

  • Design doc landing in design/ covering open questions 1–8 above with chosen answers.
  • LLM call sites no longer emit mcp_invoke_tool for tools with required parameters; they emit the typed wrapper.
  • Trace of a send_email call from a subagent shows the typed-wrapper schema + populated arguments in callContent.Arguments.
  • Existing wisp Direct/Mcp pipelines keep working unchanged.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions