mcp-attest
Product page: balanov.pro/mcp-attest — one-page overview, install and live examples.
Prove that your MCP policy actually enforces.
mcp-attest is a conformance and attestation harness for agent access control.
It takes the policy you wrote and the trace of what actually happened, and
reports every point where they disagree — including the failure modes that
gateways accept silently: a rule that was registered but never enforced, a
tool that is hidden from tools/list but still callable, a client that reaches
a server outside the gateway, and a tool definition that changed after it was
approved.
pipx install mcp-attest
attest init
attest check --policy policy.json --trace trace.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.
!! [CRITICAL] C9 Tool 'fs__read_file' changed after it was listed
The description hash of an already-approved tool changed between two
listings. This is the shape of a rug pull: the tool was harmless at
approval time.
summary: critical=7, high=6
Exit code is 1 when a blocking finding exists, so it works as a CI gate
without a wrapper script. The session above is examples/session.jsonl — run
attest check --policy examples/policy.json --trace examples/session.jsonl to
reproduce it.
Why this exists
Gateways, policy engines and allowlists for Model Context Protocol are now
everywhere — Docker MCP Gateway, agentgateway, Bifrost, Runlayer, Lasso, Obot,
and GitHub's own gh-aw-mcpg. The policy language is not the problem.
The problem is that nobody verifies the policy is enforced. In a detailed evaluation of agentgateway published in October 2026, an argument-conditioned policy was "reported as accepted and still did not enforce what it said", and the conclusion was blunt: "turning a policy on is not the end of the job — send real calls and check."
The same evaluation found policies that were accepted and had no effect at all. The State of MCP Security 2026 survey describes the structural gap: "Gateways only protect the servers they route. An agent calling an MCP server outside the gateway is invisible," and "Most gateways also do not do deep content inspection of responses. The gateway enforces who and where, not what."
Meanwhile the threat volume is not theoretical: more than 40 CVEs against MCP implementations were disclosed in the first four months of 2026, an estimated 200,000 servers are exposed, 38% of scanned MCP servers had no authentication on critical endpoints, and in April 2026 researchers hijacked Claude Code, Gemini CLI and GitHub Copilot through malicious pull-request titles — with bug bounties paid by the vendors.
And the buyers of this category are being told the same thing. The US DoD's Center for Security and Information Technology wrote that "without these traceable audit logs, incident response and accountability become significantly more difficult, especially in multi-tenant environments."
So the missing primitive is small, sharp and useful: a harness that takes a policy and a trace and tells you, with evidence, whether your control plane does what you wrote down.
mcp-attest is that harness. It is not a gateway, it does not sit on the hot
path, and it does not need to be trusted with your traffic — it reads what
already happened.
What it 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) |
C6 is the one that matters most, and it is the one no existing tool does: it re-evaluates your policy locally against every recorded call and flags any call whose observed decision disagrees with the policy. That catches the "accepted but not enforced" class directly.
Install
pipx install mcp-attest # isolated CLI
uvx mcp-attest check --help # one-shot
pip install mcp-attest # into a virtualenv
Python 3.10+, zero runtime dependencies.
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 the security questionnaire asks about.
Policy format
{
"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 format
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
attest receipts --trace trace.jsonl --out receipts.jsonl produces a
hash-chained receipt per allowed call. 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.
export MCP_ATTEST_KEY=…
attest receipts --trace trace.jsonl --out receipts.jsonl
# chain: ok (12 receipts, key=yes)
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?
Commands
| Command | Purpose |
|---|---|
attest init |
Write a starter policy |
attest check |
Check a trace against a policy (--json, --strict) |
attest receipts |
Build and verify a hash-chained receipt trail |
attest shadow |
List paths that bypass the control point |
attest rules |
List the enforced rules |
Design principles
- Read-only. It never sits in the request path and never holds a credential for the upstream. It reads evidence.
- Evidence over opinion. Every finding carries the exact line, tool, arguments and decision that produced it.
- Offline. No network calls, no telemetry, no account. Traces contain payloads and stay where they are.
- Exit codes, not vibes.
0clean,1blocking finding,2bad input.
Development
python -m venv .venv && . .venv/Scripts/activate
pip install -e ".[dev]"
python -m unittest discover -s tests -v
License
MIT. See LICENSE.
Metadata
Release files for mcp-attest 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mcp_attest-0.1.2.tar.gz | 22.5 kB | With provenance | Details for mcp_attest-0.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mcp_attest-0.1.2-py3-none-any.whl | Python 3 | none | any | Details for mcp_attest-0.1.2-py3-none-any.whl |
Total release size: 43.3 kB
Release files / mcp_attest-0.1.2.tar.gz
| Download URL | mcp_attest-0.1.2.tar.gz |
|---|---|
| Size | 22.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2127daecf5e3a1903ea2b774d547fe045453fe628ea7c5d9e6b878b59f8622da
|
|
BLAKE2b-256 checksum How to use checksums |
4a5124ece248e4adc767deb82453b721eb5c7fb096b6434eb27d9ceab0ff199e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.
Transparency logRelease files / mcp_attest-0.1.2-py3-none-any.whl
| Download URL | mcp_attest-0.1.2-py3-none-any.whl |
|---|---|
| Size | 20.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc622a9634064724f961879077e1b5d2e00d348cdd5fcbae25caf475d3dc4524
|
|
BLAKE2b-256 checksum How to use checksums |
97c8cc462878fed4a91d5b3f482c400b511734a10d4c570eed15e3344417fd8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.
Transparency logSecurity for mcp-attest 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for mcp_attest-0.1.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.