Tools
Tools let an agent do more than generate text: they expose your Rust functions to the model so it can call them to fetch data, run computations, or reach out to external systems. When the model decides a tool is needed, Rig parses the call, runs your code, feeds the result back to the model, and continues the loop — turning “just an LLM” into a system that can take actions.
A complete example
Section titled “A complete example”The snippet below defines two tools — an Adder written by hand against the Tool trait and a
Subtract generated from a plain function with the tool_macro — and attaches both to an agent.
use rig::{ client::{CompletionClient, ProviderClient}, completion::{Prompt, ToolDefinition}, providers::openai, tool::Tool,};use serde::{Deserialize, Serialize};use serde_json::json;
#[derive(Deserialize)]struct OperationArgs { x: i32, y: i32,}
#[derive(Debug)]struct MathError;
impl std::fmt::Display for MathError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "math error") }}
impl std::error::Error for MathError {}
// A tool implemented by hand against the `Tool` trait.#[derive(Deserialize, Serialize)]struct Adder;
impl Tool for Adder { const NAME: &'static str = "add";
type Error = MathError; type Args = OperationArgs; type Output = i32;
async fn definition(&self, _prompt: String) -> ToolDefinition { ToolDefinition { name: "add".to_string(), description: "Add x and y together".to_string(), parameters: json!({ "type": "object", "properties": { "x": { "type": "number", "description": "The first number to add" }, "y": { "type": "number", "description": "The second number to add" } }, "required": ["x", "y"] }), } }
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> { Ok(args.x + args.y) }}
// A tool generated from a plain function. The macro creates a tool type// named after the function in PascalCase (`subtract` -> `Subtract`).#[rig::tool_macro(description = "Subtract y from x", required(x, y))]async fn subtract(x: i32, y: i32) -> Result<i32, rig::tool::ToolError> { Ok(x - y)}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let openai = openai::Client::from_env()?;
let calculator = openai .agent("gpt-5.5") .preamble("You are a calculator. Use the provided tools to answer arithmetic questions.") .max_tokens(1024) .tool(Adder) .tool(Subtract) .build();
let answer = calculator.prompt("What is 5 - 2?").await?; println!("{answer}");
Ok(())}3The rest of this page unpacks each piece.
The Tool trait
Section titled “The Tool trait”Every tool implements rig::tool::Tool. The
trait ties together everything the model and the runtime need:
const NAME— a unique identifier the model uses to reference the tool.type Args— aDeserializetype the model’s JSON arguments are parsed into.type Output— what your tool returns on success.type Error— your error type, returned when a call fails.definition(&self, prompt)— returns aToolDefinition(name, description, and a JSON-schemaparametersobject) that is sent to the provider.call(&self, args)— the execution logic.
The parameters schema is what tells the model how to call your tool, so keep the descriptions clear.
Deriving the schema with schemars
Section titled “Deriving the schema with schemars”Writing the parameters JSON by hand is error-prone. The schemars crate can generate it from a Rust
struct instead: derive schemars::JsonSchema and describe each field with a /// doc comment.
#[derive(Deserialize, Serialize, schemars::JsonSchema)]struct OperationArgs { /// The first number to add. x: i32, /// The second number to add. y: i32,}Then generate the schema inside definition instead of writing it out:
async fn definition(&self, _prompt: String) -> ToolDefinition { let parameters = schemars::schema_for!(OperationArgs);
ToolDefinition { name: "add".to_string(), description: "Add x and y together".to_string(), parameters: serde_json::to_value(parameters).unwrap(), }}The tool_macro
Section titled “The tool_macro”For simple tools you rarely need a full trait impl. The tool_macro turns a plain function into a
tool type (named after the function in PascalCase, so subtract becomes Subtract):
#[rig::tool_macro(description = "Perform basic arithmetic operations", required(x, y, operation))]async fn calculator(x: i32, y: i32, operation: String) -> Result<i32, rig::tool::ToolError> { match operation.as_str() { "add" => Ok(x + y), "subtract" => Ok(x - y), "multiply" => Ok(x * y), "divide" if y == 0 => Err(rig::tool::ToolError::ToolCallError("Division by zero".into())), "divide" => Ok(x / y), _ => Err(rig::tool::ToolError::ToolCallError( format!("Unknown operation: {operation}").into(), )), }}The macro derives the argument struct, the JSON schema, and the Tool impl for you. The generated type
is passed to .tool(...) just like a hand-written one.
When a tool call fails
Section titled “When a tool call fails”Your call returning Err does not abort the prompt. Rig converts the error to its string
representation and sends it back to the model as the tool result, and the
agent loop continues — the model reads the error and
can retry with corrected arguments, try a different tool, or explain the failure to the user. Two
consequences for how you write tools:
- Make error messages instructive. The model is the audience.
"Division by zero"lets it recover;"error 500"doesn’t. Say what was wrong and, when you can, what a valid call looks like. - Budget turns for recovery. A retry costs a turn, so a prompt that may need self-correction
wants
.max_turns(...)headroom.
A different failure — the model calling a tool that doesn’t exist — fails the prompt immediately by default; a prompt hook can opt into retry/repair/skip recovery, as covered on the Agents page.
Designing good tools
Section titled “Designing good tools”The model chooses tools by reading their names, descriptions, and parameter schemas — that text is the interface, and most tool-use failures trace back to it rather than to the model.
- Name tools descriptively, in
snake_case, without abbreviations:search_ordersbeatsso. - Write descriptions for the model, not for docs. Say what the tool does, when to use it, and when not to. Recommendations and short examples in the description measurably help.
- Describe every parameter and keep parameters few and primitive. A model fills in three well-described string/number fields far more reliably than one nested object.
- Offer few tools per request. Selection quality degrades as the tool list grows (roughly past the ten-to-twenty range). Split large tool inventories across specialized agents, or use dynamic tool retrieval so each request only sees the relevant few.
- Return compact results. Tool output is prompt input on the next turn: filter and summarize in the tool rather than dumping raw API responses into the context. For large or sensitive data, have the tool return an ID your code resolves later instead of the payload itself.
Attaching tools to an agent
Section titled “Attaching tools to an agent”Tools reach the model in two ways.
Static tools are always available. Add each with .tool(...):
let agent = client .agent("gpt-5.5") .preamble("You are a calculator.") .tool(Adder) .tool(Subtract) .build();Dynamic tools are retrieved from a vector store at prompt time based on the user’s query — useful when you have many tools and don’t want to send every definition on every request. See the next section.
Tool-RAG with ToolEmbedding
Section titled “Tool-RAG with ToolEmbedding”To make a tool retrievable by semantic similarity, also implement
ToolEmbedding. It provides the
text that gets embedded (embedding_docs) plus the state/context needed to reconstruct the tool:
use rig::tool::ToolEmbedding;
#[derive(Debug)]struct InitError;
impl std::fmt::Display for InitError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "init error") }}
impl std::error::Error for InitError {}
impl ToolEmbedding for Adder { type InitError = InitError; type Context = (); type State = ();
fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> { Ok(Adder) }
fn embedding_docs(&self) -> Vec<String> { vec!["Add x and y together".into()] }
fn context(&self) -> Self::Context {}}Embed the tools into a VectorStoreIndex, then attach them with .dynamic_tools(n, index, toolset).
At each turn the agent fetches the n most relevant tools and offers only those to the model:
let agent = client .agent("gpt-5.5") .preamble("You are a calculator.") .dynamic_tools(2, tool_index, toolset) .build();For a full walkthrough of building the index and toolset, see Vector Stores & RAG and
the rag_dynamic_tools example in the Rig repository.
Tool servers
Section titled “Tool servers”Sharing a mutable tool set across async tasks usually means reaching for Arc<Mutex<T>>, which can
cause lock contention or deadlocks. Rig’s tool server (available since v0.22.0) avoids this: it runs
the tools in a dedicated Tokio task and communicates with them over message passing. As long as one copy
of the handle is alive, the server keeps running.
use rig::tool::server::{ToolServer, ToolServerHandle};
let tool_server: ToolServerHandle = ToolServer::new() .tool(Adder) .run();Like agents, tool servers accept static tools, dynamic tools, and MCP tools (via rmcp). Attach the
handle with AgentBuilder::tool_server_handle(...); handing several agents clones of one handle lets
them share a single set of tools.
MCP tools
Section titled “MCP tools”Tools don’t have to live in your crate. The Model Context Protocol
lets an agent consume tools served by external processes — filesystem access, browsers, databases,
third-party SaaS integrations — through one standard interface. Rig connects to MCP servers via the
rmcp crate and exposes their tools with AgentBuilder::rmcp_tool(...) / rmcp_tools(...), side by
side with your native tools. See
Model Context Protocol for setup and a worked example.
Tool organization
Section titled “Tool organization”Tools are collected in a ToolSet, which registers tools, looks them up by name, routes calls to the
right implementation, and (for ToolEmbedding tools) produces the embeddings used for dynamic retrieval.
When you attach tools to an agent, Rig handles the rest of the integration: converting each
ToolDefinition into the provider’s format, parsing model output into tool calls, executing them, and
returning results to the model.
See also
Section titled “See also”- Agents — attach tools and run the multi-turn loop
- Vector Stores & RAG — the retrieval layer behind dynamic tools
- Model Context Protocol — expose external MCP tools to an agent
