Attest your MCP package's build provenance
What we checked
We looked for build provenance: a signed attestation, published alongside your package, that cryptographically binds the artifact to the source commit and the CI run that built it. The signal passes only when that attestation exists and verifies. The other outcomes (no attestation, an attestation that fails verification, an unpublished version, or an inconclusive check) mean:
Why it matters
A registry account proves who uploaded a package; it doesn’t prove where the bytes came from. Build provenance closes that gap. A signed attestation lets anyone confirm the artifact was built from a specific commit in your public repository, by your CI, and hasn’t been swapped or tampered with since. That defends against a stolen publish token, a compromised maintainer laptop, or a malicious mirror, none of which can forge a valid signature bound to your repo.
How to fix it
Publish from CI and emit a signed attestation as part of the publish step. The
mature, turnkey paths today are npm and PyPI, both built on Sigstore and OIDC:
no long-lived signing keys to manage. For NuGet, a GitHub artifact attestation made with
actions/attest-build-provenance
gives you the same thing with one extra workflow step.
name: publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for provenance (OIDC)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
# --provenance generates and publishes a signed attestation
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} name: publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # required for Trusted Publishing + attestations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build && python -m build
# Uses PyPI Trusted Publishers (OIDC); publishes PEP 740 attestations
- uses: pypa/gh-action-pypi-publish@release/v1 name: publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required to sign the attestation (OIDC)
attestations: write # required to store it in this repository
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- run: dotnet pack -c Release -o artifacts
# For a RID-specific tool, make sure every platform package lands in artifacts/ too
# Attest each .nupkg exactly as dotnet pack produced it, before any signing
- uses: actions/attest-build-provenance@v2
with:
subject-path: 'artifacts/*.nupkg'
- run: dotnet nuget push "artifacts/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json For npm, also confirm your package.json repository field points at the same
public repo the workflow runs in; the attestation binds to it, and a mismatch
means the attestation exists but fails verification.
For PyPI, Trusted Publishing is what earns the pass. When you publish with
pypa/gh-action-pypi-publish from a Trusted Publisher, PyPI stores a
PEP 740 attestation for each file, which we
read from PyPI’s Integrity API. We credit two kinds of attestation: PyPI’s own
publish attestation (the one Trusted Publishing produces) and SLSA build
provenance. Either counts only once it verifies: the Sigstore signature and
transparency-log entry check out, it covers the exact file PyPI serves, and the
signing certificate names the same GitHub or GitLab repository your server
declares (its registry entry, or failing that a repository URL in your project
metadata, such as Source or Repository, or your homepage URL if you declare
no repository). Keep that URL pointing at the repository your publish workflow
runs in. A few details worth knowing:
- Every file counts. We check the attestation of every sdist and wheel in the release, each against its own file, and the release passes only when all of them verify and name the same repository. If some files are attested and others aren’t (for example, one uploaded by hand with an API token), the release is not verified. A release with no attested files scores as no attestation. Publishing everything from the same Trusted Publishing job avoids both.
- A file PyPI holds no PEP 740 attestation for is also looked up in GitHub’s
artifact attestations, by the file’s SHA-256, in the GitHub repository your
server declares (only that one). This covers files you attest in CI with
actions/attest-build-provenance. Only SLSA build provenance counts from GitHub (PyPI’s publish attestation counts only through PyPI itself), it is verified exactly as above, and the every-file rule still applies: a release can mix files attested on PyPI and on GitHub, provided every file verifies and names the same repository. This lookup is for repositories on GitHub only, and the attestation must be made in a public repository (see the NuGet notes below). - We verify up to 16 files. For a larger release we check every file (up to 128; beyond that the first 64 and the last 64) for an attestation. If any of them has none, the release is not verified. If all are attested, the result is inconclusive, because verifying that many files is beyond our limit, not a finding about your package. A release this large is checked through PyPI’s attestations only, not GitHub’s.
- If PyPI or its Integrity API is down, or your declared repository is on a host other than GitHub or GitLab, the result is inconclusive rather than a pass or a fail: we could not bind the attestation to your repository. If GitHub is down or rate-limiting us when we look a file up there, the result is inconclusive or checked again later, never counted against you.
- An attestation that verifies but names a different repository is reported as a metadata mismatch, not as forgery: usually the declared URL is out of date.
For NuGet, the pass comes from a GitHub artifact attestation over your
.nupkg, made in the workflow that runs dotnet pack (the NuGet tab above).
nuget.org adds its own repository signature (a .signature.p7s file) to every
package it hosts, which changes the package’s bytes, so we remove that entry
exactly as NuGet itself defines an unsigned package and look up the SHA-256 of
the result: the .nupkg your workflow built. A few details worth knowing:
- Attest the
.nupkgbefore anydotnet nuget sign. Author signing also adds a.signature.p7s, and removing it gives back the package as it was before signing, so an attestation made over the signed package can never match. If you sign, the order is pack, attest, sign, push. - Attest every
.nupkgyou push. A RID-specific .NET tool ships as a root package that holds no executable, only aDotnetToolSettings.xmlpointing at one platform package per runtime (for example<id>.linux-x64and<id>.win-arm64), anddotnet tool installfetches the platform package that holds the binary. Today we check the root package, but attest the platform packages as well, so the attestation covers what actually runs.subject-path: 'artifacts/*.nupkg'covers them all when they are packed into one folder. - Only SLSA build provenance over the
.nupkgfiles themselves counts. In-toto release attestations and SBOM attestations are not credited, and neither is an attestation over the binaries alone (for example, the checksums of your release assets): the package also carries the nuspec (its dependencies),DotnetToolSettings.xml(what runs, and which platform package is installed) and anybuild/*.targets, so the attestation has to name the package. - We look in the GitHub repository your server declares, and only there: its
registry entry’s repository, or failing that the repository or project URL
nuget.org lists for the package. The attestation must be SLSA build provenance
(what
actions/attest-build-provenancemakes) and passes only once it verifies: the Sigstore signature and transparency-log entry check out, it covers exactly that package, and the signing certificate names that same repository. One made in a different repository is reported as a metadata mismatch. - Attest from a public repository. GitHub signs a private repository’s attestations with its own Sigstore instance rather than the public one, and we verify only against the public one, so such an attestation reads as inconclusive: neither credited nor counted against you. The same applies to PyPI files and OCI images looked up in GitHub’s attestation store.
- A package whose archive is laid out in a way nuget.org’s signing would not produce is not looked up, and our diagnostics name the reason. So is a package over our 50 MiB download limit. Both are gaps on our side, not findings about your package.
- If GitHub is down or rate-limiting us, the result is inconclusive or checked again later, never counted against you.
- Without an attestation, NuGet reads as before. Author package signing
(
dotnet nuget signwith a code-signing certificate) proves the signer’s identity, not a repo-bound build, and nuget.org repository-signs every package, so almost every NuGet package carries a signature. Our diagnostics show whether a signature says it is an author or a repository signature, and the repository’s countersignature if there is one, for information only: we do not verify NuGet signatures, so a signature on its own reads as inconclusive and does not lift the score.
How we re-check
We re-fetch the attestation for your latest published version on our next crawl. Once a valid, repo-bound attestation is present, the signal flips to verified on the following score refresh.
Give this to your AI
Paste this into Claude Code (or any coding agent) from inside your server's repository. It states the failing signal, the outcome we re-check for, and the format the fix has to take.
Context: this repository publishes an MCP server. VerifyMCP's build provenance check failed, which means the published package carries no signed attestation binding it to the source commit and CI run that built it, or the attestation exists but doesn't verify. Goal: the published artifact carries a valid, repo-bound attestation, so the next crawl records verified provenance. Do this: 1. Identify the registry you publish to (npm, PyPI, NuGet, Go modules, crates.io, or an OCI container image). 2. For npm: publish from CI with `id-token: write` permission and run `npm publish --provenance` (add `--access public` only if the package is meant to be public and is not already); confirm the `repository` field in `package.json` points at the same public repo the workflow runs in, since a mismatch stops the attestation verifying. 3. For PyPI: configure Trusted Publishing (OIDC) and publish with `pypa/gh-action-pypi-publish`, which emits PEP 740 attestations. 4. For NuGet: in a GitHub Actions workflow in the repository the package declares, run `dotnet pack`, then attest the `.nupkg` it produced with `actions/attest-build-provenance` (`subject-path` pointing at the `.nupkg`, with `id-token: write` and `attestations: write` permissions), then push it. Attest every `.nupkg` you push, including each platform package of a RID-specific .NET tool, so check that they all land in the attested folder. It must be SLSA build provenance over the `.nupkg` files themselves: release or SBOM attestations, or ones over the binaries alone, do not count. If the package is also author-signed with `dotnet nuget sign`, attest before signing, never after: we check the package as `dotnet pack` produced it. 5. For Go or crates.io: there is no native equivalent of repo-bound build provenance today (Go relies on the `go.sum` checksum database; crates.io has no mechanism). Say so plainly rather than approximating one. 6. If you ship an OCI container image, attest the pushed image digest in CI with a keyless, signed SLSA provenance attestation: either `actions/attest-build-provenance` (with or without `push-to-registry: true`, provided it runs in the image's declared GitHub repository), or `cosign attest --type slsaprovenance`. A plain `cosign sign` signature and BuildKit's own unsigned `--provenance` output do not count. 7. Prepare the release, then stop and let me run the publish myself, since the check reads the published attestation. Rules: - Never run the publish command (`npm publish`, `twine upload`, `dotnet nuget push`, `cargo publish`, a release tag that triggers one) yourself. A published version is permanent and notifies every downstream consumer: prepare the change, show me the diff, and let me publish it. - Never fabricate, hand-craft, or hardcode an "attestation" file; it must come from a real signing step tied to the actual CI run and commit. - If the registry or ecosystem genuinely has no provenance mechanism, say so in your report rather than claiming the signal now passes. - Ask me before changing where or how the package is published (e.g. moving publishing into CI) if that touches secrets or release process you don't control. Report back: the workflow files you changed, the registry involved, and how I can verify the attestation myself (e.g. the provenance link on the npm package page, the PyPI attestations tab, or the Attestations page of the GitHub repository). Reference: https://verifymcp.io/docs/packages/build-provenance