Repository navigation
Docs: the recommended GitHub Actions workflow installs mcp-publisher unpinned and unverified, in a job that holds the publishing credential #1505
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-publisherThree 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.gzPinned version, download to disk rather than piping, verify, then extract.
Suggested changes to the page
- Pin the version and verify the signature, as above. This is the substantive one — it turns
"trust whateverlatestresolves to" into "trust a signature this project produced". - Move the publishing step into its own job, so the step that runs a downloaded binary is not
the step holdingid-token: writeor a private key.chrome-devtools-mcpsplit it into a
separate workflow entirely, which is stronger still. - Consider pinning the Actions by digest — the examples use
actions/checkout@v5and
actions/setup-node@v5, and GitHub's own hardening guidance recommends a full commit SHA. - If a
mcp-publisherGitHub 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.
Activity
docs(security): pin and verify mcp-publisher installer (modelcontextp…