From 9924338361d68523a59234c577aa8c4f5b76a86b Mon Sep 17 00:00:00 2001 From: craigjmidwinter Date: Fri, 31 Jul 2026 07:33:01 -0500 Subject: [PATCH] release: publish to the MCP Registry from the tag, not by hand The registry entry names a version and an image tag, both of which go stale the moment a new release ships. Keeping them right by hand means remembering to edit server.json, re-run `mcp-publisher login` through a device-code flow, and publish -- every time. That is a step that gets skipped, and a skipped step here means the registry advertises an old version indefinitely. So the tag does it. A second job rewrites `version` and the image tag from GITHUB_REF_NAME and publishes. It `needs: release` because the entry points at an image goreleaser has to have pushed first; publishing earlier would advertise something nobody can pull. Authentication is OIDC, so there is no registry token in this repository to store, rotate or leak -- the registry trusts the workflow identity directly. Tag pushes only. A workflow_dispatch run carries a branch name rather than a version, and its dry run pushes no image at all. The version left in server.json is now last release's by design; CI stamps it. README says so, because a lagging number in a committed file otherwise reads as a bug. CI also validates server.json on every PR. A tag release is the worst place to learn it is malformed: by then the image is pushed and the GitHub release is cut. --- .github/workflows/ci.yml | 13 ++++++++++ .github/workflows/release.yml | 47 +++++++++++++++++++++++++++++++++++ README.md | 9 +++++++ 3 files changed, 69 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730cfd9..20f9d4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,3 +100,16 @@ jobs: - name: Show what a release would contain run: ls -l dist/*.tar.gz dist/checksums.txt + + # server.json is only exercised during a tag release, which is a bad + # place to discover it is malformed: the image is already pushed and the + # GitHub release already cut by then. Validating on every PR moves that + # failure to where it is cheap to fix. + # + # This checks shape, not version numbers -- the release workflow rewrites + # `version` and the image tag from the tag before publishing, so the + # values committed here are last release's and are expected to lag. + - name: Validate server.json + run: | + curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher + ./mcp-publisher validate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9e7bc8..d972224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,3 +117,50 @@ jobs: dist/checksums.txt retention-days: 7 if-no-files-found: error + + # Listing the release in the MCP Registry, which is metadata only: it records + # that this version exists and which image holds it. Kept as its own job that + # needs `release`, because the entry names an image tag -- publishing before + # goreleaser has pushed it would advertise something nobody can pull. + mcp-registry: + name: mcp registry + needs: release + # Tag pushes only. A workflow_dispatch run has a branch in GITHUB_REF_NAME + # rather than a version, so there is no release for an entry to describe, + # and a dry run pushes no image at all. + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + # The whole authentication story. `login github-oidc` trades this + # workflow's identity for a short-lived registry token, so there is no + # registry secret in this repository to store, rotate or leak, and no + # device-code flow to sit through by hand. + id-token: write + contents: read + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Install mcp-publisher + run: | + curl -fsSL "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 + + # The committed server.json carries the version of the *previous* + # release, so by the time this runs it is stale by exactly one tag. The + # tag is the truth. Both fields are rewritten from it, and they have to + # move together: `version` is what the registry lists, `identifier` is + # the image that version actually lives in. + - name: Stamp the tag into server.json + run: | + version="${GITHUB_REF_NAME#v}" + jq --arg v "$version" \ + --arg img "ghcr.io/${{ github.repository }}:" \ + '.version = $v | .packages[0].identifier = $img + $v' \ + server.json > server.tmp && mv server.tmp server.json + cat server.json + + - name: Authenticate to the MCP Registry + run: ./mcp-publisher login github-oidc + + - name: Publish + run: ./mcp-publisher publish diff --git a/README.md b/README.md index f74a91e..b46e1cf 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,15 @@ The image is also what backs the [MCP Registry](https://registry.modelcontextpro listing; [`server.json`](server.json) is that entry, and its `name` has to match the `io.modelcontextprotocol.server.name` label baked into the image. +Publishing that entry is automatic. Tagging a release builds and pushes the +image, and then a second job rewrites `version` and the image tag in +`server.json` from the git tag and publishes to the registry, authenticating +with the workflow's own OIDC identity rather than a stored token. + +**So the `version` committed in `server.json` is last release's, and lags by +one tag on purpose.** The tag is the source of truth; the file is a template +that CI stamps. Bumping it by hand achieves nothing. + ### As a Claude Code skill The repo ships a skill and plugin package under [`skills/`](skills/), which