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