Skip to content

feat: derive per-call MCP _meta from the chosen tool call #2442

Description

@aimable100
  • I have looked for existing issues (including closed) about this

Feature Request

Allow per-call MCP _meta to be derived from the tool call the model chose.

The _meta channel added in #1954 and kept through #2141 and #2398 is read from the run's ToolContext at dispatch time: context.get::<rmcp::model::Meta>().cloned() at rig-agent/src/tool/rmcp.rs:487 on 0.42.0, and crates/rig-rmcp/src/native.rs:478 on main, then request.meta = meta. That context is set once by the caller through PromptRequest::tool_context before the model has picked a tool or its arguments. Metadata that has to be computed from the call itself has no way onto the request.

The pre-tool hook sees the call but cannot contribute to it. on_tool_call(&self, &HookContext, ToolCall<'_>) receives tool_name, tool_call_id, internal_call_id, and args, and returns ToolCallAction::{Run, Rewrite(Value), Skip(String), Stop(String)}. No hook method receives a mutable ToolContext; the only context exposed to hooks is the immutable tool_context on the result event.

Motivation

Some metadata is inherently per call because it binds to the arguments:

  • Request signing. A signature or proof of possession over (tool, arguments, time) lets the MCP server verify that the caller authorized this call, and a captured signature cannot be replayed with different arguments. This is my case (Tenuo warrants); it is the same shape as any HMAC-signed request.
  • Idempotency keys derived from the argument set, so retries of one logical call coalesce downstream.
  • Trace or budget context that depends on which tool was chosen.

A run-level Meta covers auth tokens, session ids, and A2A context_id/task_id, which is what #1536 asked for and what #1954 delivered. It cannot cover a value that does not exist until the model decides.

Current workaround: implement the MCP tool by hand (impl Tool), compute the metadata inside call() from the deserialized arguments, and drive the rmcp client directly, setting CallToolRequestParams::meta. That works, but it gives up the registration path (rmcp_tools() on 0.42; PortableDynamicTool on main), tools/list_changed reconciliation through McpClientHandler, and the result preservation in preserve_mcp_result, and it has to be repeated for every tool.

Prior art

In this repo:

Elsewhere:

  • LangChain's @wrap_tool_call middleware receives a ToolCallRequest and a handler, runs after the model selects a tool and before it executes, and can inspect or modify the call. Tools also read runtime.tool_call_id from ToolRuntime.
  • pydantic-ai passes a per-call RunContext carrying tool_call_id and tool_name (no arguments), and tools have a prepare(RunContext, ToolDefinition) -> ToolDefinition | None step.

Describe the solution you'd like

Smallest first. Any one closes the gap.

A. Per-call context from the hook. Let on_tool_call contribute values merged into that call's dispatch snapshot. Sketch, not a proposal for exact naming:

async fn on_tool_call(&self, ctx: &HookContext, event: ToolCall<'_>) -> ToolCallAction {
    let meta = self.sign(event.tool_name, event.args);      // computed from the chosen call
    ToolCallAction::Run.with_context(|c: &mut ToolContext| { c.insert(meta); })
}

This generalizes beyond MCP, since sub-agents and native tools read the same context, and leaves the Meta forwarding in rig-rmcp unchanged.

B. A per-call meta provider on the MCP registration. Narrower, MCP only:

trait McpMetaProvider: Send + Sync {
    fn meta_for(&self, tool: &str, arguments: Option<&JsonObject>, ctx: &ToolContext) -> Option<Meta>;
}
McpTool::from_mcp_server(..).with_meta_provider(provider)

Falls back to the context Meta on None, so it is additive.

C. Land #2278 and document ExecuteTools as the supported path. Works for hand-driven loops today; the cost is the hook lifecycle.

I'd suggest A, with B as a reasonable narrow alternative. Happy to open a PR for either once there is agreement on the shape.

Reproduction

Register any MCP tool via the portable path and try to set a Meta whose value depends on arguments. The only place to set it is before prompt() runs. An end-to-end example of the hand-written workaround (a Rig agent, a guarded MCP tool, and an rmcp server verifying per-call _meta) is at https://github.com/tenuo-ai/tenuo-rig-demo — see src/tools/incident_mcp.rs for the tool and src/bin/incident_mcp_server.rs for the server.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions