Auto-publish to MCP registry on release via mcp-publisher#28
Auto-publish to MCP registry on release via mcp-publisher#28cmeans-claude-dev[bot] merged 2 commits intomainfrom
Conversation
Adds a `publish-registry` job to publish.yml that runs after publish-pypi and pushes server.json to registry.modelcontextprotocol.io via mcp-publisher. Uses GitHub OIDC (id-token: write) so no long-lived registry token is needed. Idempotent on re-run — the registry's "version already exists" error (versions are immutable per the spec) is caught and downgraded to a workflow warning. Also adds a validate-server-json job to ci.yml so schema breakage is caught on every PR, complementing the existing version-sync check. Scaffolding spec's Release & Distribution section now documents all four publish jobs. Closes #27. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cmeans
left a comment
There was a problem hiding this comment.
QA Review — PR #28 (Round 1)
Verification (this session, on pr-28 HEAD dcb42c2)
| Check | Result |
|---|---|
uv run pytest (full suite) |
495 passed, 94 deselected (integration + vdsm markers, excluded by -m 'not integration and not vdsm' in addopts; vdsm runs in .github/workflows/vdsm.yml — green on this PR), coverage 96.03% |
uv run ruff check . |
All checks passed |
uv run mypy src |
Success: no issues found in 27 source files |
python -c 'yaml.safe_load(...)' on both workflows |
OK |
| CI on PR | lint, typecheck, test (3.11/3.12/3.13), version-sync, validate-server-json (new), vdsm integration tests — all green |
mcp-publisher validate server.json (v1.5.0, local) |
✅ server.json is valid (against https://registry.modelcontextprotocol.io + 2025-12-11 schema) |
Scope vs issue #27
Issue acceptance criteria traced:
-
publish-registryadded,needs: publish-pypi✓ (publish.yml:51-55) - GitHub OIDC (no long-lived secret) ✓ (
permissions: id-token: write+mcp-publisher login github-oidc) - Re-run on same tag does not fail — idempotent path via stdout grep ✓
-
docs/specs/project-scaffolding-spec.mdrelease section updated ✓ (new "Automated release pipeline" subsection) - CHANGELOG
## Unreleased→### Addedentry ✓ - Nice-to-have from "open questions": server.json PR-time validation → implemented as new
validate-server-jsonCI job ✓
Remaining AC ("Verify with a dry-run / test tag before relying on it") is deferred to the first post-merge tag; flagged as a nit below.
Code review
The design is clean: publish-registry sequenced after publish-pypi (correct — registry validates the PyPI package+version exists), github-release kept parallel to publish-registry (correct — a registry failure shouldn't block the GH release), OIDC permissions scoped to the single job, and ./mcp-publisher --version as a post-install sanity check. The idempotency block uses set +e + captured output + regex match + loud fallback (re-exit $status), which is the right shape — mis-matches fail loudly rather than silently corrupt.
Findings
O1 — observation: mcp-publisher binary pinned to latest without checksum verification.
Both the validate-server-json job (ci.yml:34-44) and publish-registry (publish.yml:63-67) pull from github.com/modelcontextprotocol/registry/releases/latest with no pinned version and no SHA-256 verification. PR body acknowledges this: "Pinned to latest rather than a specific version so we pick up registry schema updates automatically; if this proves unstable we can pin later."
Supply-chain risk is real but bounded (trusted upstream repo). Dev paths: (a) accept and keep latest per the design note — reply to close this finding; (b) pin to a specific version tag (mcp-publisher_v1.5.0_*.tar.gz) and optionally add sha256sum -c against a known digest.
O2 — observation: mcp-publisher install block duplicated across ci.yml and publish.yml.
Same curl+arch-detection one-liner appears in both workflows (ci.yml:35-39 and publish.yml:64-67). If the install URL schema changes (e.g., upstream reorganizes releases), both copies must be updated in lockstep. Dev paths: (a) factor into .github/actions/install-mcp-publisher/action.yml composite action and reference from both jobs; (b) accept the 2-copy duplication as low-cost given the job count.
O3 — observation: idempotency grep patterns are speculative, not sourced from mcp-publisher.
Inspection of mcp-publisher v1.5.0 binary strings did not surface a clear match for already (exists|published)|duplicate version|version.*conflict in the publish error path (only server.json already exists from the init command). The pattern list looks reasonable but isn't anchored to actual tool output. Mitigation in place: echo "$output" before the grep ensures the raw error is in workflow logs, and a miss produces a loud failure, not silent swallowing. Dev paths: (a) accept with a comment noting the patterns were pattern-matched and will need to be tightened after the first real re-run exercises the path; (b) add a brief comment in the block pointing to the exact mcp-publisher source file / commit this was derived from; (c) wait until the first post-merge re-run of a tag to harden the regex against real output.
O4 — nit: issue #27's "verify with dry-run / test tag" AC is deferred.
PR Test plan's "Manual (post-merge, on next tag)" section covers this — valid, since a production publish flow can't be dry-run without a real tag. Flagging so the first v0.5.x tag after merge is exercised as a verification event (registry entry appears, re-run produces ::warning:: and exits 0).
Label
Applying QA Failed because O1–O3 are observations and per standing guidance every observation blocks signoff. Dev can address each by fix or explicit acceptance — both count as resolving the finding. Flip to Ready for QA when the four are closed out.
|
QA audit — applying Verification in this session on
Four non-blocking observations / nits — each can be closed by either a fix or an explicit reply accepting the current approach. See review comment for detail. Flip back to Ready for QA once all four are addressed. |
- O1: Pin mcp-publisher to v1.5.0 (was `latest`). Supply-chain risk
argued down to "bounded, trusted upstream" but pinning gives
reproducible CI runs and intentional bumps.
- O2: Extract the install step into a composite action at
.github/actions/install-mcp-publisher/action.yml. Single source of
truth for the curl URL and the pinned version — used by both
`validate-server-json` (ci.yml) and `publish-registry` (publish.yml).
- O3: Replace speculative regex with the exact ErrInvalidVersion
string from internal/database/database.go@v1.5.0:
"invalid version: cannot publish duplicate version"
Comment in publish.yml cites the upstream source. `grep -qF` (fixed
string) instead of `-iE` so intent is obvious. Mismatches still fail
loudly via `exit $status`.
O4 (deferred dry-run verification on first post-merge tag) remains as
documented in the PR body — it can only be exercised by a real release.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Addressed QA round 1 findings in 8c583fe. O1 (pin mcp-publisher): Fixed — pinned to O2 (install duplication): Fixed — extracted into O3 (speculative grep): Fixed — verified the exact error string against upstream at tag ErrInvalidVersion = errors.New("invalid version: cannot publish duplicate version")Handler maps any service error to HTTP 400 via O4 (deferred dry-run): Acknowledged — remains in the PR body's "Manual (post-merge, on next tag)" test plan. This verifies against real registry behavior and cannot be exercised without a real tag. Local verification on 8c583fe: all three YAMLs parse, |
cmeans
left a comment
There was a problem hiding this comment.
QA Review — PR #28 (Round 2)
Round-1 findings resolution
| ID | Round-1 finding | Round-2 fix | Status |
|---|---|---|---|
| O1 | mcp-publisher binary pulled from /releases/latest without pin |
New composite action at .github/actions/install-mcp-publisher/action.yml pins to v1.5.0 via an input default; comment on the input says "Bump when a new release is needed." |
Resolved (pin path of the two options offered; checksum remains optional — OK to defer) |
| O2 | Install block duplicated across ci.yml + publish.yml |
Both jobs now call uses: ./.github/actions/install-mcp-publisher |
Resolved |
| O3 | Idempotency grep patterns speculative | Grep replaced with grep -qF 'cannot publish duplicate version'; comment anchors to upstream constant ErrInvalidVersion in internal/database/database.go. Verified via gh search code: exact string exists at that path and is asserted in internal/api/handlers/v0/publish_test.go, so the match is test-guarded upstream |
Resolved |
| O4 | Dry-run on real tag deferred to post-merge | Unchanged — not addressable in this PR | Deferred (accepted) |
Verification (this session, on pr-28 HEAD 8c583fe)
| Check | Result |
|---|---|
uv run pytest (full suite) |
495 passed, 94 deselected (integration + vdsm markers excluded by addopts; vdsm runs in .github/workflows/vdsm.yml — green on this PR), coverage 96.03% |
uv run ruff check . |
All checks passed |
uv run mypy src |
Success: no issues found in 27 source files |
YAML parse (publish.yml, ci.yml, new actions/install-mcp-publisher/action.yml) |
OK |
| CI on PR | lint, typecheck, test (3.11/3.12/3.13), version-sync, validate-server-json (exercises the new composite action), vdsm integration tests — all green |
The fact that validate-server-json passed in CI on the new 8c583fe is the strongest signal here — it exercises the composite action end-to-end against the pinned v1.5.0 binary.
Findings
None. Applying Ready for QA Signoff.
|
QA audit — applying All three O1–O3 observations resolved on new commit
Round-2 session verification: pytest 495 passed / coverage 96.03%, ruff clean, mypy clean, all three YAML files parse, |
Summary
Closes #27. Tagging a
v*release now automatically publishes (or updates) the server entry inregistry.modelcontextprotocol.io— no more manualmcp-publisher publishstep outside CI, andserver.jsoncan't silently drift from the registry listing.Changes
.github/workflows/publish.ymlpublish-registryjob —needs: publish-pypi,permissions: id-token: write, installsmcp-publisherfrom the registry's latest GitHub release, authenticates vialogin github-oidc, runsmcp-publisher publish. Idempotent re-run: duplicate-version errors are caught viagrep -iE 'already (exists|published)|duplicate version|version.*conflict'and downgraded to::warning::..github/workflows/ci.ymlvalidate-server-jsonjob on every PR — installsmcp-publisher, runsmcp-publisher validate server.json. Complements the existingversion-syncjob (which enforces alignment withpyproject.toml) by catching schema-level issues before a tag push.docs/specs/project-scaffolding-spec.mdbuild→publish-pypi→publish-registry/github-release) and thevalidate-server-jsonPR gate.CHANGELOG.md### Addedentry.Design notes
Why GitHub OIDC, not a PAT. The registry supports
login github-oidcwhich consumes the Actions-provided OIDC token whenid-token: writeis set. This is the path recommended in the MCP Registry GitHub Actions guide and avoids maintaining a long-livedMCP_GITHUB_TOKENsecret. Theio.github.cmeans/*namespace is covered by the OIDC flow.Why
needs: publish-pypi. The registry server validatespackages[].identifier+versionon the referenced package registry before accepting the entry. Running the registry step in parallel withpublish-pypirisks racing PyPI's availability; sequencing it after guarantees the PyPI version exists when the registry checks.Idempotency approach. The registry docs (
docs/modelcontextprotocol-io/versioning.mdx) state: "The version string MUST be unique for each publication of the server. Once published, the version string (and other metadata) cannot be changed." The CLI doesn't have a--if-not-existsflag, so we capture stdout+stderr, check the exit status, and if the output matches duplicate-version error patterns we exit 0 with a::warning::. This matches the pattern already used by thegithub-releasejob for its own idempotency (create vs edit).mcp-publisher install source. Downloads the prebuilt binary from
github.com/modelcontextprotocol/registry/releases/latestusing the arch-detection one-liner from the upstream guide. Pinned tolatestrather than a specific version so we pick up registry schema updates automatically; if this proves unstable we can pin later.Test plan
Automated (CI)
python -c 'import yaml; yaml.safe_load(open(f))'for both workflows)mcp-publisher validate server.jsonpasses against the current registry schema (ran locally, v1.5.0)uv run ruff check src/ tests/ scripts/— cleanuv run mypy src/ scripts/— cleanuv run pytest— 495 passed, 96.03% coveragevalidate-server-jsonjob on this PRManual (post-merge, on next tag)
publish-registryjob succeedsio.github.cmeans/mcp-synologyat the new version::warning::rather than failing🤖 Generated with Claude Code