llm-tool
Framework-agnostic Rust tool definitions for LLM agents.
Write plain Rust functions. Get typed LLM tools with JSON Schemas, automatic deserialization, and instant MCP server support.
Why llm-tool?
- Zero Boilerplate:
#[llm_tool]on a function → typed tool with JSON Schema. - Strongly Typed: Parameters are validated. Missing or extra arguments are caught instantly.
- Framework Agnostic: Use the
ToolRegistryto get JSON Schemas for any LLM SDK (OpenAI, Anthropic, Gemini, …). - MCP Ready: Spin up a fully compliant MCP server in 3 lines with
llm-tool-mcp. no_stdCompatible: Core types work in embedded and WASM targets.
⚡ Quick Start
[]
= "0.10.1"
= "0.10.1" # Optional: for MCP server support
Define a Tool
Doc comments become tool and parameter descriptions automatically.
use ;
/// Fetches the current weather for a given location.
async
// Register it! The macro generated a `GetWeather` struct for us.
let registry = new.with_tool;
// You can now extract the JSON schema for any LLM SDK...
let definitions = registry.definitions;
assert_eq!;
// ...or execute calls directly from JSON arguments!
# block_on;
🚀 MCP: Tools, Prompts, and Resources
Use llm-tool-mcp to expose everything over the Model Context Protocol.
# use ;
/// A Tool for the LLM to execute.
/// A Prompt template for the LLM to use.
/// A Resource for the LLM to read.
// Register tools in the ToolRegistry.
let registry = new.with_tool;
assert_eq!;
// Prompts and Resources are registered via llm-tool-mcp's builder:
// McpServer::builder("my-server", "1.0", registry)
// .with_prompt(CodeReview)
// .with_resource(GetConfig)
// .build();
🧠 Features
Return Types & Error Handling
Return Result<T, E> or just T. The ? operator works out of the box.
- Auto-Serialization: Return any
T: Serialize→ automatic JSON response. - Structured Metadata: Attach hidden metadata to
ToolOutputorToolError(logged but not sent to the LLM).
Context
Add ctx: &ToolContext to any tool function to access shared state,
conversation IDs, or typed extensions — automatically hidden from the JSON
Schema.
Custom Descriptions
Override doc comments with an inline string — no extra features needed:
# use ;
async
With the md-tmpl feature, you can also load descriptions from .tmpl.md
template files (description_file = "..."), with compile-time variable
substitution and validation. See the md-tmpl docs
for details.
Middle-Out Truncation & Observation Recall
When tools return massive payloads (e.g. file dumps, logs, shell output),
oversized returns can blow context budgets. llm-tool provides safe middle-out
truncation that bounds context while preserving both initial context and the
latest results or errors:
use ;
let policy = new
.with_head_ratio
.with_alignment;
let truncated = truncate_middle;
assert!;
Observation Spilling
To prevent irrecoverable data loss on non-idempotent tools, attach an
[OutputSpillSink]. The full output is persisted before truncation, and a typed
[SpillRef] handle is embedded into the marker for downstream recall tools:
use Arc;
use ;
;
let policy = new
.with_spill_sink;
let payload = "A".repeat;
let outcome = policy.truncate_detailed.unwrap;
assert!;
assert_eq!;
Spill Failure Semantics
Retention can fail — a bounded store evicts, a disk fills, a policy rejects. Losing that fact silently would be worse than losing the bytes, because the model would read a truncation marker that promises a recall handle which does not exist. Two APIs, two deliberate behaviours:
| API | On spill failure |
|---|---|
[TruncationPolicy::truncate_detailed] |
Returns Err(SpillError); the caller decides. |
[TruncationPolicy::truncate] |
Never fails. Logs the error and renders it into the marker, so the model is told the omitted bytes are unrecoverable. |
The in-band warning is appended even to custom marker templates that do not
mention it, and truncation never sacrifices the marker to fit max_bytes —
content is dropped first, so a byte budget too small for the full marker still
yields an explicit notice rather than a bare slice that looks like complete
output.
Prompt-Injection Prevention via md-tmpl Response Templates
Untrusted tool outputs (web pages, user files, logs) can contain LLM control
delimiters (<|im_start|>, <tool_call>, </tool_response>, [INST]) or
forged XML/fence delimiters designed to break out of the observation turn.
Sanitize untrusted fields directly in the response / response_file md-tmpl
template using | sanitize_tokens, | quarantine, | fence, | escape_xml,
and | escape_json:
use ;
/// Fetch untrusted external content with template-level sanitization.
# block_on;
Tool Effects & Safe Parallel Batch Dispatch
Classify tools with
#[llm_tool(effect = "read_only" | "mutating" | "destructive")] (alias
idempotent = true maps to ReadOnly). ToolRegistry::dispatch_batch executes
batches concurrently with [BatchExecutionMode::Parallel] while automatically
blocking Destructive tools unless explicitly confirmed via
[BatchExecutionMode::ParallelAllowDestructive] or [AllowDestructiveBatch]:
use ;
/// Drop a database table.
let registry = new.with_tool;
assert_eq!;
assert!;
Documentation
llm-tool— Return types, tool context, metadata, template descriptions.llm-tool-mcp— MCP transports, stdio/TCP, routing.md-tmpl— Template syntax, env variables, response templates.
License
Dual-licensed under Apache-2.0 OR MIT.