Skip to content

Auto-publish to MCP registry on release via mcp-publisher#28

Merged
cmeans-claude-dev[bot] merged 2 commits intomainfrom
feat/mcp-registry-publish
Apr 14, 2026
Merged

Auto-publish to MCP registry on release via mcp-publisher#28
cmeans-claude-dev[bot] merged 2 commits intomainfrom
feat/mcp-registry-publish

Conversation

@cmeans-claude-dev
Copy link
Copy Markdown
Contributor

Summary

Closes #27. Tagging a v* release now automatically publishes (or updates) the server entry in registry.modelcontextprotocol.io — no more manual mcp-publisher publish step outside CI, and server.json can't silently drift from the registry listing.

Changes

File Change
.github/workflows/publish.yml New publish-registry job — needs: publish-pypi, permissions: id-token: write, installs mcp-publisher from the registry's latest GitHub release, authenticates via login github-oidc, runs mcp-publisher publish. Idempotent re-run: duplicate-version errors are caught via grep -iE 'already (exists|published)|duplicate version|version.*conflict' and downgraded to ::warning::.
.github/workflows/ci.yml New validate-server-json job on every PR — installs mcp-publisher, runs mcp-publisher validate server.json. Complements the existing version-sync job (which enforces alignment with pyproject.toml) by catching schema-level issues before a tag push.
docs/specs/project-scaffolding-spec.md Adds an "Automated release pipeline" subsection documenting all four publish jobs (buildpublish-pypipublish-registry / github-release) and the validate-server-json PR gate.
CHANGELOG.md ### Added entry.

Design notes

Why GitHub OIDC, not a PAT. The registry supports login github-oidc which consumes the Actions-provided OIDC token when id-token: write is set. This is the path recommended in the MCP Registry GitHub Actions guide and avoids maintaining a long-lived MCP_GITHUB_TOKEN secret. The io.github.cmeans/* namespace is covered by the OIDC flow.

Why needs: publish-pypi. The registry server validates packages[].identifier + version on the referenced package registry before accepting the entry. Running the registry step in parallel with publish-pypi risks 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-exists flag, 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 the github-release job for its own idempotency (create vs edit).

mcp-publisher install source. Downloads the prebuilt binary from github.com/modelcontextprotocol/registry/releases/latest using the arch-detection one-liner from the upstream guide. Pinned to latest rather than a specific version so we pick up registry schema updates automatically; if this proves unstable we can pin later.

Test plan

Automated (CI)

  • YAML parses (python -c 'import yaml; yaml.safe_load(open(f))' for both workflows)
  • mcp-publisher validate server.json passes against the current registry schema (ran locally, v1.5.0)
  • uv run ruff check src/ tests/ scripts/ — clean
  • uv run mypy src/ scripts/ — clean
  • uv run pytest — 495 passed, 96.03% coverage
  • CI should now run the new validate-server-json job on this PR

Manual (post-merge, on next tag)

  • Cut the next release tag (e.g., 0.5.1 when ready) and verify:
    • publish-registry job succeeds
    • Entry appears at https://registry.modelcontextprotocol.io servers endpoint for io.github.cmeans/mcp-synology at the new version
    • If the workflow is re-run on the same tag (simulate via GitHub Actions UI), the second run exits 0 with a ::warning:: rather than failing

🤖 Generated with Claude Code

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>
@github-actions github-actions bot added Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 14, 2026
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cmeans cmeans added QA Active QA is actively reviewing; Dev should not push changes and removed Ready for QA Dev work complete — QA can begin review labels Apr 14, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-registry added, 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.md release section updated ✓ (new "Automated release pipeline" subsection)
  • CHANGELOG ## Unreleased### Added entry ✓
  • Nice-to-have from "open questions": server.json PR-time validation → implemented as new validate-server-json CI 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.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 14, 2026

QA audit — applying QA Failed (round 1)

Verification in this session on pr-28 HEAD dcb42c2:

  • uv run pytest: 495 passed, 94 deselected (integration + vdsm markers, excluded by pytest addopts; vdsm runs in its own workflow — green here), coverage 96.03%
  • ruff check, mypy src: clean
  • Both workflow YAMLs parse
  • Local mcp-publisher v1.5.0 validate server.json: valid against 2025-12-11 schema
  • All CI checks green, including the new validate-server-json job
  • Scope vs issue Auto-publish to MCP registry on release via mcp-publisher #27: 5 of 6 ACs met in this PR; 6th (dry-run on real tag) is correctly deferred to post-merge

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.

@cmeans cmeans added QA Failed QA found issues — needs dev attention and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 14, 2026
- 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>
@cmeans-claude-dev cmeans-claude-dev bot added Ready for QA Dev work complete — QA can begin review and removed QA Failed QA found issues — needs dev attention labels Apr 14, 2026
@cmeans-claude-dev
Copy link
Copy Markdown
Contributor Author

Addressed QA round 1 findings in 8c583fe.

O1 (pin mcp-publisher): Fixed — pinned to v1.5.0 via the composite action's default version input. Drops latest. Skipped SHA-256 verification; the maintenance burden (sync-on-bump) outweighs the marginal supply-chain gain for a trusted upstream, and the pinned version gives us reproducibility which was the core issue.

O2 (install duplication): Fixed — extracted into .github/actions/install-mcp-publisher/action.yml. Both validate-server-json (ci.yml) and publish-registry (publish.yml) now use - uses: ./.github/actions/install-mcp-publisher. Version bumps happen in one place.

O3 (speculative grep): Fixed — verified the exact error string against upstream at tag v1.5.0. The duplicate-version path returns ErrInvalidVersion from internal/database/database.go:18:

ErrInvalidVersion = errors.New("invalid version: cannot publish duplicate version")

Handler maps any service error to HTTP 400 via huma.Error400BadRequest (internal/api/handlers/v0/publish.go), and mcp-publisher publish surfaces it in the publish failed: server returned status 400: ... body. The step now uses grep -qF 'cannot publish duplicate version' (fixed-string match). Added a comment in publish.yml citing the source file/commit. Mismatches still fall through to exit $status and fail loudly.

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, ruff check + mypy src/ scripts/ clean, pytest 495 passed with 96.03% coverage.

@github-actions github-actions bot added Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA Ready for QA Dev work complete — QA can begin review and removed Ready for QA Dev work complete — QA can begin review Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 14, 2026
@cmeans cmeans added QA Active QA is actively reviewing; Dev should not push changes and removed Ready for QA Dev work complete — QA can begin review labels Apr 14, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 14, 2026

QA audit — applying Ready for QA Signoff (round 2)

All three O1–O3 observations resolved on new commit 8c583fe:

  • O1: mcp-publisher pinned to v1.5.0 via new composite action input default (pin path of the two options offered; checksum remains optional — OK to defer)
  • O2: composite action at .github/actions/install-mcp-publisher/action.yml referenced from both ci.yml and publish.yml
  • O3: grep replaced with grep -qF 'cannot publish duplicate version' anchored to upstream ErrInvalidVersion constant in modelcontextprotocol/registry:internal/database/database.go — verified via gh search code; exact string is asserted in the registry's own publish_test.go
  • O4: dry-run on real tag remains deferred to first post-merge tag (not addressable in this PR)

Round-2 session verification: pytest 495 passed / coverage 96.03%, ruff clean, mypy clean, all three YAML files parse, validate-server-json CI job (which exercises the new composite action end-to-end) green. Zero findings this round.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 14, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cmeans cmeans added QA Approved Manual QA testing completed and passed and removed Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Apr 14, 2026
@cmeans-claude-dev cmeans-claude-dev bot merged commit 14cc09c into main Apr 14, 2026
37 checks passed
@cmeans-claude-dev cmeans-claude-dev bot deleted the feat/mcp-registry-publish branch April 14, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-publish to MCP registry on release via mcp-publisher

2 participants