Skip to content
verify mcp Beta VerifyMCP is currently in beta. If you notice any issues, get in touch and we’ll put it right.

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:

Cryptographically verified build provenance (signed and bound to the source repository)
Provenance check failed: no build-provenance attestation is published.
A provenance attestation was published but failed verification
Provenance check failed: the declared version isn't published, so its provenance can't be verified.
Provenance check failed: the registry has no such package, so there's no artefact to verify.
Build provenance not yet verified.

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 }}

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 .nupkg before any dotnet 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 .nupkg you push. A RID-specific .NET tool ships as a root package that holds no executable, only a DotnetToolSettings.xml pointing at one platform package per runtime (for example <id>.linux-x64 and <id>.win-arm64), and dotnet tool install fetches 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 .nupkg files 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 any build/*.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-provenance makes) 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 sign with 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.

Prompt
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

Written by Stuart Blackler · Last reviewed 26 September 2026.