Documentation

bayesian-cage is an open-source confidence gate for MCP tool calls. It fronts your tool servers, verifies every output, and returns PROCEED / FLAG / BLOCK with a calibrated confidence. This is the reference for installing, configuring, and extending it.

Getting Started

The cage installs in one line and needs no account. It runs as an MCP proxy: your host — Claude Desktop, Cursor, or any MCP client — talks to the cage, and the cage spawns your real tool server as a stdio subprocess and re-exposes every tool, gated. Requires Python 3.10+ and the stdlib only — no runtime dependencies.

Install
pipx install bayesian-cage # MCP-proxy binary on PATH pip install bayesian-cage # library use, inside a 3.10+ venv

macOS ships Python 3.9 as python3; install from a 3.10+ interpreter (e.g. brew install [email protected]) or pip reports “no matching distribution”.

Point your host at the cage

Add the cage to your MCP host config. It speaks stdio to the host and spawns your real MCP server as a stdio subprocess, set via BAYESIAN_CAGE_DOWNSTREAM. It transparently forwards initialize / tools/list / tools/call, and grades every tools/call result before it reaches the host.

{ "mcpServers": { "filesystem-gated": { "command": "/Users/you/.local/bin/bayesian-cage", "env": { "BAYESIAN_CAGE_DOWNSTREAM": "npx -y @modelcontextprotocol/server-filesystem /Users/you/Documents", "BAYESIAN_CAGE_MODE": "advisory", "BAYESIAN_CAGE_VERIFIER": "filesystem", "BAYESIAN_CAGE_MODEL": "fs-server" } } } }

Use the absolute path to the bayesian-cage binary (which bayesian-cage) — the host spawns it without your shell's PATH. In advisory mode (the default) the cage labels each result; switch to enforce and a BLOCK is withheld and comes back as an MCP error (isError: true), so a compliant client won't act on it.

How It Works

The cage wraps every tool call in a four-step loop — verify → calibrate → gate → observe. It treats a tool output as a hypothesis to check against evidence, not an assertion to trust.

  1. Verify. One or more checks — grounding against the sources you supply, schema, non-emptiness — score the output, not the model's own say-so.
  2. Calibrate. The checks combine with the tool's belief state into a single calibrated confidence in [0, 1].
  3. Gate. The confidence maps to PROCEED, FLAG, or BLOCK.
  4. Observe. The per-tool belief updates on the outcome, so the gate sharpens with use.

The thresholds are named constants — commit at 0.72, flag floor at 0.40 — not magic numbers. Grounded in Bayes (1763), Cox (1946), Pearl (1988), Jaynes (2003).

The Gate

Every verified output resolves to exactly one decision, based on its calibrated confidence:

ConfidenceDecisionMeaning
≥ 0.72PROCEEDClears the commit threshold — the tool output passes through to the agent unchanged.
0.40 – 0.72FLAGThin or partially inconsistent evidence — passed, but marked unverified.
< 0.40BLOCKContradiction or failed check — returned to the host as an MCP error; the agent does not act.

A BLOCK is returned to the host as an MCP tool error (isError: true), so a compliant client will not act on it.

Configuration

The gateway is configured entirely through environment variables — no config file required.

VariableRequiredDescription
BAYESIAN_CAGE_DOWNSTREAMyesCommand that launches the real MCP server; the cage spawns it as a stdio subprocess and fronts it — e.g. "npx -y @modelcontextprotocol/server-filesystem ~/Documents".
BAYESIAN_CAGE_MODEnoadvisory (default — label only) · enforce (BLOCK withholds the result and returns isError) · iterate (retry instead of stop).
BAYESIAN_CAGE_VERIFIERnoWhich verifier to run — heuristic (default) · sql · json · filesystem · ensemble:heuristic+sql · your.pkg.module:CustomVerifier.
BAYESIAN_CAGE_MODELnoBelief-bucket key; use one per downstream server (fs-server, pg-server). The server is what is scored, not the host LLM.
BAYESIAN_CAGE_DBnoSQLite belief-store path. Default ~/.bayescore/bayescore.db.

v0.1 gates stdio (local-subprocess) downstream servers; HTTP/remote downstreams are on the roadmap.

Verifiers

A verifier scores whether a tool output is trustworthy. The cage ships with several — all offline, pure stdlib — and the interface is small enough to add your own. Pick one with BAYESIAN_CAGE_VERIFIER.

HeuristicVerifier (default, offline)

Catches refusals, non-answers, error markers, placeholders, degenerate repetition, truncation, and heavy hedging. Pass expected or reference in the context for a grounded signal.

SqlVerifier

Catches swallowed SQL errors, schema / identifier typos, broken-empty vs legitimately-empty results, and degenerate all-NULL rows.

JsonVerifier

Distinguishes malformed from well-formed JSON and checks a required key list for structured outputs.

FilesystemVerifier

POSIX errno markers and access-denied envelopes BLOCK; file listings and plain content pass.

EnsembleVerifier

Composes verifiers and gates on the strictest (minimum) signal — e.g. ensemble:heuristic+sql.

Bring your own

Any class implementing Verifier.verify(output, context) → VerifierResult can replace or extend the defaults — point at it with BAYESIAN_CAGE_VERIFIER=your.pkg.module:CustomVerifier. New grounding strategies are the highest-leverage contribution.

Response Shape

The cage stamps each gated result with a _bayescore envelope describing the decision, the calibrated probability, why, and the tool's current belief. The observation_id is what you pass back to observe when the real outcome is known:

{ "result": { "...the real tool output..." }, "_bayescore": { "decision": "BLOCK", "p": 0.26, "reason": "contradiction: tool said 'Sydney', sources say 'Canberra'", "observation_id": "9f2c1a…", "belief": { "model": "kb-server", "task": "lookup", "mean": 0.5, "n": 12 } } }

The same gate is available as a library via the Kernel API, with the per-(model, task) belief persisted to local SQLite:

from bayesian_cage import Kernel k = Kernel(db_path="~/.bayescore/bayescore.db") v = k.check("SELECT 1", {"expected": "select 1"}, model_id="phi3", task="sql") v.decision, v.p # -> "PROCEED", 1.0 # feed the real outcome back so calibration sharpens over time k.observe("phi3", "sql", correct=True, observation_id=v.observation_id)

Belief State

The cage keeps a Beta(α, β)belief per tool — the correct distribution for binary (verified / not) outcomes. α increments when a tool's output verifies, β when it doesn't. The posterior mean α / (α + β) calibrates the gate, and a forgetting factor lets recent behaviour outweigh stale history.

Beliefs are keyed per (model, task) and persist between runs in local SQLite, so the gate gets more precise the more it sees a given downstream — with no manual rating step. Calibration is learned per model, so swapping the LLM recalibrates instead of conflating.

"belief": { "model": "kb-server", "task": "lookup", "mean": 0.5, "n": 12 }

Calibration

The whole point is that the cage is better calibrated than the model trusting itself. Reproducible head-to-head — phi-3 via Ollama, 55-task execution-graded text-to-SQL, 5-fold (seed=7), 67.3% accuracy — the cage's calibrated confidence vs phi-3's own:

MetricCageRaw phi-3
ECE — calibration (lower better)0.0810.325
Brier (lower better)0.1740.322
catch-rate (higher better)33%0%
acts on wrong outputs (lower better)1218
AUROC (higher better)0.5440.583

phi-3's raw confidence isn't discriminative — nearly every answer comes back ~1.0, so AUROC sits near chance either way; the cage's win is calibration (ECE ~4× tighter) and catching a third of wrong answers at zero correct blocked. Correctness is labeled by executing the SQL against a real database — nothing hand-labeled. Rerun it: python -m bayesian_cage.eval.sqlbench.run --model phi3 --seed 7. More in the research.

Reference

The full API, the verifier protocol, and the calibration eval live in the repository.

Questions, bug reports, or a wrong PROCEED / false BLOCK? Open an issue on GitHub or email [email protected].