# Writing custom tools

> Define, register and test a tool before connecting it to an LLM.

A tool exposes a named operation and a JSON input schema to the model. Start with a small deterministic tool so you can verify its behaviour without making a model call.

## A complete tool example

Use the project from [Installation](/docs/polaris/getting-started/installation.md) and add JSON support:

```sh
cargo add serde_json
```

Replace `src/main.rs` with:

```rust
use polaris_ai::tools::{ToolError, ToolRegistry, tool};


#[tool]
/// Add two integers.
async fn add(
    /// First integer.
    left: i64,
    /// Second integer.
    right: i64,
) -> Result<String, ToolError> {
    Ok((left + right).to_string())
}


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut registry = ToolRegistry::new();
    registry.register(add());
    let result = registry
        .execute("add", &serde_json::json!({"left": 2, "right": 3}))
        .await?;
    assert_eq!(result, serde_json::json!("5"));
    println!("{}", result.as_str().unwrap());
    Ok(())
}
```

Run `cargo run`. It should print `5`.

The `#[tool]` macro derives the input schema from the parameter types and uses the doc comments for the descriptions exposed to the model. `add()` constructs the generated tool; `registry.register(add())` makes it available by name. `execute` deserialises the arguments and returns JSON.

The example returns a string, so the JSON result is `"5"`. Do not serialise a JSON object into a string unless that is deliberately the output format: it produces a JSON string containing JSON text, not a structured object.

## Register through a plugin

For an agent, register tools during plugin `build` after `ToolsPlugin` has supplied the registry. The repository’s [FileToolsPlugin](https://github.com/RadiantAILabs/polaris/blob/5ff84646db63589d58bf87956976d749b8c754de/examples/src/plugins/file_tools.rs) is a complete example of dependency declaration, registration and a permission override.

Use `#[toolset]` when several tools share configuration or state; the file-tools example demonstrates that pattern as well. [Writing custom plugins](/docs/polaris/guides/writing-custom-plugins.md) covers the lifecycle.

## Connect tools to the model

Import `LlmRequestBuilderExt` and call `.with_registry(&registry)` on the model request builder to attach definitions. The caller then reads tool calls from the response, executes them and appends the corresponding tool results to the conversation. Follow the [ReAct example](/docs/polaris/guides/building-a-react-agent.md) for the complete loop.

A tool’s permission is metadata for the calling runtime. Direct `ToolRegistry::execute` does **not** perform an interactive confirmation. Check the effective permission and handle Allow, Confirm and Deny before dispatching a model-requested action. The CLI example demonstrates this for file writes.

## Inputs that need execution context

Tools can also declare context-injected parameters. In that case, call `execute_with` with a `ToolContext` containing the required resources. Plain `execute` supplies an empty context, so it cannot satisfy required context parameters. See the [tool reference](https://github.com/RadiantAILabs/polaris/blob/5ff84646db63589d58bf87956976d749b8c754de/docs/reference/tools.md/) for the complete parameter and context API.

## Test the boundary

Test a valid call, malformed arguments, missing inputs and domain-specific failures before attaching a model. For filesystem or network tools, test the allowed scope as part of the tool’s own implementation. Return actionable errors so the agent can distinguish invalid input from a transient service failure.

Keep each tool focused, name it after its action and describe units, formats and constraints in parameter comments. These descriptions are part of the model’s interface.
