fix(workflow): bump mcp-publisher v1.5.0 → v1.7.6 for registry OIDC audience#79
Conversation
…udience The v0.5.1 release ran with mcp-publisher v1.5.0 (the existing pin in .github/actions/install-mcp-publisher/action.yml) and the publish-registry job failed at "Authenticate to MCP registry (GitHub OIDC)" with HTTP 401: Token exchange failed: failed to validate OIDC token: invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry] PyPI publish succeeded, GitHub release succeeded, only the registry-publish leg failed. Root cause: registry PR #1229 (`auth: bind GitHub OIDC token exchange to a per-deployment audience`) deployed in v1.7.6 on 2026-04-30 — one day before our 2026-05-01 release. v1.5.0's `login github-oidc` flow uses audience `mcp-registry`; v1.7.6's uses `https://registry.modelcontextprotocol.io`, matching what the prod deployment now validates against. Bumps the action's `default` from v1.5.0 → v1.7.6 and adds an explanatory comment with the rationale for the next bump. v0.5.1 itself is on PyPI cleanly. Re-running the failed registry job against the existing v0.5.1 tag won't pick up this fix because actions/checkout@v6 resolves to the tag's commit; the next release tag will exercise the fix end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (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 Round 1 — One observation
Tight, well-scoped fix for the v0.5.1 publish-registry OIDC failure. Verified the cited evidence chain end-to-end:
| Claim | Verification |
|---|---|
mcp-publisher v1.5.0 audience mismatch on the v0.5.1 publish run |
Pulled run 25218030961 logs: Error: failed to get token: failed to exchange OIDC token: token exchange failed with status 401: ... "failed to validate OIDC token: invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry]". Verbatim match. |
publish-pypi: SUCCESS, github-release: SUCCESS, publish-registry: FAILURE on the v0.5.1 run |
gh run view confirms the per-job tally exactly as stated. |
| Registry deployed PR #1229 in v1.7.6 on 2026-04-30 | Pulled v1.7.6 release notes via gh api: published 2026-04-30T01:03:06Z, body mentions #1229 and audience. |
| Bumping the pin from v1.5.0 → v1.7.6 fixes the audience binding | The failing flow is mcp-publisher login github-oidc; the audience the OIDC exchanger sends is set inside the publisher binary, so a binary upgrade is the right lever. |
Code review
The bump itself is one line in .github/actions/install-mcp-publisher/action.yml:14 (default: v1.5.0 → default: v1.7.6). The new stanza-level comment quotes the actual error verbatim and links the upstream PR — exactly the right shape so the next contributor doesn't have to re-derive the rationale.
I grep'd mcp-publisher and v1.5.0 repo-wide for stale references — see F1 below.
Verification
| Check | Result |
|---|---|
uv run pytest |
550 passed, 100 deselected, 96.13% coverage (no Python touched, expected to be unchanged) |
uv run ruff check src/ tests/ scripts/ |
clean |
uv run ruff format --check src/ tests/ scripts/ |
72 files already formatted |
uv run mypy src/ scripts/ |
clean (30 files, strict-mode) |
Required CI on fd13040f |
13/13 green (vdsm completed SUCCESS) |
PR-body manual tests 1–4 verified directly; #5 is a post-merge tag-push observation by design.
Findings
| ID | Finding |
|---|---|
| F1 (observation) | Stale verification annotation in .github/workflows/publish.yml:78 — the comment reads (verified against tag v1.5.0) referring to the upstream ErrInvalidVersion constant that the duplicate-version idempotency grep anchors on. This PR bumps the installed publisher to v1.7.6, so the annotation now references the wrong version. I checked: the error string invalid version: cannot publish duplicate version is still present in internal/database/database.go at v1.7.6 (ErrInvalidVersion = errors.New("invalid version: cannot publish duplicate version")), so the idempotency grep still works correctly — this is purely an annotation freshness issue, not a logic bug. Suggested fix: either (a) flip the annotation to (verified against tag v1.7.6), or (b) rewrite version-agnostically (verified upstream against the currently-pinned mcp-publisher tag). One-line edit. Per the project's doc-drift-is-substantive rule, blocking. |
Verdict
QA Failed Round 1. F1 is a single-line comment edit; round 2 should be quick. The actual fix is solid and the evidence chain checks out.
|
Applying QA Failed as the final act of round 1. Single observation F1: stale |
…stically Addresses QA F1 on PR #79. The annotation in publish.yml:78 read "(verified against tag v1.5.0)" — it was true at write time but became stale the moment we bumped the pin to v1.7.6 in this same PR. Rewrote it as "verified upstream against the currently-pinned mcp-publisher tag in .github/actions/install-mcp-publisher/action.yml" so future bumps don't need to re-touch this comment. The underlying grep string is unchanged — QA confirmed the upstream ErrInvalidVersion string is still present in v1.7.6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 — F1 addressedPicked QA's option (b) — rewrote the annotation version-agnostically so future ```diff
``` The underlying grep string is unchanged — QA already confirmed No code or test changes; CI on the new head should match the pre-fix run. Ready for round 2. |
cmeans
left a comment
There was a problem hiding this comment.
QA Round 2 — PASS
F1 closed via the version-agnostic option I suggested (option b). The new annotation in publish.yml:75-79 reads:
verified upstream against the currently-pinned mcp-publisher tag in
.github/actions/install-mcp-publisher/action.yml
That's the right shape — future pin bumps don't require touching this comment, so the same drift class can't recur. Underlying grep string is unchanged; the upstream ErrInvalidVersion constant text I verified at v1.7.6 in round 1 still anchors the idempotency check correctly.
Bonus: the commit message acknowledges the round-1 verification work explicitly ("QA confirmed the upstream ErrInvalidVersion string is still present in v1.7.6"). Helpful audit trail.
Verification (re-run on bdd9e7a)
| Check | Result |
|---|---|
uv run pytest |
550 passed, 100 deselected, 96.13% coverage |
uv run ruff check src/ tests/ scripts/ |
clean |
uv run ruff format --check src/ tests/ scripts/ |
72 files already formatted |
uv run mypy src/ scripts/ |
clean (30 files, strict-mode) |
Required CI on bdd9e7a |
13/13 green (vdsm completed SUCCESS) |
Verdict
Ready for QA Signoff. F1 closed; no other findings. Final maintainer call.
|
Applying Ready for QA Signoff as the final act of round 2. F1 closed via the version-agnostic rewrite (option b from round 1) — annotation now references the canonical pin location instead of a specific tag, so the same drift class can't recur on future bumps. 550/550 pass, 96.13% coverage, ruff/mypy clean, 13/13 required CI green. Final maintainer call. |
## Summary Cuts v0.5.2, shipping six PRs that landed since v0.5.1 (2026-05-01): - **#79** mcp-publisher v1.5.0 → v1.7.6 (registry OIDC audience fix) - **#80** keyring exception handler narrowing (closes #38) - **#81** bg update-check executor timeout + log (closes #39) - **#82** pygments 2.19.2 → 2.20.0 (GHSA-5239-wwwm-4pmq, ReDoS) - **#83** `--revert` version-string validation (closes #40) - **#85** per-path serial for `move_files` + `copy_files` (closes #84) ## Why now Two recent bug fixes (#83, #85) are user-visible enough to warrant shipping, and #84 in particular is a confusing silent-no-op regression on multi-file moves — getting that to PyPI promptly matters. The four post-0.5.1 quality fixes (#79–#82) are stacked behind it. This release also exercises **#79's mcp-publisher v1.7.6 pin end-to-end** so the registry can catch up to current. v0.5.1's registry entry is missing because #79 landed AFTER the v0.5.1 tag-push, and `actions/checkout@v6` resolved to the tag's commit on re-runs of the failed `publish-registry` job — the fix wasn't picked up. The v0.5.2 tag will pull the correct pin from main. ## State after merge Bug-labeled issue queue is empty. The structural multi-path-serial fix family (delete + getinfo + move + copy + restore) is now complete on real DSM 7.x — every File Station write tool that takes a `paths: list[str]` issues one DSM task per path, sidestepping the comma-joined-multipath quirk that #68 and #84 each surfaced. ## Files changed - `pyproject.toml` — version 0.5.1 → 0.5.2 - `server.json` — auto-synced via `python scripts/sync-server-json.py` - `uv.lock` — refreshed via `uv lock` - `CHANGELOG.md` — `## Unreleased` (with the six entries above) renamed to `## 0.5.2 (2026-05-01)`, fresh empty `## Unreleased` inserted above it for the next cycle ## Test plan - [x] CI green on this branch (lint, typecheck, test 3.11/3.12/3.13, vdsm integration tests, version-sync, validate-server-json) - [ ] After merge: tag `v0.5.2` push fires `publish.yml`; PyPI publish succeeds - [ ] After merge: `publish-registry` job succeeds end-to-end (this is the validation point for #79's fix — the failure mode in v0.5.1 was `invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry]`) - [ ] After tag: `mcp-synology --check-update` from a v0.5.1 install reports v0.5.2 available; `uv tool install mcp-synology@latest` upgrades cleanly - [ ] Smoke (post-install): two-file `move_files` actually moves both files (the #84 regression scenario) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Closes #44. Adds a `validate-server-json` job to `publish.yml` that runs **before** `publish-pypi` on every release tag, mirroring the same check that already runs on every PR via `ci.yml`. ## Why Without this gate, a malformed `server.json` (new required field in a future registry schema, type change, etc.) would PyPI-publish cleanly and then fail at the registry leg — leaving a discoverable PyPI release that isn't in the MCP registry, with no way to roll back PyPI short of yanking. Re-running the failed registry job can't fix that asymmetry. v0.5.1 hit the registry-leg-fails-after-PyPI scenario for a different reason (mcp-publisher OIDC audience mismatch, fixed in #79). The failure mode isn't theoretical — it just hasn't yet fired for a *schema* reason. ## What changed - New `validate-server-json` job in `publish.yml` runs `./mcp-publisher validate server.json` using the same `./.github/actions/install-mcp-publisher` composite that `publish-registry` uses. Validating publisher version always matches publishing publisher version. - `publish-pypi` now lists `needs: [build, validate-server-json]` so a validation failure stops the entire pipeline before any external side-effect. - `validate-server-json` runs in parallel with `build` — no dep on it — so the gate adds ~10s to the critical path, not a serial round-trip. ## Why not the optional manifest artifact The `publish-manifest` artifact (record `mcp-publisher`/`uv`/Python versions for CI-PR vs release-tag reconciliation) is named as optional in #44. Deferring — it's a debugging aid that's only useful if a CI-PR / release-tag mismatch *does* fire, and is trivially additive later when there's a concrete failure to investigate. ## Test plan - [x] `python -c "import yaml; yaml.safe_load(open('.github/workflows/publish.yml'))"` parses - [x] CI green on this PR (lint/typecheck/test/version-sync/validate-server-json/vdsm) - [ ] After merge: next release tag (v0.5.3 or whichever) shows the new `validate-server-json` job in the publish.yml run, gating `publish-pypi` - [ ] Negative test (informal): if you intentionally corrupt `server.json` on a branch and tag-push, the publish workflow stops at validate-server-json with no PyPI side-effect — won't actually run this since it requires breaking server.json on main; the validate step is the same `./mcp-publisher validate` invocation that ci.yml exercises on every PR, so it's already proven to fail loudly on schema errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ublish-registry workflow (#120) Closes #114. Five-file registry-publish setup ported from mcp-synology. No runtime code change. ## Files added | File | Role | |---|---| | `server.json` (root) | Registry manifest. `io.github.cmeans/mcp-clipboard` name (the GitHub-OIDC namespace), starts at version `2.4.0` to match what just shipped to PyPI. | | `scripts/sync-server-json.py` | Single-source-of-truth sync from `pyproject.toml`'s `[project].version` to `server.json`'s two version fields (top-level + `packages[0]`). Stdlib only — runs before any `uv sync` in CI. `--check` mode exits 1 on drift. | | `.github/actions/install-mcp-publisher/action.yml` | Composite action that downloads a pinned `mcp-publisher` (default `v1.7.6`). The pin is load-bearing: the registry rolled out a new GitHub-OIDC audience on 2026-04-30; earlier publisher releases (incl. `v1.5.0`) fail with `401 invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry]`. | ## Workflow changes **`ci.yml` gains two jobs:** - `version-sync` — runs `python scripts/sync-server-json.py --check`. Fails PRs that bump `pyproject.toml` without re-running the sync. - `validate-server-json` — runs `./mcp-publisher validate server.json` against the live registry schema. Catches schema drift at PR time rather than at tag-push time. **`publish.yml` gains:** - `validate-server-json` (release-time backstop). `publish-pypi` is now `needs: [build, validate-server-json]`. Job renamed from `publish` → `publish-pypi` for symmetry with the new `publish-registry` sibling. (Workflow runs only on tag pushes, no PR branch protection references this job name, so the rename is safe.) - `publish-registry` (runs after `publish-pypi`). Uses GitHub OIDC token exchange — no API key, no secret. Idempotent on duplicate-version errors so a partially-failed tag rerun is a no-op; if the upstream error text changes, falls through to `exit $status` and surfaces the real error rather than silently swallowing it. ## Why this matters PyPI publishes are **irreversible per-version**: a yanked release still occupies the version slot. If the registry schema changes between PR-merge and tag-push and we discover it only on the publish run, PyPI ships fine but the registry leg fails — leaving a discoverable PyPI release that isn't in the MCP registry, with no way to fix without bumping the version. Both gates (`ci.yml` PR-time + `publish.yml` release-time) close that window. Reference for the same pattern landing in mcp-synology: - cmeans/mcp-synology#16 — initial `server.json` + first registration attempt. - cmeans/mcp-synology#44 — gating story (why validate runs at both PR time and release time). - cmeans/mcp-synology#79 — the `v1.5.0 → v1.7.6` mcp-publisher bump after v0.5.1's registry leg failed on the OIDC audience mismatch. - cmeans/mcp-synology#89 — `publish-pypi` wired to `needs: [build, validate-server-json]`. - cmeans/mcp-synology#92 — `sync-server-json.py` ergonomics improvements. ## Verification - `python scripts/sync-server-json.py --check` → `server.json in sync with pyproject.toml (2.4.0)`. - `uv run pytest -q`: **548 passed**, 16 deselected, 5 xfailed (no runtime code changed). - `uv run ruff check src/ tests/`: clean. - New CI jobs (`version-sync`, `validate-server-json`) will run on this PR and serve as the first smoke test of the gates themselves. ## First-run plan server.json starts at `2.4.0` (the just-shipped version). After this PR merges: - The next release tag (whether `v2.4.1`, `v2.5.0`, or whatever comes next) fires `publish.yml` → `validate-server-json` → `publish-pypi` → `publish-registry`. - `publish-registry` registers `io.github.cmeans/mcp-clipboard` with the registry for the first time. - Subsequent releases update the entry in place. Optionally we could re-tag `v2.4.0` after merge to register immediately — `publish-pypi` will hit a duplicate-version no-op on PyPI, and `publish-registry` will register the entry. Not strictly necessary; can wait for the next bump. ## Test plan - [x] CI green across `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, `validate-server-json`. - [ ] First post-merge tag-push lands all three publish jobs green. - [ ] `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=mcp-clipboard'` returns a hit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
The v0.5.1 release run hit
publish-registryfailure at "Authenticate to MCP registry (GitHub OIDC)" with HTTP 401:```
Token exchange failed: failed to validate OIDC token: invalid audience:
expected https://registry.modelcontextprotocol.io, got [mcp-registry]
```
PyPI publish ✅, GitHub release ✅, registry publish ❌. v0.5.1 is on PyPI; only directory metadata is missing.
Root cause
The MCP registry deployed
modelcontextprotocol/registry#1229— "auth: bind GitHub OIDC token exchange to a per-deployment audience" — inv1.7.6on 2026-04-30, one day before our 2026-05-01 release. Our.github/actions/install-mcp-publisher/action.ymlpinnedmcp-publisherto v1.5.0, which sends audiencemcp-registry. The new registry server requireshttps://registry.modelcontextprotocol.io, mints a 401 on mismatch.Fix
One-line bump of the pin's default from
v1.5.0→v1.7.6. Added a stanza-level comment with the rationale so future maintainers understand why this can't drift back.What this PR does NOT do
publish-registryjob on the existingv0.5.1tag won't pick up this fix becauseactions/checkout@v6resolves to the tag's commit (which doesn't have the fix). The next release tag-push will exercise the fix end-to-end. Until then, v0.5.1 lives on PyPI but not in the registry directory — purely a metadata gap, not a user-install issue.QA
Manual tests
.github/actions/install-mcp-publisher/action.ymlshowsdefault: v1.7.6and the new explanatory comment.## Unreleased→### Fixed(the next release will roll this up).publish-pypi: SUCCESS,github-release: SUCCESS,publish-registry: FAILUREwith the cited audience-mismatch error.publish-registryjob.Verification I already ran
mcp-publisherv1.7.6 release notesgit diff --stat HEAD~1.github/actions/install-mcp-publisher/action.yml+CHANGELOG.md🤖 Generated with Claude Code