How to Automate Publishing with GitHub Actions
Source: MCP b4a909c3
Treat package upload and Registry publication as separate release steps
The workflow below writes to two systems: a package registry and the MCP Registry. They have separate identities and separate failure modes. Decide the intended namespace and package version before pushing a release tag, then make the job’s evidence precise enough to tell which write succeeded.
| Job stops at | Inspect first | Recovery direction |
|---|---|---|
| Build or tests | Source revision and dependency lock | Fix before publishing either system |
| Package upload | Package credentials, name and version | Determine whether the exact version already exists |
| Registry login | OIDC permission or the selected PAT/domain proof | Repair that identity, without republishing the package |
| Registry publication | Published package’s marker and server metadata | Reuse the intended artifact when it is already correct |
| Post-release query | Exact name and Registry version | Check the read result separately from job status |
OIDC removes the dedicated Registry secret for that authentication path; it does not automatically authorize npm publication. Likewise, having NPM_TOKEN says nothing about Registry namespace permission. Choose one of the documented Registry authentication flows and retain the matching package-registry authentication independently.
Before automating, compare the tag, server.json version and package version. If the job copies the tag into metadata, inspect the resulting file before publication. For repeatable releases, pin the publisher version you reviewed and record it alongside the workflow revision; the reference’s latest-download example otherwise allows the publisher to change between runs.
If npm succeeds and the Registry step fails, rerunning the entire job without checking can fail on an already published package version. Recover from the failed stage after inspecting the existing artifact. When metadata itself needs correction, follow Versioning rather than trying to overwrite a published Registry record. These are workflow review recommendations based on the attributed examples; this site has not executed them against either public registry.
Step 1: Create a Workflow File
In 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:
name: Publish to MCP Registry
on:
push:
tags: ["v*"] # Triggers on version tags like v1.0.0
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC authentication
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
### Publish underlying npm package:
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "lts/*"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test --if-present
- name: Build package
run: npm run build --if-present
- name: Publish package to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
### Publish MCP server:
- 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
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
# Optional:
# - name: Set version in server.json
# run: |
# VERSION=${GITHUB_REF#refs/tags/v}
# jq --arg v "$VERSION" '.version = $v' server.json > server.tmp && mv server.tmp server.json
- name: Publish server to MCP Registry
run: ./mcp-publisher publishname: Publish to MCP Registry
on:
push:
tags: ["v*"] # Triggers on version tags like v1.0.0
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
### Publish underlying npm package:
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "lts/*"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test --if-present
- name: Build package
run: npm run build --if-present
- name: Publish package to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
### Publish MCP server:
- 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
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }}
# Optional:
# - name: Set version in server.json
# run: |
# VERSION=${GITHUB_REF#refs/tags/v}
# jq --arg v "$VERSION" '.version = $v' server.json > server.tmp && mv server.tmp server.json
- name: Publish server to MCP Registry
run: ./mcp-publisher publishname: Publish to MCP Registry
on:
push:
tags: ["v*"] # Triggers on version tags like v1.0.0
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
### Publish underlying npm package:
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "lts/*"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test --if-present
- name: Build package
run: npm run build --if-present
- name: Publish package to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
### Publish MCP server:
- 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
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# TODO: Replace `example.com` with your domain name
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- name: Authenticate to MCP Registry
run: ./mcp-publisher login dns --domain example.com --private-key ${{ secrets.MCP_PRIVATE_KEY }}
# Optional:
# - name: Set version in server.json
# run: |
# VERSION=${GITHUB_REF#refs/tags/v}
# jq --arg v "$VERSION" '.version = $v' server.json > server.tmp && mv server.tmp server.json
- name: Publish server to MCP Registry
run: ./mcp-publisher publishStep 2: Add Secrets
You may need to add a secret to the repository depending on which authentication method you choose:
- GitHub OIDC Authentication: No dedicated secret necessary.
- GitHub PAT Authentication: Add a
MCP_GITHUB_TOKENsecret with a GitHub Personal Access Token (PAT) that hasread:organdread:userscopes. - DNS Authentication: Add a
MCP_PRIVATE_KEYsecret with your Ed25519 private key.
You may also need to add secrets for your package registry. For example, the workflow above needs an NPM_TOKEN secret with your npm token.
For information about how to add secrets to a repository, see Using secrets in GitHub Actions.
Step 3: Tag and Release
Create and push a version tag to trigger the workflow:
git tag v1.0.0
git push origin v1.0.0The workflow will run tests, build the package, publish the package to npm, and publish the server to the MCP Registry.
Troubleshooting
| Error Message | Action |
|---|---|
| “Authentication failed” | Ensure id-token: write permission is set for OIDC, or check secrets. |
| “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. |