mcp-attest

mcp
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Conformance and attestation harness for MCP agent access control: prove that a policy actually enforces.

README.md
MCP-ATTEST — the conformance harness for MCP agent access control. Policy says deny, the gateway allowed it: nine checks replay your trace against your own rules.

Prove that your MCP policy actually enforces.

Two input files · nine checks · zero runtime dependencies · MIT

Install · Run it · The nine checks · CI · Specification · Website · Русский

CI Latest release MIT license PyPI version Python 3.10+

mcp-attest check output on a recorded trace: C6 policy denied the call but the gateway allowed it, C9 tool definition changed after approval, summary critical=7 high=6

Real output from examples/session.jsonl. Exit code 1 means a blocking finding. Examples · Why this exists.

mcp-attest is a conformance harness for MCP agent access control. It takes the policy you wrote and the trace of what actually happened, and reports every point where they disagree.

It is not a gateway. It does not sit on the request path, and it never holds a credential for the upstream server. It reads evidence that already exists.

Why mcp-attest?

  • It catches the accepted-but-not-enforced rule. C6 re-evaluates your policy locally against every recorded call. A call your policy denies but the gateway allowed is invisible in every dashboard, because dashboards show what the gateway believed.
  • Evidence, not opinion. Every finding carries the exact tool, arguments and decision that produced it, and the fix.
  • Offline by construction. No network calls, no telemetry, no account. Traces carry payloads and stay where they are.
  • Exit codes, not vibes. 0 clean, 1 blocking finding, 2 bad input. It is a CI gate without a wrapper script.
  • Zero dependencies. Python 3.10+ and the standard library.
  • Tamper-evident receipts. Optional hash-chained, HMAC-signed receipts for every allowed call.
mcp-attest flow: the policy and the trace are replayed locally and compared, producing findings, an exit code and a receipt chain

Nothing is proxied. The trace is the evidence. Full specification.

Website · Specification · Research notes · MIT license

Install

pipx install mcp-attest          # isolated CLI
uvx mcp-attest check --help      # one-shot
pip install mcp-attest           # into a virtualenv

From source:

git clone https://github.com/Balanov/mcp-attest.git
cd mcp-attest
python -m pip install -e ".[dev]"
python -m unittest discover -s tests -v

Run it

attest init                                            # write a starter policy
attest check --policy examples/policy.json --trace examples/session.jsonl
!! [CRITICAL] C6 Policy denies 'shell__exec' but the gateway allowed it
          Local evaluation of the policy says deny (tool is on the deny list); the trace
          records allow ('argument-conditioned rule matched'). This is the 'policy accepted
          but not enforced' failure mode — a rule that is registered but never applied.
          fix: Replay the call against the gateway with the same arguments and fix
               the rule binding, then add this call as a regression case in CI.

summary: critical=7, high=6

Exit code 1 means a blocking finding exists. --json prints machine-readable findings; --strict treats high findings as blocking as well.

Command Purpose
attest init Write a starter policy
attest check Check a trace against a policy
attest receipts Build and verify a hash-chained receipt trail
attest shadow List the servers an agent can reach outside the gateway
attest rules List the enforced rules

The nine checks

ID Rule The failure it catches
C1 default-deny An unset allowlist silently meaning "allow everything"
C2 list/call parity A tool hidden from tools/list but callable by name
C3 narrowing only A per-request control widening a client's grant
C4 credential separation The client's token replayed upstream verbatim
C5 fail closed A call allowed because the policy check errored
C6 enforcement parity The policy says deny, the gateway allowed it
C7 shadow MCP A server the agent can reach outside the gateway
C8 receipt chain Allowed calls with no verifiable receipt, or a broken chain
C9 description integrity A tool definition changed after approval (rug pull)

Input files

Policy

{
  "version": 1,
  "default_decision": "deny",
  "clients": {
    "ci-bot": {
      "tools": {
        "allow": ["fs__read_file", "fs__list_directory"],
        "deny": ["fs__write_file", "fs__delete_everything"],
        "conditional": [
          {
            "tool": "fs__write_file",
            "allow_when": { "path": { "starts_with": "./out/" } },
            "action": "allow",
            "reason": "writes are allowed only inside ./out/"
          }
        ]
      },
      "egress_hosts": ["api.internal.example.com"]
    }
  },
  "gateway": { "mode": "routed", "fail_closed": true, "passthrough_credentials": false },
  "receipts": { "required": true, "key_env": "MCP_ATTEST_KEY" }
}

Condition operators: eq, not_eq, starts_with, ends_with, contains, in, not_in, gt, gte, lt, lte, glob, regex. A bare string ending in / is a prefix; a bare string containing * is a glob.

Trace

Newline-delimited JSON, one event per line. Any proxy, agent harness or CI step can produce it.

{"type":"tools_list","actor":"ci-bot","tools":[{"name":"fs__read_file","description_sha256":"a1b2…"}]}
{"type":"tool_call","actor":"ci-bot","tool":"fs__read_file","args":{"path":"/srv/README.md"},"decision":"allow","upstream":true,"reason":"tool is on the allow list"}
{"type":"client_servers","client":"claude-code","servers":["fs","git"]}
{"type":"gateway_route","server":"fs","tools":["read_file","list_directory"]}

Full specification: SPEC.md.

Receipts

export MCP_ATTEST_KEY=…
attest receipts --trace examples/session.jsonl --out receipts.jsonl
# chain: ok (6 receipts, key=yes)

Each receipt commits to the previous one, so removing or reordering a receipt breaks the chain. With MCP_ATTEST_KEY set, every receipt is additionally signed with HMAC-SHA256, so an auditor can detect edits without access to the gateway.

This is deliberately not a PKI. It answers the question an incident responder actually asks: did this session really contain these calls, in this order, and has anything been altered since?

Use it in CI

- uses: Balanov/mcp-attest@v0
  with:
    policy: .attest/policy.json
    trace: .attest/trace.jsonl

The action fails the job on any critical or high finding, so "our agent policy is enforced" becomes something the pipeline proves instead of something a security questionnaire asks about.

What mcp-attest is not

  • Not a gateway, proxy or enforcement point. It cannot block a call.
  • Not a scanner. It does not connect to your MCP server and sends no probe traffic.
  • Not a PKI. A receipt chain and an HMAC prove order and integrity, not identity.
  • Not a policy editor. It reads the policy you already have.
  • Not a hosted service. No account, no network calls, no telemetry.

Development

python -m pip install -e ".[dev]"
python -m unittest discover -s tests -v

CI runs the suite on Python 3.10–3.13 and publishes the package to PyPI from a tagged release through trusted publishing, with no API token in the repository.

License

MIT. Fork it, modify it, embed it, ship it.

Reviews (0)

No results found