Skip to content

Improve RMCP tool registration ergonomics around RunningService lifetimes #2200

Description

@gold-silver-copper

Summary

Manual RMCP tool registration currently makes it easy to accidentally drop the RunningService while keeping only a cloned peer/sink in Rig's tool registry. The resulting behavior is confusing: tools can appear available immediately after setup, then disappear later when the agent is stored in application state and prompted again.

Related bug/repro issue: #2199
Broken user gist: https://gist.github.com/arcayr/1ebb21ee4fe80d5d3fa3f454c3f2f578
Corrected lifetime example: https://gist.github.com/gold-silver-copper/46bee9339c5d0b1f33c03394feece4b7

Problem

This shape looks reasonable at first glance:

let srv = client_info.clone().serve(transport).await?;

for tool in srv.list_all_tools().await? {
    tool_server = tool_server.rmcp_tool(tool, srv.peer().clone());
}

let handle = tool_server.run();
let agent = client.agent(model).tool_server_handle(handle).build();

But srv.peer().clone() does not keep srv alive. RunningService owns the live RMCP connection/task. When srv falls out of scope, RMCP closes/cancels the transport. Rig later observes the peer sink as closed and retires the MCP tool registration.

The first prompt can still appear to work because shutdown is asynchronous, so this presents as an intermittent or state-hoisting bug rather than an obvious lifetime issue.

Why this is ergonomic/documentation debt

The current API accepts the exact thing users naturally have at registration time: a Tool plus a cloned peer/sink. That makes it easy to miss that another value, the RunningService, must be owned somewhere else for the registered tool to remain usable.

The intended long-lived app shape is closer to:

struct AppModel {
    agent: Agent<...>,
    mcp_services: Vec<rmcp::service::RunningService<rmcp::service::RoleClient, ClientInfo>>,
}

For dynamic/server-backed RMCP tools, the McpClientHandler + ToolServerHandle path also makes this clearer because connect(...) returns the service that must be retained.

Possible improvements

A few options, not mutually exclusive:

  • Document prominently on ToolServer::rmcp_tool, ToolServer::rmcp_tool_with_timeout, AgentBuilder::rmcp_tool, and AgentBuilder::rmcp_tools that the caller must retain the RunningService backing the peer.
  • Add an example showing manual registration from list_all_tools() with a struct that stores both Agent and Vec<RunningService<...>>.
  • Consider renaming docs/parameters from client/peer to something that hints it is only a sink handle, not the connection owner.
  • Consider a higher-level helper that returns both a tool-server handle update and a guard/service value that must be retained.
  • Consider a diagnostic log when directly registered RMCP tools are retired because their peer sink is closed, including a hint about keeping RunningService alive.

Acceptance Criteria

Users following docs/examples for runtime RMCP tool registration should understand that peer().clone() is insufficient to own the connection lifetime, and should have an obvious pattern for storing the returned RunningService alongside their app model/agent state.

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