Skip to content

Repository files navigation

MCP TrustFabric for AI agents

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.

What it does

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:

  1. Transport interceptor — parses the JSON-RPC line off the STDIO pipe.
  2. Identity verifier — validates the agent's JWT. Symmetric (HS256) for dev, or asymmetric (RS256/ES256) and SPIFFE JWT-SVIDs for production.
  3. Permission engine — checks the agent's policy: may it call this tool with these arguments, or read this resource URI?
  4. Delegation tracker — verifies the delegation chain and depth limit, blocking the confused-deputy attack.
  5. 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.

Install

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install -e .          # installs the `trustfabric` command

Quickstart (60 seconds)

# 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.yaml

Now 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.

Test interactively with the MCP Inspector

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

In the Inspector UI:

  • List Tools shows only echo and read_filewrite_file is filtered out because the policy denies it.
  • Call read_file with path: /docs/x succeeds; with path: /etc/passwd it 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).

HTTP / SSE transport

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.yaml

Over 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.py

Verify the audit log

trustfabric verify --audit audit.log
# [OK] chain intact         <- or [TAMPERED] ... if an entry was altered

Configuration

All 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.

Identity modes

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.pem

SPIFFE 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

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

Note: in URI globs, * spans path separators, so file:///docs/* also matches nested paths like file:///docs/sub/x.md.

How identity is attached over STDIO

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.

Tests

pip install -r requirements-dev.txt
pytest

Or run the dependency-free smoke test:

python smoke_test.py

Project layout

src/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

Roadmap (not in this MVP)

  • prompts/list / prompts/get gating (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

Security note

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.

Contributing & license

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.

About

Nobody knows who an AI agent is, what it's allowed to do, or who's responsible when it does something wrong. This is arguably the single most urgent unsolved infrastructure problem in AI right now. When Agent A invokes Agent B, which calls a tool, which triggers a payment — every hop must be cryptographically provable back to an accountable human.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages