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
- 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".
- 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.
- 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.
- 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.
Automate Publishing with GitHub Actions is
the workflow most servers copy, and all three authentication variants share this step:
Three properties, and the third is the one that makes the first two matter.
1.
releases/latestis mutable. Resolving the exact URL the doc uses: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.0ships, forthe exact tarball the doc downloads:
The verification material exists and is published for a reason. The documented workflow fetches the
first file and ignores the other two. Piping
curlstraight intotar xzalso means there is nopoint 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:
./mcp-publisherrunspermissions: id-token: writeat job level, so any step in the job can mint an OIDC token./mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }}— passed on the command line./mcp-publisher login dns --domain … --private-key ${{ secrets.MCP_PRIVATE_KEY }}— an Ed25519 private key, on the command lineSo whoever controls the contents of
releases/latest— maintainers here, or anyone who compromisesthis 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-mcphit exactly this and fixed it today inPR #2474. Their result is a good
template for the doc:
Pinned version, download to disk rather than piping, verify, then extract.
Suggested changes to the page
"trust whatever
latestresolves to" into "trust a signature this project produced".the step holding
id-token: writeor a private key.chrome-devtools-mcpsplit it into aseparate workflow entirely, which is stronger still.
actions/checkout@v5andactions/setup-node@v5, and GitHub's own hardening guidance recommends a full commit SHA.mcp-publisherGitHub Action were published, most of this would collapse into one pinneduses: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 patterncame 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.