Skip to main content
MCP-ATTEST — the authorization conformance harness for MCP servers. One command, every identity, real calls, evidence you can hand to an auditor.

Prove that your MCP server denies what it must deny.

One command · every identity · real calls · twenty-four rules · zero dependencies · MIT

Install · Run it · The rules · OAuth · Suites · Policy · Evidence · CI · Specification · Website · Русский

CI Latest release MIT license PyPI version Python 3.10+

mcp-attest test output against a vulnerable MCP server: alice was allowed to call refund_order although the policy denies it, the tool description hides an instruction from the user, the ledger export returns an AWS key

Real output from the bundled vulnerable server. Exit code 1 means a blocking finding. Examples · Why this exists.

mcp-attest tests a live MCP server against the access control you wrote. It connects as each identity in your policy, sends real calls, and reports every place where the server's decision differs from the decision your policy requires.

It is not a gateway. It cannot block anything. It proves what your server does.

Why mcp-attest?

  • It tests the decision, not the definition. A scanner tells you what a tool description looks like. mcp-attest tells you whether alice may call refund_order — by calling it, as alice, and looking at what came back.
  • It covers the OAuth half of the standard too. Wrong audience, wrong issuer, wrong scope, tampered signature, expired token, alg: none, a redirect URI that is not registered, a missing PKCE challenge, a dropped state, a token that must not reach the upstream. Twelve checks that the OWASP guide lists and no tool in the ecosystem performs.
  • The matrix is generated, not typed. Write who may call what. The harness derives every identity × tool pair, derives the arguments from the schema, and compares. Adding a tool to your server does not mean rewriting the test suite.
  • Deterministic. No model, no API key, no network beyond the server under test. The same policy and the same server give the same report every time, which is what makes it usable as a gate.
  • Evidence, not opinion. Every call is recorded with the request, the response, the identity, the timestamp and the policy decision. The records are hash-chained and optionally HMAC-signed, so an auditor can verify the bundle offline without trusting the machine that produced it.
  • 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. It speaks JSON-RPC itself instead of pulling an SDK into your pipeline.

Where this sits

The MCP testing space is crowded, and pretending otherwise would be dishonest. mcp-assert lints, tests and fuzzes servers over real transports with YAML assertions; mcp-fence scans configuration, inspects and fuzzes with a Docker sandbox; mcp-fuzz grades protocol robustness; MCP-Scan scans tool metadata for poisoning; promptfoo red-teams agents and MCP servers with a model in the loop; mcpsnoop and mcp-conformance judge recorded sessions.

What none of them does is the thing the OWASP MCP Security Testing Guide puts first in its minimum automated test set: the authorization matrix — every tool, role, scope, tenant and object, including identifiers that belong to someone else — and token validation. The guide names the tool it expects as a category: "Policy-as-code test harness — asserts allow/deny decisions against the documented policy, with stable reason codes."

That is this product. Full comparison, sources and the claims we cannot make.

mcp-attest flow: the policy is the expected side, the live server is the observed side, the comparison produces findings, an exit code and a signed evidence bundle

The policy is the expected side. The server is the observed side. The gap between them is the finding. Full specification.

Website · Specification · Positioning · Research notes · MIT license

Install

pipx install mcp-attest          # isolated CLI
uvx mcp-attest test --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 --command "python my_server.py"        # scaffold a policy from the live tools
attest test --command "python my_server.py" --policy policy.json

Against the bundled vulnerable server:

!! [CRITICAL] AT-POISON-001 tool 'refund_order' hides an instruction in its description
          The description contains text that matches 'hides the call': 'do not tell the user'.
          A tool definition is read by the model, so this is an instruction channel that the
          user never sees.
          fix: Remove the instruction from the description. The tool's description should state
               what it does, not what the model must do.

!! [CRITICAL] AT-AUTHZ-001 'alice' was allowed to call 'refund_order'
          The policy denies 'refund_order' for this identity (tool is on the deny list), but the
          server executed the call. This is broken access control, not a policy tuning problem.
          fix: Enforce the decision inside the server's tool dispatcher, before the handler runs.

!! [CRITICAL] AT-AUTHZ-002 case AUTHZ-CROSS-01: 'alice' reached an object it must not reach
          The case expects 'get_order' to be refused for 'alice' (order B-77 belongs to bob,
          not to alice), but the server executed the call.
          fix: Check ownership of the object at the point of use, not only the role of the caller.

summary: critical=4, high=2, medium=4, low=3
tested: 2 identities, 6 tools, 12 calls

Exit code 1 means a blocking finding exists. --json prints machine-readable findings, --sarif writes SARIF 2.1.0 for code scanning, --strict treats medium findings as blocking.

Command Purpose
attest test Test a live server against a policy, as every identity in it
attest init Write a starter policy, optionally from the live tool list
attest suite Export, validate and run a portable conformance suite
attest verify Verify an evidence bundle offline
attest check Audit a recorded 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 posture Measure public MCP servers without calling a tool on them
attest rules List the enforced rules

Check an export you already have

If your agent already emits OpenTelemetry spans, attest check reads the export directly. You do not need a second instrumentation path to prove what your agent was allowed to do:

attest check --policy policy.json --otlp otel-export.json

The reader takes a single OTLP JSON payload or newline-delimited payloads, and skips everything that is not an MCP span with a warning. It understands four span names:

Span Attributes Meaning
mcp.tool.call mcp.actor, mcp.tool.name, mcp.tool.arguments, mcp.policy.decision, mcp.policy.reason one tool invocation and the decision it received
mcp.tools.list mcp.actor, mcp.tools the tool list an actor received
mcp.client.servers mcp.actor, mcp.servers the servers a client is configured with
mcp.gateway.route mcp.server, mcp.tools the tools a gateway routes for a server

The rules

Live rules run against a server you point it at. Each maps to a test in the OWASP MCP Security Testing Guide.

ID Rule OWASP The failure it catches
AT-AUTHZ-001 policy enforced per identity MCPSTG-AUTHZ-01 The server allows what your policy denies
AT-AUTHZ-002 cross-user objects refused MCPSTG-AUTHZ-03 A valid identifier belonging to another principal
AT-TOKEN-001 tokens validated MCPSTG-AUTHN-04 A malformed, tampered or expired token gets through
AT-POISON-001 no hidden instructions MCPSTG-TOOL-01 Instructions smuggled into tool metadata
AT-DRIFT-001 no manifest drift MCPSTG-SUPP-01 A tool added or changed after approval (rug pull)
AT-SECRET-001 no secrets in output MCPSTG-SECR-01 A credential in the tool output
AT-FAILCLOSED-001 fail closed on invalid input MCPSTG-CONF-01 A call executed with missing required arguments
AT-INJECT-001 no injected instructions in output MCPSTG-TOOL-01 Instructions aimed at the model inside a tool result
AT-SINK-001 guarded execution sinks MCPSTG-TOOL-02 Model input reaching a shell, a file or a URL
AT-SCHEMA-001 strict tool schemas MCPSTG-TOOL-04 Untyped, unbounded or free-form arguments
AT-APPROVAL-001 declared risk and approval MCPSTG-AUTHZ-02 A state-changing tool that declares no risk
AT-ERROR-001 clean error messages MCPSTG-CONF-03 Stack traces and internal paths in errors

Trace rules run against a recorded session and are unchanged since 0.1: C1 default-deny, C2 list/call parity, C3 narrowing only, C4 credential separation, C5 fail closed, C6 enforcement parity, C7 shadow MCP, C8 receipt chain, C9 description integrity.

The OAuth checks

The second half of the OWASP minimum set. They run when the policy carries an oauth block, and they need a server that speaks Streamable HTTP.

ID Rule OWASP The failure it catches
AT-OAUTH-001 a token is required MCPSTG-AUTHN-01 A request with no bearer token is accepted
AT-OAUTH-002 the signature is verified MCPSTG-AUTHN-02 A tampered signature is accepted
AT-OAUTH-003 unsigned tokens are refused MCPSTG-AUTHN-03 An alg: none token is accepted
AT-OAUTH-004 expiry is enforced MCPSTG-AUTHN-05 An expired token is accepted
AT-OAUTH-005 audience is enforced MCPSTG-AUTHN-06 A token minted for another audience is accepted
AT-OAUTH-006 issuer is enforced MCPSTG-AUTHN-07 A token from another issuer is accepted
AT-OAUTH-007 scope is enforced MCPSTG-AUTHZ-04 A tool runs without the scope it requires
AT-OAUTH-008 protected resource metadata MCPSTG-AUTHN-08 The refusal does not point at the metadata (RFC 9728)
AT-OAUTH-009 PKCE is required MCPSTG-AUTHN-09 A code is issued without an S256 challenge
AT-OAUTH-010 redirect_uri is validated MCPSTG-AUTHN-10 A code is issued for an unregistered redirect URI
AT-OAUTH-011 state is echoed MCPSTG-AUTHN-11 The authorization response drops the state
AT-OAUTH-012 no token passthrough MCPSTG-AUTHN-12 A token for this server is accepted by the upstream

Three of them need the signing key, because a token that is expired or minted for the wrong audience must be correctly signed — otherwise the server can refuse it for the wrong reason and the test proves nothing:

"oauth": {
  "issuer": "https://auth.example.com/",
  "audience": "mcp://orders",
  "signing_key_env": "MCP_ATTEST_OAUTH_KEY",
  "authorization_endpoint": "http://127.0.0.1:8931/authorize",
  "redirect_uri": "http://127.0.0.1:8931/callback",
  "resource": "http://127.0.0.1:8931/mcp",
  "upstream": "http://127.0.0.1:8932/mcp",
  "required_scopes": { "refund_order": "orders:write" }
}

Without the key those three are reported as skipped, never as passed. A test that did not run is not a test that passed.

A suite other harnesses can run

A policy file is written for one tool. A suite is the portable projection of it: who may call what, as whom, and what must come back. Any harness that can open an MCP session and compare a decision with an expectation can run it — and the schema is published, so "any harness" means something concrete.

attest suite export --policy policy.json --out suite.json --name orders-authorization
attest suite validate --suite suite.json
attest suite run --suite suite.json --url https://mcp.example.com/mcp

The schema lives at docs/mcp-attest-suite.schema.json. Credentials stay as ${VAR} references, exactly as in the policy.

How the public servers look

attest posture measures the servers the official MCP registry lists, without calling a tool on any of them: at most three well-formed requests per server, one connection, a pause in between. No fuzzing, no malformed payloads, no tool calls — so nothing can be changed on a server that is not yours.

attest posture --stride 9 --out posture.md --json posture.json

The published report carries aggregates and the method, and no host names: a server that accepts an unauthenticated session is a disclosure, and disclosure is the operator's decision. The scan ships so that anyone can reproduce it.

The policy file

One file holds both sides of the comparison: what each identity may call, and how that identity presents itself.

{
  "version": 1,
  "default_decision": "deny",
  "clients": {
    "alice": {
      "tools": {
        "allow": ["list_orders", "get_order"],
        "deny": ["refund_order"]
      },
      "identity": {
        "headers": { "Authorization": "Bearer ${ALICE_TOKEN}" }
      }
    }
  },
  "cases": [
    {
      "id": "AUTHZ-CROSS-01",
      "identity": "alice",
      "tool": "get_order",
      "args": { "order_id": "B-77" },
      "expect": "deny",
      "reason": "order B-77 belongs to bob, not to alice"
    }
  ],
  "probes": {
    "get_order": { "order_id": "A-1" }
  }
}
  • identity — how to present yourself. headers for HTTP servers, env for stdio servers. ${VAR} is expanded from the environment, so credentials never live in the file.
  • cases — explicit expectations that a generated probe cannot express: an identifier owned by someone else, an amount above a threshold, a tool that must be refused.
  • probes — the arguments to send for a tool. Pinning arguments is also how you opt in to calling a tool that changes state.
  • oauth — what the server's OAuth layer must look like. Omit it when the server does not use OAuth; the OAuth checks then do not run.

Evidence

attest test --url https://mcp.example.com/mcp --policy policy.json --evidence bundle.json
attest verify --bundle bundle.json
# bundle: ok (14 records, key=yes)

Each record carries the request, the response, the identity, the timestamp, the expected decision and the observed decision, with anything that looks like a credential redacted. The records are hash-chained and, with MCP_ATTEST_KEY set, signed with HMAC-SHA256.

That is what turns "our agent policy is enforced" from a sentence in a security questionnaire into an artifact a third party can check.

Use it in CI

- uses: Balanov/mcp-attest@v0
  with:
    policy: .attest/policy.json
    command: "python my_server.py"
    sarif: mcp-attest.sarif

The action fails the job on any critical or high finding, and the SARIF output lands in code scanning next to the rest of the repository's security results.

What mcp-attest is not

  • Not a gateway, proxy or enforcement point. It cannot block a call.
  • Not a scanner. It does not judge a schema or a description; it exercises a server.
  • Not a model-based red team. Nothing here calls an LLM, so nothing here is non-deterministic or billable.
  • Not a PKI. A hash chain and an HMAC prove order and integrity, not identity.
  • Not a hosted service. No account, no telemetry, no network calls beyond the server under test.

Development

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

The suite starts real MCP servers — one over stdio, one over HTTP, one with broken token checks, one with a full OAuth layer where every check can be switched off on its own — and asserts on what the harness finds. CI runs it on Python 3.10–3.13 and publishes 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.

Metadata

Release files for mcp-attest 1.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcp-attest 1.2.1
File Size Uploaded Provenance Actions
mcp_attest-1.2.1.tar.gz 109.8 kB With provenance Details for mcp_attest-1.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mcp-attest 1.2.1
File Interpreter ABI Platform Actions
mcp_attest-1.2.1-py3-none-any.whl Python 3 none any Details for mcp_attest-1.2.1-py3-none-any.whl

Total release size: 184.4 kB

Release files / mcp_attest-1.2.1.tar.gz

Download URL mcp_attest-1.2.1.tar.gz
Size 109.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c32fdd5369d85669965ce9dfe59a76a46d83d960b5477d39376d63888152c52d
BLAKE2b-256 checksum
How to use checksums
1380dae25c4a62084f231541c6b4721da9a7978f4b6c7987bcb0dceef147b1a5
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 14, 2026.

Transparency log

Release files / mcp_attest-1.2.1-py3-none-any.whl

Download URL mcp_attest-1.2.1-py3-none-any.whl
Size 74.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
311d47e9b3ce0b98b0d74ca54cc763524fc5e5a12eed906b4768e35003341658
BLAKE2b-256 checksum
How to use checksums
7aac70abc8091aa6ad202d01b4a9e2678e2873333048163eae63b0e2b67e8e75
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 14, 2026.

Transparency log

Security for mcp-attest 1.2.1

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.

Select from 2 files

Attestation for mcp_attest-1.2.1.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 14, 2026.

Transparency log

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

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page