Skip to content

Model Context Protocol

The Model Context Protocol (MCP) is a standard way to expose tools to LLM applications. An MCP server hosts tools; an MCP client discovers and calls them. Rig agents act as clients through rmcp, the official Rust MCP SDK: each MCP tool becomes a Rig tool the agent can call.

Enable the rmcp feature on rig (native targets only, not WASM), and add rmcp 2 with the client transport you need:

[dependencies]
rig = { version = "0.44.0", features = ["rmcp"] }
rmcp = { version = "2", features = ["client", "transport-streamable-http-client-reqwest"] }
tokio = { version = "1", features = ["full"] }

The integration lives in rig::tool::rmcp. It also re-exports the rmcp crate it was built against as rig::tool::rmcp::rmcp.

The simplest setup fetches the server’s tools once. Wrap each rmcp tool definition in an McpTool (which keeps a handle to the server), turn it into a Rig DynamicTool, and add the tools to the agent:

use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
use rig::tool::DynamicTool;
use rig::tool::rmcp::{McpTool, rmcp::service::ServerSink};
/// `server` is the peer of a connected MCP client (`client.peer().clone()`).
async fn mcp_agent(server: ServerSink) -> anyhow::Result<Agent> {
let tools = server
.list_all_tools()
.await?
.into_iter()
.map(|tool| DynamicTool::try_from(McpTool::from_mcp_server(tool, server.clone())))
.collect::<Result<Vec<_>, _>>()?;
Ok(AgentBuilder::new(OpenAI::from_env()?.completion(openai::GPT_5_5))
.preamble("You can use the tools of an MCP server.")
.dynamic_tools(tools)
.build())
}

rig::tool::rmcp::tools_from_server(tools, &server) does the wrapping step for a whole list. To get the ServerSink, connect a client with rmcp:

use rmcp::ServiceExt;
use rmcp::model::{ClientCapabilities, ClientInfo, Implementation};
use rmcp::transport::StreamableHttpClientTransport;
let client_info = ClientInfo::new(
ClientCapabilities::default(),
Implementation::new("my-agent", "0.1.0"),
);
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8080");
let client = client_info.serve(transport).await?;
let agent = mcp_agent(client.peer().clone()).await?;
let answer = agent.prompt("What is 2 + 5?").await?.output();

Keep client alive while the agent runs: dropping it closes the connection, and the tools stop working.

Live tools: follow the server’s tool list

Section titled “Live tools: follow the server’s tool list”

MCP servers can change their tools at runtime and announce it with notifications/tools/list_changed. McpClientHandler is an rmcp client handler that registers the server’s tools in a shared Rig ToolServer and re-registers them whenever the list changes. Give the agent the same tool server handle:

use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
use rig::tool::rmcp::McpClientHandler;
use rig::tool::server::ToolServer;
use rmcp::model::{ClientCapabilities, ClientInfo, Implementation};
let tool_server = ToolServer::new().run();
let client_info = ClientInfo::new(
ClientCapabilities::default(),
Implementation::new("my-agent", "0.1.0"),
);
let handler = McpClientHandler::new(client_info, tool_server.clone());
// Connects, fetches the initial tool list, and keeps it in sync.
let transport = rmcp::transport::StreamableHttpClientTransport::from_uri("http://localhost:8080");
let mcp = handler.connect(transport).await?;
let agent = AgentBuilder::new(OpenAI::from_env()?.completion(openai::GPT_5_5))
.preamble("You can use the tools of an MCP server.")
.tool_server_handle(tool_server)
.build();
let answer = agent.prompt("What is 2 + 5?").max_turns(2).await?.output();

A refresh only replaces or removes the registrations this handler made, so tools you registered yourself under other names are left alone.

  • Timeouts. Each MCP call has a 5-minute deadline by default (DEFAULT_MCP_TOOL_TIMEOUT). Change it per tool with McpTool::with_timeout(..) or for every tool of a handler with McpClientHandler::with_timeout(..); pass None for no deadline. A timed-out call fails like any other tool error and Rig tries to cancel it on the server.
  • Request metadata. To send MCP _meta with a call without exposing it to the model, put McpMeta(meta) in the call’s ToolContext.
  • Results. The model sees the tool’s content. The full CallToolResult, its structuredContent and its response _meta are published to the tool context’s results (McpCallToolResult, McpStructuredContent, McpResponseMeta) for your hooks and code to read. A result the server marks is_error becomes a failed tool call.
  • Disconnects. Each MCP tool carries a liveness check tied to its connection, so a tool registry can retire tools whose server went away.

rmcp also builds servers. Enable its server features:

[dependencies]
rmcp = { version = "2", features = ["server", "macros", "transport-streamable-http-server"] }
schemars = "1"
serde = { version = "1", features = ["derive"] }

Tools are methods on a type that holds a ToolRouter. #[tool_router] collects the #[tool] methods, and #[tool_handler] wires the router into ServerHandler:

use rmcp::{
ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolResult, ContentBlock, ErrorData, ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct SumRequest {
a: i32,
b: i32,
}
#[derive(Clone)]
struct Calculator {
tool_router: ToolRouter<Calculator>,
}
#[tool_router]
impl Calculator {
fn new() -> Self {
Self { tool_router: Self::tool_router() }
}
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::text((a + b).to_string())]))
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
}
}

Serve it over HTTP with rmcp::transport::streamable_http_server::StreamableHttpService, mounted in hyper or axum.

The rmcp example in the Rig repository runs a server like this and an auto-updating MCP agent in one program.