# Tools-rs - Tool Collection and Execution Framework
*It's pronounced tools-r-us!!*
[](https://crates.io/crates/tools-rs)
[](https://docs.rs/tools-rs)
[](https://opensource.org/licenses/MIT)
Tools-rs is a framework for building, registering, and executing tools with automatic JSON schema generation for Large Language Model (LLM) integration.
## Features
- **Automatic Registration** - Use `#[tool]` to automatically register functions with compile-time discovery
- **JSON Schema Generation** - Automatic schema generation for LLM integration with full type information
- **Type Safety** - Full type safety with JSON serialization at boundaries, compile-time parameter validation
- **Async Support** - Built for async/await from the ground up with `tokio` integration
- **Error Handling** - Comprehensive error types with context and proper error chaining
- **LLM Integration** - Export function declarations for LLM function calling APIs (OpenAI, Anthropic, etc.)
- **Manual Registration** - Programmatic tool registration for dynamic scenarios
- **Inventory System** - Link-time tool collection using the `inventory` crate for zero-runtime-cost discovery
## Quick Start
```rust
use serde_json::json;
use tools_rs::{collect_tools, FunctionCall, tool};
#[tool]
/// Adds two numbers.
async fn add(pair: (i32, i32)) -> i32 {
pair.0 + pair.1
}
#[tool]
/// Greets a person.
async fn greet(name: String) -> String {
format!("Hello, {name}!")
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tools = collect_tools();
let sum = tools
.call(FunctionCall {
name: "add".into(),
arguments: json!({ "pair": [3, 4] }),
})
.await?;
println!("add → {sum}"); // Outputs: "add → 7"
let hi = tools
.call(FunctionCall {
name: "greet".into(),
arguments: json!({ "name": "Alice" }),
})
.await?;
println!("greet → {hi}"); // Outputs: "greet → \"Hello, Alice!\""
// Export function declarations for LLM APIs
let declarations = tools.json()?;
println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);
Ok(())
}
```
## Installation
Add the following to your `Cargo.toml`:
```toml
[dependencies]
tools-rs = "0.1.1"
tokio = { version = "1.45", features = ["macros", "rt-multi-thread"] }
serde_json = "1.0"
```
## Crate Structure
The tools-rs system is organized as a Rust workspace with three main crates:
- **tools-rs**: Main entry point, re-exports the most commonly used items
- **tools_core**: Core runtime implementation including:
- Tool collection and execution (`ToolCollection`)
- JSON schema generation (`ToolSchema` trait)
- Error handling (`ToolError`, `DeserializationError`)
- Core data structures (`FunctionCall`, `ToolRegistration`, etc.)
- **tools_macros**: Procedural macros for tool registration:
- `#[tool]` attribute macro for automatic registration
- `#[derive(ToolSchema)]` for automatic schema generation
- **examples**: Comprehensive examples demonstrating different use cases
For more details about the codebase organization, see [CODE_ORGANIZATION.md](CODE_ORGANIZATION.md).
## Compatibility
### Rust Version Support
Tools-rs requires **Rust 1.70** or later and supports:
- Automatically generate JSON schemas for LLM consumption
- Execute tools safely with full type checking
- Handle errors gracefully with detailed context
## Function Declarations for LLMs
Tools-rs can automatically generate function declarations suitable for LLM APIs:
```rust
use tools_rs::{function_declarations, tool};
#[tool]
/// Return the current date in ISO-8601 format.
async fn today() -> String {
chrono::Utc::now().date_naive().to_string()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate function declarations for an LLM
let declarations = function_declarations()?;
// Use in API request
let llm_request = serde_json::json!({
"model": "gpt-4o",
"messages": [/* ... */],
"tools": declarations
});
Ok(())
}
```
The generated declarations follow proper JSON Schema format:
```json
[
{
"description": "Return the current date in ISO-8601 format.",
"name": "today",
"parameters": {
"properties": {},
"required": [],
"type": "object"
}
}
]
```
## Manual Registration
While the `#[tool]` macro provides the most convenient way to register tools, you can also register tools manually for more dynamic scenarios:
```rust
use tools_rs::ToolCollection;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tools = ToolCollection::new();
// Register a simple tool manually
tools.register(
"multiply",
"Multiplies two numbers",
|args: serde_json::Value| async move {
let a = args["a"].as_i64().unwrap_or(0);
let b = args["b"].as_i64().unwrap_or(0);
Ok(json!(a * b))
}
)?;
// Call the manually registered tool
let result = tools.call(tools_rs::FunctionCall {
name: "multiply".to_string(),
arguments: json!({"a": 6, "b": 7}),
}).await?;
println!("6 * 7 = {}", result);
Ok(())
}
```
### Advanced Manual Registration
For complex scenarios with custom types:
```rust
use tools_rs::{ToolCollection, ToolSchema};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ToolSchema)]
struct Calculator {
operation: String,
operands: Vec<f64>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tools = ToolCollection::new();
tools.register(
"calculate",
"Performs arithmetic operations on a list of numbers",
|input: Calculator| async move {
match input.operation.as_str() {
"sum" => Ok(input.operands.iter().sum::<f64>()),
"product" => Ok(input.operands.iter().product::<f64>()),
"mean" => Ok(input.operands.iter().sum::<f64>() / input.operands.len() as f64),
_ => Err(format!("Unknown operation: {}", input.operation)),
}
}
)?;
Ok(())
}
```
## Examples
Check out the [examples directory](examples/) for comprehensive sample code:
```bash
# Run the basic example - simple tool registration and calling
cargo run --example basic
# Run the function declarations example - LLM integration demo
cargo run --example function_declarations
# Run the schema example - complex type schemas and validation
cargo run --example schema
# Run the newtype demo - custom type wrapping examples
cargo run --example newtype_demo
```
Each example demonstrates different aspects of the framework:
- **basic**: Simple tool registration with `#[tool]` and basic function calls
- **function_declarations**: Complete LLM integration workflow with JSON schema generation
- **schema**: Advanced schema generation for complex nested types and collections
- **newtype_demo**: Working with custom wrapper types and serialization patterns
## API Reference
### Core Functions
- `collect_tools()` - Discover all tools registered via `#[tool]` macro
- `function_declarations()` - Generate JSON schema declarations for LLMs
- `call_tool(name, args)` - Execute a tool by name with JSON arguments
- `call_tool_with(name, typed_args)` - Execute a tool with typed arguments
- `call_tool_by_name(collection, name, args)` - Execute tool on specific collection
- `list_tool_names(collection)` - List all available tool names
### Core Types
- `ToolCollection` - Container for registered tools with execution capabilities
- `FunctionCall` - Represents a tool invocation with name and arguments
- `ToolError` - Comprehensive error type for tool operations
- `ToolSchema` - Trait for automatic JSON schema generation
- `ToolRegistration` - Internal representation of registered tools
- `FunctionDecl` - LLM-compatible function declaration structure
### Macros
- `#[tool]` - Attribute macro for automatic tool registration
- `#[derive(ToolSchema)]` - Derive macro for automatic schema generation
## Error Handling
Tools-rs provides comprehensive error handling with detailed context:
```rust
use tools_rs::{ToolError, collect_tools, FunctionCall};
use serde_json::json;
#[tokio::main]
async fn main() {
let tools = collect_tools();
match tools.call(FunctionCall {
name: "nonexistent".to_string(),
arguments: json!({}),
}).await {
Ok(result) => println!("Result: {}", result),
Err(ToolError::FunctionNotFound { name }) => {
println!("Tool '{}' not found", name);
},
Err(ToolError::Deserialize(err)) => {
println!("Deserialization error: {}", err.source);
},
Err(e) => println!("Other error: {}", e),
}
}
```
## Performance Considerations
### Schema Caching
- JSON schemas are generated once and cached.
- Schema generation has minimal runtime overhead after first access
- Primitive types use pre-computed static schemas for optimal performance
### Tool Discovery
- Tool registration happens at compile-time via the `inventory` crate
- Runtime tool collection (`collect_tools()`) is a zero-cost operation
- Tools are stored in efficient hash maps for O(1) lookup by name
### Execution Performance
- Tool calls have minimal overhead beyond JSON serialization/deserialization
- Async execution allows for concurrent tool invocation
- Error handling uses `Result` types to avoid exceptions and maintain performance
### Memory Usage
- Tool metadata is stored statically with minimal heap allocation
- JSON schemas are shared across all instances of the same type
- Function declarations are generated on-demand and can be cached by the application
### Optimization Tips
```rust
// Reuse ToolCollection instances to avoid repeated discovery
let tools = collect_tools(); // Call once, reuse multiple times
// Generate function declarations once for LLM integration
let declarations = function_declarations()?;
// Cache and reuse declarations across multiple LLM requests
// Use typed parameters to avoid repeated JSON parsing
use tools_rs::call_tool_with;
let result = call_tool_with("my_tool", &my_typed_args).await?;
```
## Troubleshooting
### Common Issues
**Tool not found at runtime**
- Ensure the `#[tool]` macro is applied to your function
- Verify the function is in a module that gets compiled (not behind unused feature flags)
- Check that `inventory` is properly collecting tools with `collect_tools()`
**Schema generation errors**
- Ensure all parameter and return types implement `ToolSchema`
- For custom types, add `#[derive(ToolSchema)]` to struct definitions
- Complex generic types may need manual `ToolSchema` implementations
**Deserialization failures**
- Verify JSON arguments match the expected parameter structure
- Check that argument names match function parameter names exactly
- Use `serde` attributes like `#[serde(rename = "...")]` for custom field names
**Async execution issues**
- All tool functions must be `async fn` when using `#[tool]`
- Ensure you're using `tokio` runtime for async execution
- Tool execution is inherently async - use `.await` when calling tools
### Debugging Tips
```rust
// Enable debug logging to see tool registration and execution
use tools_rs::{collect_tools, list_tool_names};
let tools = collect_tools();
println!("Registered tools: {:?}", list_tool_names(&tools));
// Inspect generated schemas
let declarations = tools.json()?;
println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);
```
## Contributing
We welcome contributions!
### Development Setup
```bash
# Clone the repository
git clone https://github.com/EggerMarc/tools-rs
cd toors
# Run tests
cargo test
# Run examples
cargo run --example basic
```
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.