from line 8
88
99In your server project directory, create a `.github/workflows/publish-mcp.yml` file. Here is an example for npm-based local server, but the MCP Registry publishing steps are the same for all package types:
1010
11The examples pin `mcp-publisher` to a specific release and verify its Sigstore
12bundle before extracting it. When upgrading, update `MCP_PUBLISHER_VERSION` to
13the intended release tag deliberately; do not switch back to the mutable
14`releases/latest` URL.
15
1116```yaml OIDC authentication (recommended) theme={null}
1217name: Publish to MCP Registry
1318
from line 26
2126 permissions:
2227 id-token: write # Required for OIDC authentication
2328 contents: read
29 env:
30 MCP_PUBLISHER_VERSION: v1.8.1
2431
2532 steps:
2633 - name: Checkout code
from line 56
4956
5057 ### Publish MCP server:
5158
52 - name: Install mcp-publisher
59 - name: Install cosign
60 uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
61
62 - name: Download and verify mcp-publisher
5363 run: |
54 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
64 set -euo pipefail
65 os="$(uname -s | tr '[:upper:]' '[:lower:]')"
66 arch="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
67 archive="mcp-publisher_${os}_${arch}.tar.gz"
68 base_url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
5569
70 curl --fail --location --silent --show-error --output "$archive" "$base_url/$archive"
71 curl --fail --location --silent --show-error --output "$archive.sigstore.json" "$base_url/$archive.sigstore.json"
72 cosign verify-blob \
73 --bundle "$archive.sigstore.json" \
74 --certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" \
75 --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
76 "$archive"
77 tar xzf "$archive" mcp-publisher
78 rm -f "$archive" "$archive.sigstore.json"
79
5680 - name: Authenticate to MCP Registry
5781 run: ./mcp-publisher login github-oidc
5882
from line 100
76100jobs:
77101 publish:
78102 runs-on: ubuntu-latest
103 # Source MCP_GITHUB_TOKEN from a protected Environment, not a plain repo
104 # secret, and restrict that environment to your default branch / release
105 # tags. See "Securing your registry token in CI" below for why this matters.
106 environment: mcp-registry-publish
79107 permissions:
80108 contents: read
109 env:
110 MCP_PUBLISHER_VERSION: v1.8.1
81111
82112 steps:
83113 - name: Checkout code
from line 136
106136
107137 ### Publish MCP server:
108138
109 - name: Install mcp-publisher
139 - name: Install cosign
140 uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
141
142 - name: Download and verify mcp-publisher
110143 run: |
111 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
144 set -euo pipefail
145 os="$(uname -s | tr '[:upper:]' '[:lower:]')"
146 arch="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
147 archive="mcp-publisher_${os}_${arch}.tar.gz"
148 base_url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
112149
150 curl --fail --location --silent --show-error --output "$archive" "$base_url/$archive"
151 curl --fail --location --silent --show-error --output "$archive.sigstore.json" "$base_url/$archive.sigstore.json"
152 cosign verify-blob \
153 --bundle "$archive.sigstore.json" \
154 --certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" \
155 --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
156 "$archive"
157 tar xzf "$archive" mcp-publisher
158 rm -f "$archive" "$archive.sigstore.json"
159
113160 - name: Authenticate to MCP Registry
114161 run: ./mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }}
115162
from line 182
135182 runs-on: ubuntu-latest
136183 permissions:
137184 contents: read
185 env:
186 MCP_PUBLISHER_VERSION: v1.8.1
138187
139188 steps:
140189 - name: Checkout code
from line 212
163212
164213 ### Publish MCP server:
165214
166 - name: Install mcp-publisher
215 - name: Install cosign
216 uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
217
218 - name: Download and verify mcp-publisher
167219 run: |
168 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
220 set -euo pipefail
221 os="$(uname -s | tr '[:upper:]' '[:lower:]')"
222 arch="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
223 archive="mcp-publisher_${os}_${arch}.tar.gz"
224 base_url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
169225
226 curl --fail --location --silent --show-error --output "$archive" "$base_url/$archive"
227 curl --fail --location --silent --show-error --output "$archive.sigstore.json" "$base_url/$archive.sigstore.json"
228 cosign verify-blob \
229 --bundle "$archive.sigstore.json" \
230 --certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" \
231 --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
232 "$archive"
233 tar xzf "$archive" mcp-publisher
234 rm -f "$archive" "$archive.sigstore.json"
235
170236 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
171237 # TODO: Replace `example.com` with your domain name
172238 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from line 251
185251
186252## Step 2: Add Secrets
187253
188You may need to add a secret to the repository depending on which authentication method you choose:
254You may need to add a secret depending on which authentication method you choose:
189255
190256* **GitHub OIDC Authentication**: No dedicated secret necessary.
191* **GitHub PAT Authentication**: Add a `MCP_GITHUB_TOKEN` secret with a GitHub Personal Access Token (PAT) that has `read:org` and `read:user` scopes.
257* **GitHub PAT Authentication**: Add a `MCP_GITHUB_TOKEN` secret with a GitHub Personal Access Token. The registry only needs to read your organization role to confirm you are an **Owner** before granting its namespace, so grant the minimum for that and **no** repository scopes (the registry never reads or writes your code):
258
259 * **Classic PAT**: the `read:org` scope (`read:user` is *not* needed; a bare token still authenticates and publishes to your personal namespace).
260 * **Fine-grained PAT**: the **Organization permissions → Members → Read-only** permission (the equivalent of `read:org`). Without it you'll get your personal namespace only. A fine-grained PAT is scoped to one resource owner, so create it for the organization you're publishing to.
261
262 For an **organization** token, store it as an **Environment secret** on the `mcp-registry-publish` environment rather than a plain repository secret — see "Securing your registry token in CI" below for why this matters and how to configure the environment.
192263* **DNS Authentication**: Add a `MCP_PRIVATE_KEY` secret with your Ed25519 private key.
193264
194265You may also need to add secrets for your package registry. For example, the workflow above needs an `NPM_TOKEN` secret with your npm token.
from line 266
195266
196267For information about how to add secrets to a repository, see [Using secrets in GitHub Actions](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).
197268
269### Securing your registry token in CI
270
271Whichever token you use to authenticate, treat it as a high-value credential. When you authenticate to an **organization** namespace, the resulting registry token can publish — and overwrite — **any** server under `io.github.<org>/*`, not just the one in this repository. The relevant question for your threat model is therefore: *who can cause code to run in a job where this secret is exposed?* By default that is **every repository writer** (via branch or tag pushes), not only org Owners — so a plain repo secret quietly widens publish access to everyone with write access.
272
273GitHub gives you the tools to close that gap. We recommend, in increasing order of strength:
274
2751. **Store the token as an [Environment secret](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments), not a repository or organization secret.** A job can only read an environment secret when it declares `environment:` (as the PAT example above does). Note that the `environment:` key alone protects nothing: if the environment does not exist yet, GitHub **auto-creates it with no protection rules** on first run, and a job that declares `environment:` still also receives ordinary repository and organization secrets. The protection comes from storing the token *as a secret on that environment* and configuring the environment (steps 2–3 below) — not from the `environment:` key by itself.
2762. **Restrict that environment to your default branch and/or release tags** with a [deployment branch rule](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments#deployment-branches-and-tags), and protect that branch with [required pull request reviews](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches). Now only reviewed, maintainer-approved code can ever reach the token.
2773. **(Strongest) Add a [required reviewer](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments#required-reviewers) to the environment** so each publish pauses for an explicit human approval.
278
279Two pitfalls to avoid regardless of the above:
280
281* **Never publish from a `pull_request_target` workflow that checks out PR-head code.** That trigger runs with your secrets in the base-repo context, so an untrusted fork PR could exfiltrate the token. Environment branch rules do **not** protect you here (the ref is still your base branch) — only required reviewers do.
282* **Avoid self-hosted runners for the publish job on public repositories**, where fork PRs may be able to schedule jobs onto them.
283
284By default, pull requests **from forks** receive no secrets and cannot push branches, so external contributors cannot reach the token without one of the misconfigurations above.
285
198286## Step 3: Tag and Release
199287
200288Create and push a version tag to trigger the workflow:
from line 299
211299| Error Message | Action |
212300| - | - |
213301| "Authentication failed" | Ensure `id-token: write` permission is set for OIDC, or check secrets. |
302| "invalid audience" | Your `mcp-publisher` binary is too old for this registry deployment. Update `MCP_PUBLISHER_VERSION` to a compatible release and rerun the workflow. |
214303| "Package validation failed" | Verify your package successfully published to the package registry (e.g., npm, PyPI), and that your package has the [necessary verification information](https://modelcontextprotocol.io/registry/package-types.md). |
215304