The path of one tools/call
- Front ends. A client speaks stdio (
mcpdef run) or Streamable HTTP (mcpdef run --http/mcpdef up). The HTTP listener binds loopback by default, validatesOrigin(a cross-site request gets403), sheds load at an optional in-flight cap (503) and caps request bodies at 2 MiB. - Auth (HTTP only).
mcpdef-authvalidates the per-request bearer JWT against a JWKS and serves the RFC 9728 metadata and challenge. The resulting principal becomes the audit identity and the input to RBAC. A stdio client is a local process and skips this step. - Gateway loop. Answers
initialize,tools/listandpingitself, routestools/callthrough the gates, and forwards everything else to the upstream. - Gates, in order.
mcpdef-policy(deny-by-default allowlist and profiles, then RBAC role grants and per-argument rules) →mcpdef-pin(a tool whose definition drifted from its pin is hidden and denied) →mcpdef-ratelimit(token buckets, per tool and global) →mcpdef-inspect(scans tool descriptions at connect, and results per call, for injection and secret exfiltration). A refusal is an MCP tool-error the model can read and correct. - Transport or sandbox.
mcpdef-transportreaches a stdio child (credentials injected through[server.env]), a Streamable-HTTP server, or a legacy HTTP+SSE server, behind an egress/SSRF guard with DNS pinning.mcpdef-sandboxsits behind the same transport seam and runs an untrusted.wasmserver in-process under Wasmtime, so every gate applies to it unchanged. - Ledger. The gateway appends every outcome to the append-only, hash-linked ledger in
mcpdef-audit;mcpdef audit tailexports it to a SIEM as OCSF, CEF, syslog or JSON.mcpdef-core— the JSON-RPC 2.0 envelope and decision types — is shared by every crate.
Gate order, and what the client sees
Every outcome, allow or deny, appends one record to the ledger. A denial comes back as a tools/call result with isError: true and the text MCPdef denied: <reason>, never as a protocol error, so an agent can adjust and retry.
| # | Gate | Crate | Audit rule on deny |
|---|---|---|---|
| 1 | Routing — is the tool exposed by any governed server? | mcpdef | unknown-tool |
| 2 | Allowlist and profiles | mcpdef-policy | unknown-server, deny-glob, not-on-allowlist, not-in-active-profile |
| 3 | RBAC (authenticated HTTP callers, when [[role]] is defined) | mcpdef-policy | rbac |
| 4 | Pin / rug-pull (when [gateway] pins is set) | mcpdef-pin | rug-pull |
| 5 | Rate limit — after authorization, so denied floods cannot drain buckets | mcpdef-ratelimit | rate-limited |
| 6 | Dispatch and per-call timeout | mcpdef-transport | upstream-timeout |
The injection scan in mcpdef-inspect runs in off, warn or enforce mode; a finding is audited under an injection or secret-exfil rule. The exact client-visible messages for each row are in the API reference.
The sandboxed path
An upstream can be a .wasm server that MCPdef runs itself. It is one more kind of upstream behind the transport seam, so the same gates decide first.
- The client sends
tools/call. On the HTTP listener the bearer JWT is validated for this request. - The allowlist, RBAC, pin and rate-limit gates run. If any denies, the record is appended and the client gets the tool-error; the sandbox is never entered.
- If allowed, the call goes to the sandbox —
transport = "wasm"(core module) or"wasm-component". The instance is reused, runs on the blocking pool, and its fuel and epoch deadline are re-armed for each call. - A component that opens an outbound TCP connection is checked against the egress allowlist: permitted only if the destination is allowlisted and passes the same IP classification as the main egress guard. A core module has an empty linker, so this step never applies to it.
- The response returns to the gateway, a record is appended, and the client gets the governed response.
Boundaries
- The MCP tool wire only. MCPdef never sits between an agent and a model provider. Model traffic goes through a separate LLM gateway; MCPdef governs the tools, resources and prompts an agent invokes, plus the initialize handshake. That keeps each layer's threat model small.
- Reachability is not governance. A vendor private-MCP tunnel solves reachability. MCPdef does not build one, and it composes with one: terminate the tunnel locally and hand its JSON-RPC to MCPdef, which then runs the same path above before anything reaches an upstream.
- The whole data path is in the free binary. Transport bridging, auth and RBAC, allowlists, pinning, rate limiting, the sandbox and the complete ledger ship together under Apache-2.0. Safety and audit are not held back.
Crate reference
| Crate | Role |
|---|---|
mcpdef | The binary: config, the gateway loop, the Streamable HTTP listener, and the CLI (run, up, call, validate, audit, pin). |
mcpdef-core | The normalized JSON-RPC 2.0 envelope and the MCP method and decision types. |
mcpdef-transport | The Transport trait; stdio child with credential injection; Streamable HTTP and the legacy HTTP+SSE bridge; the egress/SSRF guard. |
mcpdef-sandbox | Runs an untrusted MCP server under Wasmtime behind the same Transport seam, with fuel, memory and optional wall-clock limits. |
mcpdef-policy | The deny-by-default allowlist with globs and named profiles, the RBAC role-to-grant model, and per-agent and per-argument rules. |
mcpdef-auth | The OAuth 2.1 resource server: per-request bearer JWT validation against a JWKS, asymmetric-only, RFC 8707/9068 audience, RFC 9728 metadata. |
mcpdef-pin | Canonical tool-definition hashing and a persistent pin store, for rug-pull detection. |
mcpdef-ratelimit | Token-bucket limits, per tool and global, for the tools/call hot path. |
mcpdef-inspect | Injection and secret-exfil scanning over tool descriptions (at connect) and results (per call). |
mcpdef-audit | The append-only, hash-linked ledger, offline verify, and OCSF, CEF and syslog export. |
Next: the configuration reference for every key that drives the boxes above, and the operations runbook for verifying the ledger.