Identity, permission, delegation, and tamper-evident audit middleware for Model Context Protocol servers.
TrustFabric wraps any MCP server and enforces a policy before forwarding, without touching the server's code. It supports both MCP transports:
- STDIO (
trustfabric run) — spawns a local server as a subprocess and inserts itself into its STDIO pipe. - HTTP / SSE (
trustfabric proxy) — runs as a reverse proxy in front of a remote HTTP-based server.
Clients connect to TrustFabric instead of directly to the server; that is the only change.
STDIO: client --stdin--> TrustFabric --stdin--> MCP server
client <--stdout-- TrustFabric <--stdout-- MCP server
HTTP: client --POST /mcp--> TrustFabric --POST /mcp--> MCP server
<--JSON/SSE--- <--JSON/SSE----
Both transports share one enforcement core (the Enforcer), so policy
decisions are identical regardless of how an agent connects.
On every tools/call and resources/read, TrustFabric runs five gates in
order. If any gate fails, the call is blocked, a JSON-RPC error is returned to
the client, and the server never sees the request:
- Transport interceptor — parses the JSON-RPC line off the STDIO pipe.
- Identity verifier — validates the agent's JWT. Symmetric (HS256) for dev, or asymmetric (RS256/ES256) and SPIFFE JWT-SVIDs for production.
- Permission engine — checks the agent's policy: may it call this tool with these arguments, or read this resource URI?
- Delegation tracker — verifies the delegation chain and depth limit, blocking the confused-deputy attack.
- Audit logger — writes a hash-chained, tamper-evident entry for the call (ALLOW and DENY) before anything is forwarded.
On tools/list and resources/list, TrustFabric filters the server's response
so an agent only ever sees the tools and resources its policy permits — denied
and unlisted items disappear from discovery entirely, rather than failing only
when used.
Tools and resources are governed separately: granting a tool does not grant
any resource access. Resources are a parallel data path, so an agent with tool
permissions but no allowed_resources can read no resources at all.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install -e . # installs the `trustfabric` command# 1. Configure
cp .env.example .env
cp policy.example.yaml policy.yaml
# Put a real secret in .env:
python -c "import secrets; print('TRUSTFABRIC_JWT_SECRET=' + secrets.token_urlsafe(48))"
# 2. Mint a token for the summariser agent (uses TRUSTFABRIC_JWT_SECRET from .env)
export $(grep TRUSTFABRIC_JWT_SECRET .env)
python examples/make_token.py --agent agent:summarizer-v2
# 3. Run the example server through the proxy
export TRUSTFABRIC_AGENT_TOKEN="<paste the token>"
trustfabric run --server "python examples/echo_server.py" --policy policy.yamlNow pipe MCP requests into the proxy's stdin. An allowed call is forwarded; a blocked call returns an error and is logged. Try these two lines:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/docs/readme.md"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/etc/passwd"}}}The first returns file contents; the second is blocked by the path: "/docs/*"
constraint in policy.yaml.
The MCP Inspector gives you a UI to call tools, list them, and watch responses — far nicer than piping JSON by hand. Point it at TrustFabric and it talks to the proxy exactly as it would to a real server.
Generate a config with a fresh token baked in, then launch:
# Mints a token and writes inspector.config.json wired to the echo server.
export $(grep TRUSTFABRIC_JWT_SECRET .env)
python examples/make_inspector_config.py --agent agent:summarizer-v2
npx @modelcontextprotocol/inspector --config inspector.config.json --server trustfabric-echoIn the Inspector UI:
- List Tools shows only
echoandread_file—write_fileis filtered out because the policy denies it. - Call
read_filewithpath: /docs/xsucceeds; withpath: /etc/passwdit returns a TrustFabric error. - The server-logs pane streams every decision live:
[trustfabric] ALLOW ...,[trustfabric] WARNING DENY ...,[trustfabric] INFO FILTER ....
To wrap a different agent, re-run the generator with another --agent, or edit
examples/inspector.config.example.json by hand. To wrap a real MCP server
instead of the echo example, change the --server argument to that server's
launch command and set policy entries matching its actual tool names (run
List Tools once to see them).
For a remote, HTTP-based MCP server, run TrustFabric as a reverse proxy instead of a STDIO wrapper. Point clients at the proxy's port; it gates requests and forwards them upstream.
# Start your HTTP MCP server (example provided):
python examples/http_echo_server.py --port 3000 &
# Put TrustFabric in front of it:
trustfabric proxy --target http://localhost:3000 --port 8080 --policy policy.yamlOver HTTP, agents present identity with a standard header — cleaner than the
STDIO _meta convention:
TOKEN=$(python examples/make_token.py --agent agent:summarizer-v2)
curl -s http://localhost:8080/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"read_file","arguments":{"path":"/docs/x.md"}}}'The proxy applies the same five gates and list filtering as STDIO. JSON
responses (including tools/list / resources/list) are filtered; SSE streams
(text/event-stream) are passed through unchanged. JSON-RPC batches are
rejected rather than forwarded ungated.
Run the HTTP end-to-end test:
python smoke_test_http.pytrustfabric verify --audit audit.log
# [OK] chain intact <- or [TAMPERED] ... if an entry was alteredAll settings come from the environment (or a .env file). See .env.example
for the full list. The essentials:
| Variable | Purpose |
|---|---|
TRUSTFABRIC_JWT_SECRET |
Secret for HS256 (symmetric) signing/verification. |
TRUSTFABRIC_JWT_ALG |
Signing algorithm: HS256 (dev) or RS256/ES256 (prod). |
TRUSTFABRIC_JWT_PUBLIC_KEY[_PATH] |
Public key (PEM) for asymmetric verification. |
TRUSTFABRIC_JWT_AUDIENCE |
Optional pinned audience (aud). |
TRUSTFABRIC_SPIFFE_TRUST_DOMAIN |
Optional pinned SPIFFE trust domain. |
TRUSTFABRIC_POLICY_PATH |
Path to the policy YAML. |
TRUSTFABRIC_AUDIT_PATH |
Path to the audit log. |
TRUSTFABRIC_REQUIRE_IDENTITY |
Enforce identity on every call (default true). |
TRUSTFABRIC_AGENT_TOKEN |
Optional default session token. |
TrustFabric verifies a JWT on every gated call. Choose the mode with
TRUSTFABRIC_JWT_ALG:
HS256 (symmetric) — dev / MVP. One shared secret signs and verifies. Simple,
but anyone who can verify can also mint tokens, so it is not a production trust
model. Set TRUSTFABRIC_JWT_SECRET.
RS256 / ES256 (asymmetric) — production. The issuer signs with a private key; TrustFabric verifies with only the public key and can never mint tokens. Generate a keypair and point TrustFabric at the public half:
python examples/make_keypair.py --out-dir keys # private + public PEM
# .env:
# TRUSTFABRIC_JWT_ALG=RS256
# TRUSTFABRIC_JWT_PUBLIC_KEY_PATH=keys/jwt_public.pem
python examples/make_token.py --agent agent:summarizer-v2 \
--alg RS256 --private-key keys/jwt_private.pemSPIFFE JWT-SVIDs. Use a SPIFFE ID (spiffe://trust-domain/path) as the
token subject; it becomes the agent_id used for policy lookup. Optionally pin
the trust domain and audience so tokens from other domains are rejected:
TRUSTFABRIC_SPIFFE_TRUST_DOMAIN=acme.example
TRUSTFABRIC_JWT_AUDIENCE=trustfabric
Full SPIFFE Workload API integration (SVID rotation via the SPIFFE socket, JWKS bundle fetching) is a production follow-on; this verifies SPIFFE-shaped JWT-SVIDs against a configured public key.
policy.yaml defines per-agent permissions in a deny-by-default model. See
policy.example.yaml. A constrained tool entry looks like:
agents:
- id: "agent:summarizer-v2"
allowed_tools:
- name: "read_file"
constraints:
path: "/docs/*" # glob match on the `path` argument
denied_tools:
- "write_file"
allowed_resources:
- "file:///docs/*" # URI glob; may read resources under /docs
denied_resources:
- "file:///secrets/*" # never, even if an allow glob would match
max_delegation_depth: 2Note: in URI globs, * spans path separators, so file:///docs/* also matches
nested paths like file:///docs/sub/x.md.
STDIO has no per-message auth header, so the agent token is read from
params._meta.trustfabric.token on each request, falling back to the session
default (TRUSTFABRIC_AGENT_TOKEN). A request carrying its own token can
present a delegation chain in the JWT chain claim.
pip install -r requirements-dev.txt
pytestOr run the dependency-free smoke test:
python smoke_test.pysrc/trustfabric/
enforcer.py transport-independent enforcement core (the five gates)
interceptor.py STDIO transport (proxy core)
http_proxy.py HTTP/SSE reverse-proxy transport
identity.py JWT verification (HS256 / RS256 / ES256 / SPIFFE)
permissions.py tool + resource policy evaluation and visibility
delegation.py delegation-chain enforcement
audit.py hash-chained audit log
policy.py policy YAML loader (tools + resources)
jsonrpc.py JSON-RPC 2.0 helpers
config.py environment configuration
log.py stderr-only decision logging
cli.py `trustfabric run` / `proxy` / `verify`
examples/
echo_server.py tiny STDIO MCP-style server
http_echo_server.py tiny HTTP MCP-style server
make_token.py mint test JWTs (symmetric or asymmetric)
make_keypair.py generate an RSA keypair for RS256
make_inspector_config.py write a ready-to-run Inspector config
inspector.config.example.json Inspector config template
tests/ unit tests
smoke_test.py STDIO end-to-end test
smoke_test_http.py HTTP end-to-end test
prompts/list/prompts/getgating (currently passed through)- TypeScript/npm package (Python/pip only today; the strategy targets both)
- Full SPIFFE Workload API: SVID rotation via the SPIFFE socket, JWKS bundles
- SSE-stream list filtering (currently JSON list responses are filtered; SSE streams pass through unchanged)
- JSON-RPC batch support over HTTP
- Postgres audit sink with retention lifecycle
This is a pre-production MVP intended to validate the architecture and attract a technical co-founder. A full security audit is required before any production deployment. See SECURITY.md for the vulnerability disclosure policy and known limitations.
Contributions welcome — see CONTRIBUTING.md for setup and ground rules (deny-by-default is non-negotiable; enforcement logic stays in the shared core). Licensed under the Apache License 2.0. Changes are tracked in CHANGELOG.md; CI runs the unit suite and both end-to-end smoke tests on every push.