Tools-rs - Tool Collection and Execution Framework
It's pronounced tools-r-us!!
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
tokiointegration - 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
inventorycrate for zero-runtime-cost discovery - Typed Metadata - Attach
#[tool(key = value)]attributes to tools and read them through a user-definedMtype onToolCollection<M>(see Tool Metadata) - Shared Context - Inject shared resources (
Arc<T>) into tools viactxfirst parameter andToolCollection::builder().with_context(...)(see Shared Context)
Quick Start
use json;
use ;
/// Adds two numbers.
async
/// Greets a person.
async
async
Installation
Add the following to your Cargo.toml:
[]
= "0.3.1"
= { = "1.45", = ["macros", "rt-multi-thread"] }
= "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 (
ToolSchematrait) - Error handling (
ToolError,DeserializationError) - Core data structures (
FunctionCall,ToolRegistration, etc.)
- Tool collection and execution (
- 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.
Compatibility
Rust Version Support
Tools-rs requires Rust 1.85 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:
use ;
/// Return the current date in ISO-8601 format.
async
async
The generated declarations follow proper JSON Schema format:
Manual Registration
While the #[tool] macro provides the most convenient way to register tools, you can also register tools manually for more dynamic scenarios:
use ToolCollection;
use json;
async
Advanced Manual Registration
For complex scenarios with custom types:
use ;
use ;
async
Tool Metadata
#[tool(...)] accepts flat key = value attributes that get stored on each
tool and read back through a user-defined metadata type. This lets a single
tool declaration feed multiple collections, each typed to the schema that
collection cares about — useful for HITL approval gates, cost tiering, and
similar policy concerns that don't belong in the function body.
use Deserialize;
use ;
/// Deletes a file.
async
/// Reads a file (no metadata declared — fields default).
async
Default behavior
ToolCollection defaults to ToolCollection<NoMeta>. NoMeta is an empty
struct that swallows any attributes a tool declared, so existing
collect_tools() callers see no behavioral change. Opt into typed metadata
by writing ToolCollection::<MyMeta>::collect_tools() instead.
Validation
ToolCollection::<M>::collect_tools() fails fast on the first tool whose
attributes don't match M. For CI, two helpers accumulate every failure
across the inventory before returning:
use ;
# ;
Attribute syntax
#[tool(key = "value")]— string#[tool(key = 42)]— integer (negative literals are accepted:-3)#[tool(key = 1.5)]— float#[tool(key = true)]— boolean#[tool(flag)]— bare flag, equivalent toflag = true- Multiple attributes are comma-separated:
#[tool(a = 1, b = "x", flag)]
Attributes are flat-only — nested structures (#[tool(policy = { ... })])
are not supported. Use richer types in runtime metadata, not at the
attribute site. The keys name and description are reserved (the
function name and doc comment supply them).
Programmatic registration with metadata
ToolCollection::register takes a metadata argument. For untyped
collections, pass (); passing () to a typed collection is a compile
error.
# use ToolCollection;
#
let mut tools: = new;
tools.register?;
# Ok::
Shared Context
Tools often need access to shared resources — database connections, HTTP
clients, caches. If a tool's first parameter is named ctx, the macro
treats it as a shared context that the collection injects at call time.
Callers only pass the "real" arguments; ctx never appears in the JSON
schema.
use Arc;
use ;
use NoMeta;
/// Look up a user by name.
async
/// A plain tool — no context needed.
async
async
How it works
- Macro detection — if the first parameter is named
ctx, it is excluded from the JSON schema wrapper struct. The macro rewrites the emitted function signature fromctx: Ttoctx: Arc<T>so that field access and method calls work viaDeref. - Builder —
ToolCollection::builder().with_context(arc).collect()stores theArc<T>and validates at startup that every context-requiring tool expects the same type (viaTypeIdcomparison). A mismatch produces a clearCtxTypeMismatcherror. - Call-time injection —
collection.call(...)passes the stored context into the closure. Non-context tools ignore it.
Important: write ctx: T, not ctx: Arc<T>
The macro wraps the type in Arc automatically. Writing ctx: Arc<T>
produces a compile error to prevent accidental double-wrapping.
Interior mutability
Arc<T> is immutable, but you can wrap an interior-mutable type:
use Mutex;
# use tool;
/// Record a cache hit.
async
The builder receives Arc::new(Mutex::new(cache)). The tool sees
Arc<Mutex<Cache>> via Deref, so ctx.lock() works directly.
Context + metadata
Context and metadata are orthogonal. Combine them freely:
# use Arc;
# use Deserialize;
# use ;
#
/// Drop a table — requires approval.
async
#
Without context
collect_tools() and ToolCollection::collect_tools() still work for
collections that don't need context. If any tool in the inventory requires
context and you call collect_tools() without the builder, it fails with
ToolError::MissingCtx at startup.
ToolsBuilder (Typestate Builder)
ToolsBuilder is a typestate-based builder for ToolCollection that
enforces invariants at compile time. It is the recommended way to
construct collections when you need context or typed metadata.
use Arc;
use Deserialize;
use ;
#
Typestate enforcement
The builder uses typestate to prevent invalid combinations at compile
time. Once with_context is called, the builder transitions to the
Native state where with_context can no longer be called:
use tools_rs::ToolsBuilder;
use std::sync::Arc;
// ERROR: with_context cannot be called twice
let b = ToolsBuilder::new()
.with_context(Arc::new(42_u32))
.with_context(Arc::new(42_u32));
Raw tool registration
register_raw lets you register tools from a pre-built JSON schema
and a raw async closure, bypassing ToolSchema derivation. This is
useful for dynamic tool registration or tools coming from external
sources.
use ;
use json;
# async
Examples
Check out the examples directory for comprehensive sample code:
# Run the basic example - simple tool registration and calling
# Run the function declarations example - LLM integration demo
# Run the schema example - complex type schemas and validation
# Run the newtype demo - custom type wrapping examples
# Run the HITL example - human-in-the-loop approval gating with metadata
# Run the context example - shared context injection
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
- hitl: Human-in-the-loop approval gating using typed metadata
- context: Shared context injection via
ToolCollection::builder().with_context(...)
API Reference
Core Functions
collect_tools()- Discover all tools registered via#[tool]macrofunction_declarations()- Generate JSON schema declarations for LLMscall_tool(name, args)- Execute a tool by name with JSON argumentscall_tool_with(name, typed_args)- Execute a tool with typed argumentscall_tool_by_name(collection, name, args)- Execute tool on specific collectionlist_tool_names(collection)- List all available tool names
Core Types
ToolCollection<M>- Container for registered tools, generic over metadata typeM(defaults toNoMeta)ToolsBuilder<S, M>- Typestate builder for constructing collections with compile-time enforcementCollectionBuilder<M>- Builder for constructing collections with shared contextToolEntry<M>- A single tool entry with its function, declaration, and metadataFunctionCall- Represents a tool invocation with id, name, and argumentsFunctionResponse- Represents the response of a tool invocation with matching id to call, name, and resultToolError- Comprehensive error type for tool operationsNoMeta- Default metadata type that ignores all#[tool(...)]attributesToolSchema- Trait for automatic JSON schema generationToolRegistration- Internal representation of registered toolsFunctionDecl- LLM-compatible function declaration structure
Macros
#[tool]- Attribute macro for automatic tool registration. Accepts flatkey = valueattributes for metadata. Detectsctxas a reserved first parameter for shared context injection.#[derive(ToolSchema)]- Derive macro for automatic schema generation
Error Handling
Tools-rs provides comprehensive error handling with detailed context:
use ;
use json;
async
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
inventorycrate - 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
Resulttypes 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
// 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 call_tool_with;
let result = call_tool_with.await?.result;
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
inventoryis properly collecting tools withcollect_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
ToolSchemaimplementations
Deserialization failures
- Verify JSON arguments match the expected parameter structure
- Check that argument names match function parameter names exactly
- Use
serdeattributes like#[serde(rename = "...")]for custom field names
Async execution issues
- All tool functions must be
async fnwhen using#[tool] - Ensure you're using
tokioruntime for async execution - Tool execution is inherently async - use
.awaitwhen calling tools
Debugging Tips
// Enable debug logging to see tool registration and execution
use ;
let tools = collect_tools;
println!;
// Inspect generated schemas
let declarations = tools.json?;
println!;
Scripting Language Tools (FFI) — Experimental
Tools-rs is being designed to support registering tools written in scripting
languages alongside native Rust tools. The builder infrastructure
(ToolsBuilder, Language, RawToolDef, from_path) is in place, but
language adapters are not yet implemented — calling
ToolsBuilder::new().with_language(...).from_path(...).collect() currently
returns a "not yet implemented" error. See tests/ffi_builder.rs for the
current status.
Planned design
- One language per builder — no collection gymnastics. Want Python and Lua? Build two collections.
- Package manager agnostic — we won't run
pip installornpm install. Users set up their own environment (venv, node_modules, etc). The adapter detects and uses whatever exists. - TypeScript = JavaScript — we will accept
.jsfiles. Users transpile their own TypeScript.
The target API (not yet functional):
use ;
// Python tools (requires `python` feature)
let py_tools = new
.with_language
.from_path
.collect?;
// Lua tools (requires `lua` feature)
let lua_tools = new
.with_language
.from_path
.collect?;
Planned language support
| Language | Feature | Interpreter | Convention |
|---|---|---|---|
| Python | python |
pyo3 | @tool decorator, type hints, docstrings |
| Lua | lua |
mlua | LuaLS --- annotations on named functions |
| JavaScript | js |
boa_engine | Tool object export |
Python convention (planned)
"""Get current weather for a city.
Args:
city: City name to look up
units: Temperature unit (celsius or fahrenheit)
"""
=
return
Lua convention (planned)
--- Get current weather for a city.
--- @param city string City name to look up
--- @param units? string Temperature unit (celsius or fahrenheit)
--- @meta requires_approval false
Planned workspace structure
tools-rs/
├── tools_core/ # Language enum, RawToolDef, builder
├── tools_macros/ # #[tool], ToolSchema
├── ffi/
│ ├── tools_python/ # pyo3 adapter
│ ├── tools_lua/ # mlua adapter
│ └── tools_js/ # boa_engine adapter
├── src/ # tools-rs facade crate
└── examples/
Roadmap
- Core framework —
#[tool]macro,ToolCollection, JSON schema generation - Tool metadata —
#[tool(key = value)]with typedToolCollection<M> - Shared context —
ctxparameter injection viaToolCollection::builder() - Typestate builder —
ToolsBuilderwithBlank/Native/Scriptedstates - Raw registration —
register_raw()for pre-built schemas - FFI foundation —
Languageenum,RawToolDef,from_pathon builder - Python adapter —
ffi/tools_python/with@tooldecorator - Lua adapter —
ffi/tools_lua/withTooltable convention - JavaScript adapter —
ffi/tools_js/withToolobject export
Contributing
We welcome contributions!
Development Setup
# Clone the repository
# Run tests
# Run examples
License
This project is licensed under the MIT License - see the LICENSE file for details.