Tools
Tools are the way Forge agents act on the world. Every tool is registered once, classified by tier, gated by capability, and approved per-call.
Registering a tool
use forge::prelude::*;
let registry = ToolRegistry::new();
registry.register(FnToolExecutor::new("get_weather", |input: serde_json::Value| async move {
let city = input["city"].as_str().unwrap_or("Mountain View");
let weather = fetch_weather(city).await?;
Ok(serde_json::json!({ "temperature_f": weather.temp_f, "conditions": weather.conditions }))
}));
FnToolExecutor::new(name, handler) is the simplest path. For richer
behavior, implement the ToolExecutor trait:
#[async_trait]
pub trait ToolExecutor: Send + Sync {
fn name(&self) -> &str;
fn definition(&self) -> ToolDefinition;
async fn execute(&self, input: serde_json::Value) -> ForgeResult<serde_json::Value>;
}
ToolDefinition
What the model sees:
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value, // JSON Schema
pub tier: ToolTier,
pub required_scopes: Vec<String>,
}
The registry produces these from your registered executors. Pass them to the
agent via AgentConfig::with_tool_registry(®istry).
Three tiers
pub enum ToolTier {
Read, // observes; safe by default in many policies
Write, // modifies state; requires explicit approval
Execute, // runs external code; requires explicit policy + scope
}
Each approval handler maps tiers to its own policy. The default handlers:
| Handler | Read | Write | Execute |
|---|---|---|---|
AutoApprove |
grant | grant | grant |
RejectAll |
reject | reject | reject |
TierPolicy |
grant | review | review |
AutoApprove is dev only.
Approval handlers
#[async_trait]
pub trait ApprovalHandler: Send + Sync {
async fn approve(
&self,
tool_name: &str,
input: &serde_json::Value,
tier: ToolTier,
) -> ApprovalDecision;
}
pub enum ApprovalDecision {
Granted,
Rejected { reason: String },
DeferredToHuman { request_id: String },
}
A custom handler for a chat product might post to a Slack channel and wait for a thumbs-up:
struct SlackApproval { client: SlackClient }
#[async_trait]
impl ApprovalHandler for SlackApproval {
async fn approve(
&self,
tool_name: &str,
input: &serde_json::Value,
tier: ToolTier,
) -> ApprovalDecision {
if matches!(tier, ToolTier::Read) {
return ApprovalDecision::Granted;
}
let request_id = self.client.post_approval_request(tool_name, input).await?;
ApprovalDecision::DeferredToHuman { request_id }
}
}
DeferredToHuman parks the tool call until a submit_approval(request_id, decision)
arrives via the harness adapter or another channel. The agent's loop pauses
on that invocation; other invocations continue.
Execution path
When the model emits a tool call, Forge:
- Looks up the executor in the registry by name
- Checks the agent's ACT contains all
required_scopesfor the tool - Calls the approval handler
- If granted, calls
executor.execute(input) - Records a
ToolInvocationRecord(success / failed / timeout / rejected) - Appends the result to the message list for the next iteration
Each step has a configurable timeout (StreamingLoopConfig::tool_timeout).
Built-in executors
The forge-tool crate ships:
FnToolExecutor— wrap an async closureMockToolExecutor— for testsBuilderExecutor— for tool definitions whose execution lives elsewhere (e.g., MCP, harness-rendered tools)
For HTTP-style tools, see forge-web
which provides web-fetch primitives wired into the registry.
MCP tools
Tools served by an MCP server can be auto-registered:
use forge::mcp::{MCPClient, register_mcp_tools};
let mcp = MCPClient::connect("stdio:./my-mcp-server").await?;
register_mcp_tools(&mcp, ®istry).await?;
See MCP integration.
Next
- Capabilities — ACT scopes that gate tool calls
- Agent events —
ToolInvocationRecordreference - Provider Capability Matrix — which providers support tool calls