modelcontextprotocol / modelcontextprotocol/registry

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

Open
#1,505 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
7.3k
Forks
994
Avg merge
4d 12h
Merged PRs (30d)
19

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Automate Publishing with GitHub Actions page and review its OIDC, PAT, and DNS workflow variants. Compare the download and verification sequence with the linked ChromeDevTools PR #2474. Done means the documented publisher version is pinned, its signature is verified before extraction, and downloaded code is isolated from publishing credentials; consider the documented action pinning suggestion too.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, shell
Domain
ci-cd, documentation, security
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.