Skip to content

feat(release): automate MCP Registry publish (RELEASING.md step 5) - #132

Merged
cdeust merged 1 commit into
mainfrom
agent/registry-publish
Aug 10, 2026
Merged

feat(release): automate MCP Registry publish (RELEASING.md step 5)#132
cdeust merged 1 commit into
mainfrom
agent/registry-publish

Conversation

@cdeust

@cdeust cdeust commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • docs/RELEASING.md step 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 serves 2.8.0 for io.github.cdeust/hypermnesia-mcp-viz while PyPI and the GitHub Release have been at 3.1.0 since the last release.
  • Adds a publish-registry job to .github/workflows/Release.yaml, gated needs: [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.
  • Authenticates with mcp-publisher login github-oidc: GitHub Actions OIDC token exchanged for a registry credential scoped to io.github.cdeust/*. No stored secret — same no-long-lived-credential shape as this workflow's existing PyPI Trusted Publishing.
  • Before publishing: re-checks PyPI already has the target version. After publishing: queries the registry's own API and fails the job if it disagrees with server.json — a green mcp-publisher publish exit code is not treated as proof.
  • Adds a workflow_dispatch recovery path (tag input, required, no silent default) for repairing an already-tagged release's stale registry entry without re-running build/publish/release.
  • Updates docs/RELEASING.md step 5 and CHANGELOG.md.

Authentication model — verified against source, not assumed

Read modelcontextprotocol/registry's own docs before writing anything:

  • docs/reference/cli/commands.mdmcp-publisher login github-oidc: "Uses GitHub Actions OIDC tokens automatically. Requires id-token: write permission 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 pypi package type is via an mcp-name: io.github.cdeust/hypermnesia-mcp-viz marker in the PyPI package README — already present at README.md:5, confirmed before relying on it.

Why ordered after PyPI + GitHub Release

needs: [test, release] plus an if that requires needs.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

  • No continue-on-error, no || true anywhere in the new job.
  • workflow_dispatch without an explicit tag input hard-fails with ::error:: rather than guessing a ref.
  • Post-publish verification queries registry.modelcontextprotocol.io's own API and fails the job (exit 1) if the reported version doesn't match server.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

  • Classification: Medium — CI/CD infrastructure change, publishes to a public registry, no runtime/user-data blast radius, additive (new job) to an existing, already-hardened release workflow.

Blast radius

  • Strategy: rolling/additive — new job only, no changes to existing test/release job bodies beyond an if: gate that scopes them to push (workflow_dispatch no longer re-triggers a full rebuild+re-release, which is the correct behavior, not a regression: PyPI publish is idempotent via skip-existing: true but re-running softprops/action-gh-release against an existing tag would rewrite release notes/assets unintentionally).
  • Affected: io.github.cdeust/hypermnesia-mcp-viz registry entry only. No user-facing runtime code touched.
  • Stateful components touched: the MCP Registry's own record for this server (external system, append-only version history; no destructive operation available or used).

Rollback — tested

  • Rollback command: git revert <merge-commit> — the job is purely additive; reverting removes it with no residual state to clean up locally.
  • The registry itself has no "rollback" primitive for a bad publish other than mcp-publisher status --status deprecated|deleted on a specific version (verified against docs/reference/cli/commands.md) — not exercised here since the job only ever publishes a version PyPI has already confirmed exists.
  • Rollback RTO: minutes (revert + merge).
  • Rollback RPO: N/A — no data loss surface; registry versions are additive history.
  • Forward-only? No.

SLIs and observability

  • SLI 1: job success/failure status on Release.yaml (GitHub Actions run status) — directly observable via gh run list.
  • SLI 2: registry API response for 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.
  • SLI 3: PyPI/registry version parity — the pre-publish check makes divergence impossible by construction rather than only detecting it after the fact.
  • Dashboard: none needed — this is a low-frequency (per-release), synchronously-verified CI step, not a running service.
  • Alert: a failed publish-registry job run is the alert; GitHub Actions run failure notifications apply per existing repo settings.

Infrastructure-as-code

  • Files changed: .github/workflows/Release.yaml, docs/RELEASING.md, CHANGELOG.md.
  • No console changes; entirely in-repo workflow YAML, validated with actionlint (0 errors) and python3 -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.
  • No new IaC state backend involved.

Secrets

  • New secrets introduced: none. OIDC only, per the source cited above.
  • If OIDC is ever unavailable for this registry deployment in the future, the documented fallback is a PAT stored as an environment secret scoped to a protected environment with deployment branch rules (docs/modelcontextprotocol-io/github-actions.mdx, "Securing your registry token in CI") — not implemented here since OIDC is available and preferred.

Capacity and idempotency

  • Capacity: N/A — no runtime service, a one-shot CI job.
  • Idempotency: mcp-publisher publish publishing the same server.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); the workflow_dispatch recovery path is explicitly designed to be safely re-run against the same tag.
  • Lockfiles: N/A (no new language dependency introduced; mcp-publisher is a pinned, checksum-verified static binary, pinned to v1.8.1 with its SHA-256 from the registry project's own release checksums file).

Rules compliance (coding-standards.md)

Rule Status Evidence Action
§7 local reasoning Pass New job is straight-line steps, no dynamic dispatch, no reflection
§8 source discipline Pass Every non-obvious constant (OIDC mechanism, checksum, ownership-verification marker) has a # source:-equivalent comment or PR-body citation
§9 anti-patterns Pass No silent skip, no swallowed error, no dead code

Boy-scout check (coding-standards.md §14)

  • Defects seen in touched material this session: none observed beyond the gap this PR fixes.
  • Fixed in this PR: N/A.
  • Deferred: none.
  • Bypass used: none.

Then: publishing 3.1.0

Once this is green on main, the plan is to invoke workflow_dispatch with tag=v3.1.0 (the tag already exists and won't re-fire the push trigger) and verify externally via the registry API that it returns 3.1.0.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

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>
@cdeust

cdeust commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

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. docs/RELEASING.md step 5 said "publish server.json with the official MCP Registry publisher" and shipped no tooling to do it — no script, no workflow step, an external binary a human was trusted to remember. Release.yaml's own header records why the build workflow exists: 2.7.1 "was cut by hand, so there was no committed, reproducible build path and therefore no artifact to attest". Step 6 records Cortex #179, where a manual checklist step was skipped and eight releases reached zero installs. Step 5 was the same shape, one notch further along, and it was drifting: the registry served 2.8.0 while PyPI had 3.1.0.

The job is ordered so it cannot lie. needs: [test, release] means the registry entry can never be published pointing at a package that does not exist — the failure mode a standalone publish step invites. The workflow_dispatch recovery path refuses to guess which release to publish and errors without an explicit tag input, which is the right behaviour for a recovery hatch: loud, not convenient.

The authentication model matches what this repo already proved out. mcp-publisher login github-oidc with id-token: write and no stored credential, the same no-long-lived-token posture as its PyPI Trusted Publishing. The publisher binary is checksum-verified at install rather than curl-piped. This was verified against the registry's documentation rather than assumed by analogy to PyPI, which was the explicit instruction and the right instinct.

Proven live, and checked by me independently rather than taken from the run's exit code:

io.github.cdeust/hypermnesia-mcp-viz    3.1.0   isLatest=True

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 server.json description exceeded the registry's 100-character cap — surfaced immediately instead of silently no-opping, which is exactly the property that was missing before. It was fixed at the source in that PR, scoped to the file the registry reads, leaving manifest.json's longer description for its different consumer untouched.

docs/RELEASING.md now describes what runs automatically, with any genuinely manual residue left explicit.

12/12 checks green at f77bea8, no unresolved threads. No declared violation, no deferral. Merging.

@cdeust
cdeust merged commit 22d4de5 into main Aug 10, 2026
15 checks passed
cdeust added a commit that referenced this pull request Aug 10, 2026
…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>
@cdeust
cdeust deleted the agent/registry-publish branch August 10, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant