Apache-2.0 · one binary · v0.2.0

Every tool call, decided and recorded before it runs.

MCPdef sits in the data path between your agents and the MCP servers they call. It brokers auth so servers never hold long-lived credentials, decides allow or deny per call, can run an untrusted server sandboxed under Wasmtime, and appends every decision to a tamper-evident ledger — before the call reaches anything.

your clients

agent · IDE
app · service
CI · script

mcpdef

one binary
in the path
deny by default

your MCP servers

stdio child process
Streamable-HTTP server
legacy HTTP+SSE server
untrusted .wasm (sandboxed)

Clients connect over stdio or Streamable HTTP with a bearer JWT. You declare the servers to front in one mcpdef.toml. The boundary is the MCP tool wire only — MCPdef never sits between an agent and a model provider, so each layer keeps a crisp threat model.

§01 — the gate pipeline

Nine stages, in order, one binary

Every tools/call crosses the same pipeline before it reaches a server. A call that is not explicitly allowed returns an MCP tool-execution error — so the model can self-correct — and is audited either way.

1 mcpdef-transport Speak stdio, Streamable HTTP, or the legacy 2024-11-05 HTTP+SSE bridge, with probe/fallback and Last-Event-ID resumption. Carries the egress/SSRF guard: cloud metadata always blocked, DNS-pinned.
2 mcpdef-auth OAuth 2.1 Resource Server. Per-request bearer JWT validated against a JWKS, asymmetric-only so algorithm confusion is off the table, RFC 8707/9068 audience, RFC 9728 metadata and a WWW-Authenticate challenge.
3 mcpdef-policy Deny-by-default allowlist with globs and reusable named profiles, an RBAC role→grant model, and per-agent / per-argument rules layered over it.
4 mcpdef-pin Pin each tool's definition at approval. If a server changes it afterwards, that is a rug-pull: denied and audited.
5 mcpdef-ratelimit Token-bucket limits per tool and global, plus a per-call upstream timeout. A wedged upstream fails as upstream-timeout rather than hanging.
6 mcpdef-inspect Scan tool descriptions at connect and results per call for injection and secret exfiltration, in off / warn / enforce modes.
7 mcpdef-sandbox Run an untrusted server in-process under Wasmtime, behind the same transport seam — fuel, memory ceiling, optional wall-clock deadline.
8 mcpdef-audit Append the decision to an append-only, hash-linked ledger, with offline verify and OCSF / CEF / syslog export.
9 mcpdef-core The shared JSON-RPC 2.0 and MCP envelope everything above is built on.

Safety and audit are never paywalled. The complete data path — transport bridging, allowlists, the ledger, token brokering, RBAC, policy enforcement and the WASM sandbox — ships in the free binary.

§02 — the audit ledger

Each record carries the hash of the one before it

One JSON line per governed call, appended before dispatch. Editing or deleting any interior record breaks every hash downstream. These are real records from the quickstart below — an allow and a deny, chained.

$ mcpdef audit verify --config quickstart.toml chain OK · 2 record(s) · head=f4bb388a0ba59cef9af31b5732c5f03c4dfb27d24e68fc078abafbe1a40fa17a # and out to whatever consumes it $ mcpdef audit tail --format ocsf | your-siem-forwarder # also cef / syslog / json
What the local verify does not prove audit verify on its own proves internal consistency. It cannot detect tail-truncation or wholesale file replacement, because a shorter valid chain is still a valid chain. Catching that needs a (head, count) pair sealed out-of-band and checked with audit verify --head <h> --count <n>. If a page tells you a hash chain alone is tamper-proof, it is selling you something.

§03 — untrusted servers

Run someone else's MCP server without giving it your machine

An upstream can be a .wasm server that MCPdef executes itself, in-process under Wasmtime — no child process, no shell, no ambient access. Every call is bounded, and the same allowlist, RBAC and audit apply as to any other upstream.

Bounded per call

  • Fuel — a CPU budget it cannot exceed
  • Linear-memory ceiling
  • Optional wall-clock deadline via epoch interruption

Path A — core module

transport = "wasm" runs against an empty linker. Zero ambient capability: no filesystem, no network, no clock. A module that imports WASI simply fails to load.

Path B — wasip2 component

transport = "wasm-component" runs the mcpdef:server WIT world under capability-scoped WASI that grants nothing by default. Its only possible host capability is outbound TCP.

Egress, deny-all

That TCP grant is gated per destination by wasm_allow_egress, default deny-all, reusing the same IP classification as the main egress guard — so cloud metadata and special-use ranges stay blocked.

§04 — see it working

One config file, and a call that gets refused

Real output. The demo upstream is the mock server that ships for the integration tests: it exposes a harmless echo and a delete_repo to exercise the deny path. Point the same config at any real stdio or HTTP MCP server.

# mcpdef.toml — declare what you front, and what is allowed [gateway] listen = "127.0.0.1:7878" audit = "./mcpdef-audit/audit.log" pins = "./mcpdef-pins.toml" [[server]] id = "mock" transport = "stdio" command = ["./target/debug/mock_mcp_server"] tools = ["echo"] # only these are exposed deny = ["delete_*"] # deny wins over allow
$ mcpdef validate --config quickstart.toml ok: quickstart.toml — 1 upstream(s), config valid $ mcpdef servers list --config quickstart.toml ID TRANSPORT PROFILE ALLOWLISTED TOOLS DENY mock stdio - echo delete_* $ mcpdef call echo --args '{"msg":"hi"}' --config quickstart.toml echo: {"msg":"hi"} # the deny path: short-circuits before dispatch, exits non-zero, always audited $ mcpdef call delete_repo --args '{"name":"prod"}' --config quickstart.toml Error: MCPdef denied: tool 'delete_repo' matches deny pattern 'delete_*' for 'mock'
# serve it to MCP clients — loopback-bound, Origin-validated $ mcpdef up --config quickstart.toml mcpdef 0.2.0 ready · 1 upstream(s) · listening streamable-http 127.0.0.1:7878 $ curl -s -X POST http://127.0.0.1:7878/mcp \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"echo",…}]}} # delete_repo is not in the list at all

Point Claude Code or Cursor at it over stdio with mcpdef run --config mcpdef.toml, or any Streamable-HTTP client at /mcp. Turn on [gateway.admin] — off by default, on a separate port from the data path — for a read-only status UI, Prometheus /metrics and a small JSON API, with no external stack required.

§05 — supply chain

You are putting this in your data path. Check what you got.

A gateway that sees every tool call has no business asking to be trusted on its word. Every release is signed and every crate ships a bill of materials.

4

Platforms per release — x86_64 and aarch64, on linux-musl and macOS.

10

CycloneDX SBOMs, one per crate, attached to the release.

100%

Of release artefacts carry a Sigstore signature and certificate.

152

Tests — unit, transport bridge, egress, pin, rate-limit, OAuth/RBAC, sandbox, injection scan, CLI.

Container

$ docker pull mancube/mcpdef:v0.2.0

The image is signed too — the registry carries a cosign signature tag alongside it.

Binary

$ sha256sum -c SHA256SUMS $ cosign verify-blob \ --certificate mcpdef-<target>.tar.gz.pem \ --signature mcpdef-<target>.tar.gz.sig \ mcpdef-<target>.tar.gz

Source

$ cargo build -p mcpdef --release $ cargo test # 152 tests

The OSS engine is Apache-2.0 at the repository root. The paid governance plane lives under ee/ on BSL 1.1, its own workspace, never packaged into the public crates.

§06 — where it actually is

What works today, and what does not

The MCP governance category is under eighteen months old and crowding fast. Here is the state of this one, without the roadmap dressed up as the product.

LandedTransport multiplexing — stdio, Streamable HTTP, and the legacy HTTP+SSE bridge with probe/fallback and resumption.
LandedDeny-by-default allowlists with globs, named profiles, and a gateway scoped to an active profile to cut the tool surface an agent sees.
LandedOAuth 2.1 termination, RBAC, and token brokering — a stdio upstream's credential is injected via [server.env], so a client's bearer is never passed through.
LandedThe Wasmtime sandbox, both paths, with fuel, memory and epoch caps and a default-deny egress allowlist.
LandedTool-def pinning, token-bucket rate limiting, per-call upstream timeout, the egress/SSRF guard, and the hash-linked ledger with SIEM export.
NextA transform policy effect — mutating arguments and results, rather than only allowing or denying them. The policy-as-code engine it sits on shipped; the effect has not.
Not yetResource-URI allowlists. They are parsed and reserved in the config, but only tools are gated today.
Why the binary is “static-ish” Wasmtime's Cranelift backend embeds a JIT, so a fully static *-linux-musl build has to be validated against it — the JIT path may fall back to glibc. That is why this page does not promise a blanket fully-static binary. For locked-down hosts that forbid W^X pages there is a fully static, no-JIT profile using the Pulley interpreter.

Built against MCP spec 2025-11-25, forward-planning the stateless 2026-07-28 RC. MCPdef governs the tool wire and explicitly does not route model traffic; a vendor private-MCP tunnel solves reachability, not governance, and the two compose — terminate the tunnel locally and hand its JSON-RPC to MCPdef.

Apache-2.0

Read it before you put it in the path

The engine is open source and complete on its own. Nothing about safety or audit is held back to sell you something.