Skip to main content
Glama

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 Docs.rs License: MIT OR Apache-2.0

 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.


Related MCP server: aperion-shield

🚀 Quickstart

Option A: Transparent MCP Proxy (Claude Desktop, Cursor, Cline)

Wrap any Model Context Protocol (MCP) server with toolguard to protect your desktop agent:

# Wrap a Postgres MCP server
toolguard mcp-proxy -- npx -y @modelcontextprotocol/server-postgres postgresql://...

Claude Desktop Configuration (claude_desktop_config.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:

[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:

# 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:

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:

toolguard bench --corpus-dir corpus

Results against real-world tool schemas (PostgreSQL, Filesystem, Brave Search, Weather):

================ 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

Core schema compiler, rule-based coercer, error renderer, and policy engine.

toolguard-mcp

Transparent Model Context Protocol (MCP) stdio and HTTP proxy.

toolguard-cli

Standalone command-line interface (toolguard check, toolguard mcp-proxy, toolguard bench).


📜 License

Licensed under either of:

at your option.

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    A
    maintenance
    Local guardrail proxy for AI coding agents. Wraps any MCP server (stdio or HTTP/SSE) and blocks destructive tool calls before they execute, with TOFU catalog pinning against rug pulls and tool-poisoning/result-injection scanning. Single Rust binary, Apache-2.0.
    14
    11
    -
  • A
    license
    A
    quality
    A
    maintenance
    LLM-native code tools: 44 MCP tools for AI agents to read, understand and modify code. Rust tree-sitter parse daemon (crash-isolated), SQLite symbol index (repo map 98ms), transactional journal-backed writes, deterministic quality gates (zero LLM calls). 512MB cgroup: 128 concurrent, zero OOM. MIT.
    13
    1
    MIT