Skip to content

Docs: the recommended GitHub Actions workflow installs mcp-publisher unpinned and unverified, in a job that holds the publishing credential #1505

Description

@bilguunbicktivism

Automate Publishing with GitHub Actions is
the workflow most servers copy, and all three authentication variants share this step:

- name: Install mcp-publisher
  run: |
    curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher

Three properties, and the third is the one that makes the first two matter.

1. releases/latest is mutable. Resolving the exact URL the doc uses:

HTTP 302 -> https://github.com/modelcontextprotocol/registry/releases/download/v1.8.0/mcp-publisher_linux_amd64.tar.gz

Two runs of an unchanged workflow, either side of a release here, execute different bytes.

2. This repository already signs every asset, and the doc does not use it. v1.8.0 ships, for
the exact tarball the doc downloads:

mcp-publisher_linux_amd64.tar.gz                 7,337,300 B
mcp-publisher_linux_amd64.tar.gz.sbom.json          85,883 B
mcp-publisher_linux_amd64.tar.gz.sigstore.json      10,599 B

The verification material exists and is published for a reason. The documented workflow fetches the
first file and ignores the other two. Piping curl straight into tar xz also means there is no
point at which verification could happen — the bytes are extracted as they arrive.

3. The binary runs in a job that is holding a credential. Per variant, as documented:

variant what the job holds when ./mcp-publisher runs
OIDC (recommended) permissions: id-token: write at job level, so any step in the job can mint an OIDC token
PAT ./mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }} — passed on the command line
DNS ./mcp-publisher login dns --domain … --private-key ${{ secrets.MCP_PRIVATE_KEY }} — an Ed25519 private key, on the command line

So whoever controls the contents of releases/latest — maintainers here, or anyone who compromises
this project's release pipeline — controls a binary that every follower of this doc runs next to
their publishing credential.

I am not reporting a compromise, and I am not implying one. This is about the recommended
configuration: the download is unpinned and unverified, the signature that would fix it is already
published, and the doc places that download in the highest-privilege job in the workflow.

A reference fix already exists

ChromeDevTools/chrome-devtools-mcp hit exactly this and fixed it today in
PR #2474. Their result is a good
template for the doc:

- name: Install Cosign
  uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0

- name: Install MCP Publisher
  run: |
    export OS=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
    export VERSION=v1.8.0
    curl -L -o mcp-publisher.tar.gz \
      "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${OS}.tar.gz"
    curl -L -o mcp-publisher.tar.gz.sigstore.json \
      "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${OS}.tar.gz.sigstore.json"
    cosign verify-blob mcp-publisher.tar.gz \
      --bundle mcp-publisher.tar.gz.sigstore.json \
      --certificate-identity-regexp="https://github.com/modelcontextprotocol/registry/\.github/workflows/.*" \
      --certificate-oidc-issuer="https://token.actions.githubusercontent.com"
    tar xzf mcp-publisher.tar.gz mcp-publisher
    rm mcp-publisher.tar.gz

Pinned version, download to disk rather than piping, verify, then extract.

Suggested changes to the page

  1. Pin the version and verify the signature, as above. This is the substantive one — it turns
    "trust whatever latest resolves to" into "trust a signature this project produced".
  2. Move the publishing step into its own job, so the step that runs a downloaded binary is not
    the step holding id-token: write or a private key. chrome-devtools-mcp split it into a
    separate workflow entirely, which is stronger still.
  3. Consider pinning the Actions by digest — the examples use actions/checkout@v5 and
    actions/setup-node@v5, and GitHub's own hardening guidance recommends a full commit SHA.
  4. If a mcp-publisher GitHub Action were published, most of this would collapse into one pinned
    uses: line and servers would get it by default.

Happy to open a PR against the docs with the OIDC variant rewritten if that is useful.

How I got here

I reported the copied version of this in chrome-devtools-mcp; a maintainer there noted the pattern
came from this page and suggested raising it upstream, which is what this is. Credit to
@OrKoN for pointing at the root rather than fixing only their own copy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions