Skip to main content
Glama
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.**

[![Crates.io](https://img.shields.io/crates/v/toolguard-core.svg)](https://crates.io/crates/toolguard-core)
[![Docs.rs](https://docs.rs/toolguard-core/badge.svg)](https://docs.rs/toolguard-core)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](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.

Maintenance

ActivityMaintained
ResponsivenessNo issues