Is this MCP server safe to install? A signed answer anyone can check.
Agents install MCP servers by name, and the code behind a name can change after it is approved. ATPP scans every server in the official MCP registry every day and publishes, for each one, a signed claim about what its listing declares and what it actually installs. Anyone can verify the claim offline, without trusting our servers, and a company can gate a launch on it.
Trust attaches to a name. The bytes behind it move.
A team approves io.example/weather once. After that, the client installs whatever that name resolves to: a newer version, a repointed container tag, a package published by a new owner. In our scan of 12 October 2026, 1,111 servers reach a different domain or package owner than when they were first published (a listing swap), 222 listings name a package version that does not exist, and 33 packages were built from a different repository than the one their listing declares.
style="margin-top:18px">A README badge cannot fix this on its own: it is an image fetched from somebody's server, and the server decides what it shows. So the claim does not live in the image. It is a signed file, bound to the exact listing and artifacts that were scanned, that expires, and that any verifier can check against a key it already holds.
- One claim per mark: a score says "passed N of M published checks on a date", never "safe".
- Offline-verifiable: the verifier carries our public key; it does not need to ask us.
- Unbuyable and red only on proof: every public server is scored whether or not its publisher asked, and paying changes nothing.
- Time-bounded, method published, disputes logged: a claim expires after 7 days; the rules and the dispute log are public. The charter.
From the registry to a signed, anchored claim
- FetchA GitHub Actions job runs daily at 21:20 UTC and downloads every version of every server from
registry.modelcontextprotocol.io. - ResolveFor each listed package it records the exact artifact: npm integrity hash, PyPI file sha256s, or the container digest behind the tag, plus the repository named in npm or PyPI Sigstore build provenance.
- ScoreRules (
atf-6) run against the manifest and the resolved artifacts. For remote servers, the scan also reads the advertised tool list (handshake andtools/listonly). No tool is called and nothing is installed or executed. - SignEach score becomes an in-toto statement in a DSSE envelope, signed with Ed25519, and is published to the badge service.
- AnchorThe day's manifest (the hash of every claim) is recorded in Sigstore Rekor, committed to a public Git log, and co-signed keyless by a public workflow.
The checks (rules atf-6)
| Check | Fails when | Applies to |
|---|---|---|
static-secrets | The server authenticates with a long-lived key in an env var or header | All |
secret-no-default | A field marked secret ships a default value | Servers with secret fields |
transport-tls | A remote endpoint is plain http:// | Remote servers |
version-pinned | A package is a range or latest, not an exact version | Packages |
source-published | No source repository is declared | All |
artifact-hash | An MCPB bundle has no content hash | MCPB |
oci-digest | A container image is referenced by a mutable tag, not @sha256: | OCI images |
target-stable | The current version reaches a different domain or package owner than the first one (listing swap) | Servers with history |
artifact-resolves | A listed npm or PyPI version was never published | npm, PyPI |
build-provenance | The Sigstore build record names a repository other than the declared one (a missing record is noted, not failed) | npm, PyPI with provenance |
tools-stable | A remote server's tool definitions changed while its version did not (the rug-pull pattern, OWASP MCP03) | Remote servers that list tools without credentials |
unchanged-since-last-scan | The manifest changed since the previous scan | From the second scan |
The claim
An in-toto Statement v1 in a DSSE envelope. The subject is mcp:<namespace>/<server> with the sha256 of the exact registry manifest that was scanned, so anyone can re-hash the live listing and compare.
predicate: {
"version": "0.0.82", "rules_version": "atf-5", "passed": 6, "total": 6,
"checks": [["target-stable", true, "points at the same domain and package owner as the first version"], …],
"artifacts": [["npm", "@playwright/mcp", "0.0.82", "sha512-OCqf…", "microsoft/playwright-mcp"]],
"tools": {"sha256": "…", "count": 21}, // remote servers that list tools without credentials
"as_of": "2026-10-12", "valid_until": "2026-10-19",
"scope": "static manifest scan — not a runtime claim", "badge_sha256": "…"
}
Keys and transparency
- Root key
atpp-root-2026is held offline and signs only the key list. The verifier embeds it, so a hijacked domain cannot swap keys. - Scan key
atpp-scan-2026-10signs claims. Since 12 October 2026 it lives only as a GitHub Actions secret used by the scan job; it is listed, rotatable and revocable in the signed key list. - Daily manifest commits to the hash of every claim issued that day. Its hash is committed to github.com/bl014h/atpp/log and anchored in Sigstore Rekor, so a claim cannot be backdated, swapped or replayed later, even by us.
- Keyless co-signature: a read-only workflow in the public repository signs each day's manifest with Sigstore keyless signing (GitHub OIDC identity, short-lived certificate, Rekor entry).
- The web servers hold only public keys. The badge service renders grey unless a signature verifies; it cannot mint a claim.
Where the same claim appears
| Surface | What it does | Status |
|---|---|---|
| README badge | Pinned (two committed files, nothing fetched) or live (follows each scan) | live |
| Verify page | /v/mcp/<namespace>/<server>: checks, what it installs, the signed claim | live |
| ChatGPT and Claude | The Redthread connector's check_mcp_server verifies the signature before answering | live |
atpp CLI | verify, check-config (with a CycloneDX AIBOM), badge, report | live |
| Launch gate | atpp gate, hooks for Claude Code and Cursor (atpp hook), the CI action's check-config mode, the ToolHive sidecar's --gate | preview |
| Org policy | Allow-list and pinned digests; exported as a ToolHive permission profile | preview |
Check, pin and gate in a few commands
One dependency (cryptography), one file, no account.
pip install cryptography
curl -sO https://raw.githubusercontent.com/bl014h/atpp/main/atpp.py
python3 atpp.py check-config # every MCP server in your Claude, Cursor, VS Code, Windsurf config
python3 atpp.py verify io.github.example/server # signature, live registry hash, public log, Rekor, expiry
python3 atpp.py gate io.github.example/server --image ghcr.io/example/server@sha256:… --strict # exit 0 allow, 1 deny
In CI
Fail a pull request that adds an MCP server needing review. Pin the action by commit SHA.
- uses: bl014h/atpp@<commit-sha>
with:
mode: check-config
configs: .mcp.json .cursor/mcp.json .vscode/mcp.json
fail-on-unscanned: "false"
With ToolHive preview
python3 atpp.py profile io.github.example/server --policy org.json > profile.json
thv run --isolate-network --permission-profile profile.json io.github.example/server
python3 toolhive/atf_receipts.py serve --gate io.github.example/server --gate-policy org.json # failure_policy: fail
The policy file is yours: {"servers": {"io.github.example/server": {"image": "…@sha256:…", "egress": ["api.github.com"], "read": ["/workspace"]}}}. The gate denies servers not on it and images other than the pinned one, on top of the scan checks.
If you publish a server
- Pin exact versions, and container images by
@sha256:digest. - Publish from CI with provenance (
npm publish --provenance, PyPI trusted publishing) from the repository your listing declares. - Ship no default secrets; prefer short-lived, scoped credentials.
- Add the badge from your verify page. Disagree with a result? [email protected]; outcomes go to the public dispute log.
Verify the keyless co-signature
pip install sigstore==4.5.0
python -m sigstore verify identity --bundle log/DATE.manifest.json.sigstore.json \
--cert-identity https://github.com/bl014h/atpp/.github/workflows/keyless.yml@refs/heads/main \
--cert-oidc-issuer https://token.actions.githubusercontent.com log/DATE.manifest.json
What you can rely on, and what you cannot
Your teams already install third-party MCP servers into agents that hold credentials and data. ATPP gives you an independent, checkable record of what those servers are, without installing anything in your estate.
| Risk | What ATPP gives you | Status |
|---|---|---|
| An approved server changes hands (listing swap) | target-stable on every claim; !! in check-config; the gate denies | detect gate |
| What runs is not what was approved | Exact artifact digests in the claim; pin to them; the gate compares the image digest | detect gate |
| A package was not built from its declared source | build-provenance reads npm and PyPI Sigstore records | live |
| Long-lived credentials and default secrets | static-secrets, secret-no-default | live |
| Shadow MCP use across teams | check-config in CI on every repository; an allow-list policy | preview |
| A server reaching data it should not | Your policy, exported as a ToolHive permission profile and enforced by ToolHive's network isolation | preview |
| Tool poisoning and rug pulls (OWASP MCP03) | tools-stable flags a remote server whose tool definitions change without a new version; the gate and the Claude Code / Cursor hooks deny it | detect gate |
| Evidence for audit | Signed, dated claims and a CycloneDX AIBOM of the MCP servers in use; a public log and Rekor anchors you can cite | live |
Operating model
- Cadence: a full scan daily; a claim expires after 7 days and its badge turns grey.
- Data: only public registry metadata and package registries are read.
check-configreads your config locally and never uploads it. - Assurance you can check yourself: every claim verifies offline against a pinned key; an hourly canary and a daily red team run against production in public.
- Disclosure: findings about a specific server are handled under our disclosure policy; public research is aggregate only.
- Key custody: the root key is offline; the scan key is a GitHub Actions secret, rotatable and revocable through the signed key list.
Questions to ask any vendor in this space, including us
- Can I verify your verdict without calling your API?
- Does the verdict name the exact bytes, or only the package name?
- Is there a public, append-only record that stops you rewriting yesterday's verdict?
- Who decides what a server may reach: you, or my policy?
What a claim does not say
A level-1 claim is a static scan of a published listing and the artifacts it names. It does not review the code, it does not say the tool descriptions are honest, and it says nothing about what a server does once it runs. The gate and the policy are a preview of a decision, not a sandbox: they check the listing and the digest before a launch. Runtime behaviour belongs to your gateway and your isolation; level 2 and level 3 attestations come from the company running the agent, not from the publisher.
Source code and key history: github.com/bl014h/atpp. Registry figures: trust.redthreadsec.com/mcp-scan. Report a security issue: security.txt.