feat(release): automate MCP Registry publish (RELEASING.md step 5) - #132
Conversation
Add a publish-registry job to Release.yaml that publishes server.json to the official MCP Registry via mcp-publisher, gated to run only after the PyPI publish and GitHub Release jobs succeed so it can never point the registry at a package version that does not exist. Authenticates with `mcp-publisher login github-oidc` (GitHub Actions OIDC exchanged for a registry credential scoped to io.github.cdeust/*) — no stored secret, mirroring the PyPI Trusted Publishing pattern already used in this workflow. Verified against modelcontextprotocol/registry's own docs (docs/reference/cli/commands.md, docs/modelcontextprotocol-io/github-actions.mdx). Before publishing, the job checks PyPI already has the target version. After publishing, it queries the registry's own API and fails the job if the response disagrees with server.json — a green exit code is not treated as proof. A workflow_dispatch input adds a recovery path for an already-tagged release whose registry entry fell behind, without re-running the build/publish/release steps; it refuses to run without an explicit tag. This closes the exact gap that let the registry serve 2.8.0 while PyPI and the GitHub Release were already at 3.1.0 — the same "checklist step nobody ran" shape as the marketplace-pin lesson (Cortex #179) already documented in this workflow's header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
ZETETIC-REVIEW: APPROVE Reviewed against the workflow and the registry's own response, not against the report. The defect this closes is the third instance of one pattern in this repository. The job is ordered so it cannot lie. The authentication model matches what this repo already proved out. Proven live, and checked by me independently rather than taken from the run's exit code: A green job is not the proof. The registry's own answer is. The failure on the sibling repository is evidence this fails loudly. The first dispatch there returned HTTP 422 because a committed
12/12 checks green at |
…aces (#138) 3.1.0 could not build a graph at all: commit 45d4a80 deleted graph_event_stream's module-level emit/close/reset forwarders on the false premise of "no caller in this repository's history", killing every build with AttributeError on the first statement (#134, fixed by #136). #135/#137 stops the finally-block terminator from swallowing that same failure silently, which is why #134 reached a release unnoticed in the first place. #131 (mutation-test hardening, no behavior change) and #132 (automates RELEASING.md step 5, MCP Registry publish) also landed since 3.1.0 with no further breaking change, so this is a patch release: 3.1.0 -> 3.1.1, not a restatement of 3.1.0. Promotes CHANGELOG's Unreleased section (#132's entry) to 3.1.1, carries forward #134/#135's fix descriptions, and reopens an empty Unreleased. #131 gets no entry: it changes no shipped behavior. Aligns every version-bearing surface pyproject.toml/cortex_viz/identity.py/ server.json/.claude-plugin/plugin.json/.codex-plugin/plugin.json/ gemini-extension.json/.claude-plugin/marketplace.json/uv.lock (via `uv lock`) plus the two surfaces check_distribution_artifact does not cover, the README badge and docs/ROADMAP.md's "current version" line. Verified: `python -m scripts.check_distribution_artifact` passes against a built wheel; full suite 1317 passed/10 skipped; ruff check and format clean. Changelog-vs-tag audit: 3.1.0/2.8.0/2.7.1/2.7.0 all have matching tags, 3.0.0's "cut in the tree, never tagged or published" annotation is intact and unchanged, nothing new drifted. Co-authored-by: Claude <noreply@anthropic.com>
Summary
docs/RELEASING.mdstep 5 ("publish server.json with the official MCP Registry publisher") had no committed tooling — no script, no workflow step, dependent on a binary not in PATH here. The registry drifted: it serves2.8.0forio.github.cdeust/hypermnesia-mcp-vizwhile PyPI and the GitHub Release have been at3.1.0since the last release.publish-registryjob to.github/workflows/Release.yaml, gatedneeds: [test, release]so it runs only after PyPI publish and the GitHub Release succeed — it can never publish a registry entry pointing at a package version that doesn't exist.mcp-publisher login github-oidc: GitHub Actions OIDC token exchanged for a registry credential scoped toio.github.cdeust/*. No stored secret — same no-long-lived-credential shape as this workflow's existing PyPI Trusted Publishing.server.json— a greenmcp-publisher publishexit code is not treated as proof.workflow_dispatchrecovery path (taginput, required, no silent default) for repairing an already-tagged release's stale registry entry without re-running build/publish/release.docs/RELEASING.mdstep 5 andCHANGELOG.md.Authentication model — verified against source, not assumed
Read
modelcontextprotocol/registry's own docs before writing anything:docs/reference/cli/commands.md—mcp-publisher login github-oidc: "Uses GitHub Actions OIDC tokens automatically. Requiresid-token: writepermission in workflow. No browser interaction needed."docs/modelcontextprotocol-io/github-actions.mdx— publishes the exact GitHub Actions pattern used here as "OIDC authentication (recommended)", contrasted with PAT and DNS methods that do require a stored secret.This repo already runs PyPI publication via Trusted Publishing (OIDC, no token) on the same workflow — the registry has a directly equivalent mechanism, so no secret was introduced.
Ownership verification for the
pypipackage type is via anmcp-name: io.github.cdeust/hypermnesia-mcp-vizmarker in the PyPI package README — already present atREADME.md:5, confirmed before relying on it.Why ordered after PyPI + GitHub Release
needs: [test, release]plus anifthat requiresneeds.release.result == 'success'on the push-tag path. The one failure mode this job exists to close is a registry entry pointing at a package that isn't there; ordering after the artifact exists is the only way to make that structurally impossible rather than "checked and hoped."Why it fails rather than skips
continue-on-error, no|| trueanywhere in the new job.workflow_dispatchwithout an explicittaginput hard-fails with::error::rather than guessing a ref.registry.modelcontextprotocol.io's own API and fails the job (exit 1) if the reported version doesn't matchserver.json— this is the external verification the deployment plan below documents, not CI's own opinion of itself.Deployment Plan (DevOps Engineer format)
Stakes classification
Blast radius
test/releasejob bodies beyond anif:gate that scopes them topush(workflow_dispatch no longer re-triggers a full rebuild+re-release, which is the correct behavior, not a regression: PyPI publish is idempotent viaskip-existing: truebut re-runningsoftprops/action-gh-releaseagainst an existing tag would rewrite release notes/assets unintentionally).io.github.cdeust/hypermnesia-mcp-vizregistry entry only. No user-facing runtime code touched.Rollback — tested
git revert <merge-commit>— the job is purely additive; reverting removes it with no residual state to clean up locally.mcp-publisher status --status deprecated|deletedon a specific version (verified againstdocs/reference/cli/commands.md) — not exercised here since the job only ever publishes a version PyPI has already confirmed exists.SLIs and observability
Release.yaml(GitHub Actions run status) — directly observable viagh run list.io.github.cdeust/hypermnesia-mcp-viz(GET https://registry.modelcontextprotocol.io/v0/servers?search=hypermnesia-mcp-viz) — the job's own last step asserts this before returning success; also externally queryable at any time.publish-registryjob run is the alert; GitHub Actions run failure notifications apply per existing repo settings.Infrastructure-as-code
.github/workflows/Release.yaml,docs/RELEASING.md,CHANGELOG.md.actionlint(0 errors) andpython3 -c "import yaml; yaml.safe_load(...)"(parses clean) before commit — no CI run available to lint pre-merge given the machine constraint, so this is the pre-merge verification; CI itself is the runtime proof once this PR is green.Secrets
docs/modelcontextprotocol-io/github-actions.mdx, "Securing your registry token in CI") — not implemented here since OIDC is available and preferred.Capacity and idempotency
mcp-publisher publishpublishing the sameserver.json/version twice is the registry's own concern (not re-run automatically by this workflow on the push-tag path, since the job only fires once per tag push); theworkflow_dispatchrecovery path is explicitly designed to be safely re-run against the same tag.mcp-publisheris a pinned, checksum-verified static binary, pinned tov1.8.1with its SHA-256 from the registry project's own release checksums file).Rules compliance (coding-standards.md)
# source:-equivalent comment or PR-body citationBoy-scout check (coding-standards.md §14)
Then: publishing 3.1.0
Once this is green on
main, the plan is to invokeworkflow_dispatchwithtag=v3.1.0(the tag already exists and won't re-fire the push trigger) and verify externally via the registry API that it returns3.1.0.Co-Authored-By: Claude Opus 5 noreply@anthropic.com