toolguard
README.md
# ToolGuard π‘οΈ
**A sub-50Β΅s Rust library and proxy that auto-repairs LLM tool calls and turns schema failures into model-readable self-correction prompts.**
[](https://crates.io/crates/toolguard-core)
[](https://docs.rs/toolguard-core)
[](LICENSE-MIT)
```text
Agent / Client βββ> [ ToolGuard ] βββ> Tool / MCP Server
β
Safe Coercionβ(e.g., "10" -> 10, ["tag"] from "tag")
Audit Logs βOTel Spans (gen_ai.tool.name)
Actionable Rejection (Sent back to model for instant retry)
```
---
## β‘ The Problem: Tool Calling Breaks in Trivial Ways
When Large Language Models call external tools or APIs, they frequently introduce slight schema mismatches:
- Passing numeric arguments as strings: `{"limit": "25"}` instead of `{"limit": 25}`.
- Passing single scalars instead of arrays: `{"tags": "rust"}` instead of `{"tags": ["rust"]}`.
- Capitalization or minor typos in enum values: `{"status": "ACTIVE"}` instead of `"active"`.
- Injecting hallucinated keys not permitted by `additionalProperties: false`.
When these calls hit standard tool servers or APIs, the server crashes with a cryptic stack trace or a 50-line raw JSON Schema validator error dump. The agent gets confused, burns context window tokens, and often enters an infinite failure loop.
**ToolGuard solves this directly at the boundary.**
1. **Auto-Repairs Safe Calls in Microseconds:** If an argument can be safely converted without losing semantic intent, ToolGuard coerces it instantly (median latency: **~25 Β΅s**) and attaches an audit flag to OpenTelemetry spans.
2. **Actionable Model-Readable Rejections:** If a call is genuinely broken (e.g. missing required fields), ToolGuard short-circuits execution and returns a concise, model-readable diff teaching the LLM how to self-correct on its immediate next turn.
3. **Loop & Policy Protection:** Automatically detects infinite identical tool-call loops and enforces strict mode on destructive tools.
---
## π Quickstart
### Option A: Transparent MCP Proxy (Claude Desktop, Cursor, Cline)
Wrap any Model Context Protocol (MCP) server with `toolguard` to protect your desktop agent:
```bash
# Wrap a Postgres MCP server
toolguard mcp-proxy -- npx -y @modelcontextprotocol/server-postgres postgresql://...
```
#### Claude Desktop Configuration (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"postgres": {
"command": "toolguard",
"args": [
"mcp-proxy",
"--",
"npx",
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://localhost/mydb"
]
}
}
}
```
---
### Option B: Embedded Rust Library (`toolguard-core`)
Add to your `Cargo.toml`:
```toml
[dependencies]
toolguard-core = "0.1"
serde_json = "1.0"
```
---
### Option C: CLI Schema & Mutation Inspector (CI/CD)
Test and debug coercions from the command line or CI scripts:
```bash
# Pretty terminal output
toolguard check --schema schema.json --input call.json --tool-name search_db
# Machine-readable JSON output for automated test runners
toolguard check --schema schema.json --input call.json --tool-name search_db --format json
```
Use inside your agentic loop or tool dispatcher:
```rust
use serde_json::json;
use toolguard_core::{ToolDefinition, ToolGuard, GuardVerdict};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let guard = ToolGuard::default();
// 1. Register tool schema (pre-compiles JSON Schema in memory)
guard.register_tool(ToolDefinition::new(
"search_db",
json!({
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" },
"status": { "type": "string", "enum": ["active", "archived"] }
},
"required": ["query"]
}),
))?;
// 2. Incoming model payload with stringified number and uppercase enum
let raw_args = json!({
"query": "Rust ecosystem",
"limit": "20",
"status": "ACTIVE"
});
// 3. Guard the call (takes ~25 microseconds)
match guard.guard_call("search_db", raw_args, None) {
GuardVerdict::Pass { arguments, flags, latency_us } => {
println!("Coerced arguments: {}", arguments);
// arguments["limit"] is now numeric 20
// arguments["status"] is now "active"
for flag in flags {
println!("Audit: {}", flag);
}
}
GuardVerdict::Reject { error_message, .. } => {
// Forward this short, crisp message back to the LLM to self-correct
println!("Send to LLM: {}", error_message);
}
}
Ok(())
}
```
---
## π Model-Readable Error Comparison
When an agent fails to provide required parameters or provides unfixable values, ToolGuard outputs an actionable diff designed specifically for the LLM's next context turn:
| Standard JSON Schema Error (Confusing for LLMs) | ToolGuard Model-Readable Error (High Recovery Rate) |
| :--- | :--- |
| `[{"instance_path": "/limit", "schema_path": "/properties/limit/type", "kind": "type", "expected": ["integer"]}]` | ```text<br>Invalid arguments for tool `search_db`:<br>- `limit`: expected integer, got "ten".<br>- `query`: required field is missing.<br>Correction instruction: Resend tool call with corrected parameters. Do not alter other valid fields.<br>``` |
---
## π Coercion Rules Matrix
| Target Schema Type | Received Input | Action | Flag Emitted |
| :--- | :--- | :--- | :--- |
| `integer` | `"42"` | Parse clean integer literal | `StringToInteger` |
| `number` | `"3.14"` | Parse clean float literal | `StringToFloat` |
| `boolean` | `"true"`, `"False"` | Case-insensitive boolean parse | `StringToBoolean` |
| `string` | `123`, `true` | Convert literal to string | `PrimitiveToString` |
| `array` | Single scalar (`"rust"`) | Wrap in `["rust"]` | `WrappedScalarInArray` |
| `enum: [A, B]` | `"a"` | Case-insensitive normalization | `EnumCaseNormalized` |
| `enum: [active, ...]` | `"actve"` | Fuzzy match (Levenshtein dist $\le 1$) | `EnumFuzzyMatched` |
| `object` | Extraneous keys | Prune unknown keys safely | `PrunedUnknownProperty` |
| `object` | Omitted property with `default` | Inject explicit schema default | `InjectedDefaultValue` |
| `string` (escaped JSON) | `"{\"query\": \"rust\"}"` | Auto-unpack stringified arguments | Evaluated on parsed object |
### Strictly Forbidden Coercions (Never Applied)
- **Lossy numerical truncation:** `3.7` is never coerced to integer `3`.
- **Ambiguous enum matches:** If input matches multiple enum candidates within edit distance 1, it is rejected.
- **Missing required keys:** Missing fields are never hallucinated with synthetic defaults unless explicitly specified by schema `default`.
- **Destructive tools carve-out:** Any tool tagged with `is_destructive: true` (e.g. `delete_db`, `execute_bash`) automatically enforces `Strict` validation mode by default.
---
## π Benchmark & Performance
Run the benchmark locally:
```bash
toolguard bench --corpus-dir corpus
```
Results against real-world tool schemas (PostgreSQL, Filesystem, Brave Search, Weather):
```text
================ BENCHMARK RESULTS ================
Total Test Cases Evaluated: 10
Auto-Repaired by Coercion: 6 (60.0%)
Correctly Rejected & Duffed: 4 (40.0%)
Total Handled Correctly: 10 (100.0%)
---------------------------------------------------
Latency p50: 25 Β΅s
Latency p95: 169 Β΅s
Latency p99: 169 Β΅s
===================================================
```
---
## π‘ Observability: OpenTelemetry & JSONL
Every evaluated tool call emits OpenTelemetry GenAI semantic attributes:
* `tool_name`: Target tool name.
* `outcome`: `"passed"`, `"coerced"`, or `"rejected"`.
* `coercion_count`: Total number of fields mutated.
* `validation_errors`: Number of unfixable schema errors.
* `latency_us`: Execution overhead in microseconds.
Zero-dependency structured JSONL logging can also be directed to disk via `TelemetryConfig`.
---
## π What ToolGuard is NOT
* **NOT another JSON syntax repair crate:** ToolGuard assumes valid JSON syntax. Upstream syntax breakage is handled by streaming token repair libraries (`llm_json`). ToolGuard enforces **semantic schema validity**.
* **NOT an agent framework:** ToolGuard does not manage prompts, memory, or state graphs. It works with Rig, LangGraph, or raw HTTP clients.
* **NOT a security / authorization firewall:** Schema validity is not user authorization. Use dedicated firewalls for role-based access control.
---
## π¦ Workspace Crates
| Crate | Purpose |
| :--- | :--- |
| [`toolguard-core`](crates/toolguard-core) | Core schema compiler, rule-based coercer, error renderer, and policy engine. |
| [`toolguard-mcp`](crates/toolguard-mcp) | Transparent Model Context Protocol (MCP) stdio and HTTP proxy. |
| [`toolguard-cli`](crates/toolguard-cli) | Standalone command-line interface (`toolguard check`, `toolguard mcp-proxy`, `toolguard bench`). |
---
## π License
Licensed under either of:
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
* MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues