chore(release): register with MCP Server registry via server.json + publish-registry workflow#120
Merged
Merged
Conversation
…ublish-registry workflow Adds the five-file registry-publish setup ported from mcp-synology (reference: cmeans/mcp-synology PRs #16, #79, #89, #92, issue #44). New files: - server.json — registry manifest at the repo root. Carries the io.github.cmeans/mcp-clipboard name (the GitHub-OIDC namespace) and duplicates pyproject.toml's version in two fields (top-level and packages[0]) per the registry schema. - scripts/sync-server-json.py — single-source-of-truth sync from pyproject.toml's [project].version into server.json's two version fields. Stdlib only (so CI can run it before any uv sync). Two CLI surfaces: bare invocation rewrites server.json in place; --check exits 1 on drift for the CI gate. - .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, and earlier publisher releases (incl. v1.5.0) send the legacy audience and fail authentication with 401 invalid audience. Workflow changes: - ci.yml gains two jobs: - version-sync — runs `python scripts/sync-server-json.py --check`, failing PRs that bump pyproject.toml without re-running the sync. - validate-server-json — runs `./mcp-publisher validate server.json` against the live registry schema, catching schema drift at PR time rather than at tag-push time. Critical because PyPI publishes are irreversible per-version, so a half-publish where PyPI accepts but the registry leg fails on schema drift would require a yank-and-bump cleanup. - publish.yml gains: - validate-server-json (release-time backstop in case the registry schema changes between merge and tag-push). publish-pypi is now `needs: [build, validate-server-json]` (renamed from `publish` to `publish-pypi` for symmetry with the new sibling). - publish-registry (runs after publish-pypi, since the registry validates the referenced PyPI package+version before accepting). Uses GitHub OIDC token exchange — no API key. Idempotent on duplicate-version errors so a partially-failed tag rerun is a no-op rather than a crash; if the upstream error text changes, this falls through to `exit $status` and surfaces the real error rather than silently swallowing it. CHANGELOG entry under [Unreleased] / Added. server.json starts at 2.4.0 (the just-shipped version). The first post-merge release tag will register io.github.cmeans/mcp-clipboard for the first time; subsequent releases update the entry in place. No runtime code change. 548 tests still pass; ruff clean; sync-script --check passes. Closes #114. 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! |
PR #120 CI Round 1: validate-server-json failed with server returned status 422: "expected length <= 100" body.description value: "MCP server that reads and writes the system clipboard — tables, text, code, JSON, URLs, images, and more." (107 chars) The MCP registry caps server.json's `description` at 100 chars; our pyproject.toml / README / FastMCP-instructions copy is 107 chars and gets used everywhere else as-is. The cap only applies to the registry manifest, so the fix is server.json-local: drop the "MCP server that" prefix (the registry context already implies that), keep the value-rich tail. Down to 87 chars and validates clean against the live registry schema. Other description copy (pyproject.toml, README, instructions/server.md) intentionally left longer — pip search results, FastMCP startup banner, and the GitHub repo description are not subject to the registry cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmeans
reviewed
May 6, 2026
Owner
cmeans
left a comment
There was a problem hiding this comment.
QA Review — Round 1
Verdict: Pass — zero findings.
Clean port of the mcp-synology registry-publish setup. Five files added (server.json, scripts/sync-server-json.py, .github/actions/install-mcp-publisher/action.yml, plus two workflow patches) + a CHANGELOG ### Added entry. No runtime code touched.
Issue #114 acceptance
| Item | Status |
|---|---|
server.json (root, registry manifest) |
Present, namespaced as io.github.cmeans/mcp-clipboard, schema 2025-12-11, version 2.4.0 (matches just-shipped PyPI), packages[0] = pypi/mcp-clipboard/2.4.0/stdio. Description 87 chars (under the registry's 100-char cap — addressed by follow-up commit 5ab1a56). |
scripts/sync-server-json.py (stdlib only, --check mode) |
Present and clean. tomllib → [project].version, propagates to top-level version + each packages[i].version, preserves 2-space JSON indent + trailing newline. Drift simulation (manually setting top_level to 99.0.0) → exit 1 with "server.json version drift detected. pyproject.toml = 2.4.0 top_level: '99.0.0'". In-sync run on the actual repo → exit 0 with "server.json in sync with pyproject.toml (2.4.0)". |
install-mcp-publisher composite action |
Pinned to v1.7.6 (the audience-binding fix per registry PR #1229 deployed 2026-04-30); rationale comment in the action input default explains the v1.5.0 → v1.7.6 jump precisely. Cross-platform os/arch derivation handles linux/amd64 + linux/arm64. |
ci.yml gains version-sync + validate-server-json |
Both jobs SUCCESS on this PR's CI run. version-sync uses stdlib python (no uv sync needed); validate-server-json uses the same composite action as release-time. |
publish.yml gains release-time validate-server-json + publish-registry |
publish-pypi (renamed from publish) now needs: [build, validate-server-json]. Rename is safe — verified publish.yml triggers on push: tags: v* only (no PR runs), and main isn't branch-protected (404 on repos/.../branches/main/protection), so no required-check renames needed. publish-registry runs after publish-pypi, uses id-token: write for OIDC, and the duplicate-version idempotency block correctly falls through to exit $status for any non-matching error text rather than silently swallowing real failures. |
CHANGELOG ### Added entry |
Present under [Unreleased], accurate description, (#120) - closes #114 link. |
| Schema/registry self-validation in CI | validate-server-json against mcp-publisher v1.7.6 SUCCESS — proves the actual JSON passes the live registry schema. |
Reference cross-check (mcp-synology pattern)
| mcp-synology PR | What it taught | Picked up here? |
|---|---|---|
| #16 | Initial server.json + first registration attempt |
✓ Same shape |
| #44 | Why validate-server-json runs at both PR-time and release-time |
✓ Both gates present |
| #79 | The v1.5.0 → v1.7.6 mcp-publisher bump after the OIDC audience mismatch |
✓ Pinned to v1.7.6 |
| #89 | publish-pypi wired to needs: [build, validate-server-json] |
✓ Same shape |
| #92 | sync-server-json.py ergonomics (distinguish missing-[project] vs missing-version-field) |
✓ Both errors distinguished (error: [project] section missing vs error: [project].version not found) |
Verification (run locally on chore/mcp-registry-114 @ 5ab1a56)
| Check | Result |
|---|---|
uv run pytest -q |
548 passed / 16 deselected / 5 xfailed (matches PR body — no runtime change) |
uv run ruff check src tests |
All checks passed |
uv run ruff format --check src tests |
10 files already formatted |
uv run mypy src |
Success: no issues found in 4 source files |
uv run ruff check scripts |
All checks passed (new file lints clean) |
python3 scripts/sync-server-json.py --check |
server.json in sync with pyproject.toml (2.4.0) |
Drift simulation (top_level=99.0.0) |
exits 1 with the correct diagnostic |
server.json description length |
87 chars (under 100-char registry cap) |
actions/checkout@v6 etc. pin convention |
Floating major-version tags throughout existing ci.yml and publish.yml; new jobs match the convention. (mcp-synology has SHA-pinned via #94 but mcp-clipboard hasn't adopted that yet — out of scope for #114; follow-up if/when desired.) |
publish.yml trigger surface |
on: push: tags: ['v*'] — no PR-time runs, so the publish → publish-pypi rename does not affect any branch-protection required-check name |
CI on 5ab1a56 |
All green: lint, typecheck, test (3.11/3.12/3.13), integration-x11, codecov/patch, plus the new version-sync and validate-server-json SUCCESS |
Test-plan checkbox status (post-review)
- CI green across
lint,typecheck,test (3.11/3.12/3.13),integration-x11,version-sync,validate-server-json— verified - First post-merge tag-push lands all three publish jobs green — post-merge step
-
curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=mcp-clipboard'returns a hit — post-merge step (will land on the next release tag)
Follow-up tickets
None for #114 itself. One observation worth surfacing to the user (not gating, not filing as an issue unless you want):
- The
descriptionfield inserver.json(87 chars) is necessarily different frompyproject.toml'sdescription(107 chars) because of the registry's 100-char cap. The sync script currently only syncsversion, notdescription, so if the pyproject description ever evolves the registry one will silently stay frozen. This is the right call for #114 (description-sync is harder than version-sync because of the cap), and updating the registry description is rare, so flagging it just to keep it on your radar — no action needed.
Applying Ready for QA Signoff as the final act.
Owner
|
QA Audit — PR #120 / round 1
Applying Ready for QA Signoff as the final act. |
Merged
4 tasks
cmeans-claude-dev Bot
added a commit
that referenced
this pull request
May 6, 2026
Release v2.5.0. Aggregates the four PRs since v2.4.0 (2026-05-05). ## Scope ### Added - **#120 — register with the MCP Server registry** (closes #114). New `server.json` (root) carries the registry manifest; `scripts/sync-server-json.py` is the single-source-of-truth sync from `pyproject.toml`'s `[project].version` to `server.json`'s two version fields. New composite action `.github/actions/install-mcp-publisher` pins `mcp-publisher` to `v1.7.6+` for the post-2026-04-30 OIDC audience requirement. CI gains `version-sync` + `validate-server-json` jobs; `publish.yml` gains a release-time `validate-server-json` gate (now `needs:` of `publish-pypi`) and a new `publish-registry` job that runs after `publish-pypi`. **This release is the first to fire `publish-registry`, registering `io.github.cmeans/mcp-clipboard` for the first time.** Subsequent releases update the entry in place. - **#121 — `clipboard_copy_markdown` tool** (closes #109). Renders a markdown source to HTML via `markdown-it-py` (with raw HTML escaped, safe by construction) and writes both `text/html` (rendered) and `text/plain` (the markdown source) to the clipboard. macOS and Windows write both formats atomically (NSPasteboard / `DataObject`); Wayland and X11 are single-MIME-per-call and write `text/html` only — Wayland's `wl-copy` auto-advertises `text/plain` whose bytes are the rendered HTML markup, X11 has no `text/plain` target. Adds `markdown-it-py>=3.0` as a new runtime dependency (pure Python, ~250 KB, no native deps). - **#122 — PRIMARY-selection support on read tools** (closes #110). `clipboard_paste`, `clipboard_read_raw`, and `clipboard_list_formats` now accept an optional `selection` argument (`"clipboard"` default, `"primary"` for the X11 PRIMARY / Wayland primary middle-click selection). macOS and Windows have no PRIMARY analog and return a clear error if `"primary"` is passed. Public APIs `read_clipboard`, `list_clipboard_formats`, and `read_clipboard_image` gained the same `selection` parameter. ### Closed without engagement - **#119 — SafeSkill drive-by scanner promotion.** Closed as duplicate of an identical filing on cmeans/mcp-synology. Same auto-template hit both repos; the methodology has gaps (`Files Scanned: 0` on Python projects, then asks for a promotional badge). ## Verification - `uv run pytest -q`: **599 passed**, 19 deselected, 5 xfailed. - `uv run ruff check src/ tests/ scripts/`: clean. - `uv run mypy src/`: clean. - `python scripts/sync-server-json.py --check`: server.json in sync with pyproject.toml (2.5.0). - `uv build --wheel`: builds `dist/mcp_clipboard-2.5.0-py3-none-any.whl` successfully. - All four landing PRs were QA-approved on `main` with green CI before this aggregation. ## Release commit shape Three-file diff: `pyproject.toml` (2.4.0 → 2.5.0), `CHANGELOG.md` (header roll, no other content changes), and `server.json` (synced via `scripts/sync-server-json.py` per the new release flow introduced in #120). The `version-sync` CI gate verifies the sync is correct before merge. ## Tag plan After this PR merges, push `v2.5.0` tag to trigger `publish.yml`: 1. `validate-server-json` — schema check against the live registry. 2. `publish-pypi` — uploads `mcp_clipboard-2.5.0-py3-none-any.whl` + sdist to PyPI via OIDC trusted publisher. 3. `publish-registry` — registers `io.github.cmeans/mcp-clipboard` in the official MCP registry. **First-run.** 4. (no `github-release` job yet — that's a future-port from mcp-synology if/when it's worth the diff.) The `[Unreleased]` section is now empty, ready for the next cycle. ## Test plan - [x] CI green across `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, `validate-server-json`. - [x] `uv build --wheel` succeeds locally on a clean checkout (verified pre-PR). - [ ] Tag push triggers `publish.yml` and the PyPI release lands. - [ ] `publish-registry` job lands green on the first run; `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=mcp-clipboard'` returns a hit afterward. 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>
4 tasks
cmeans-claude-dev Bot
added a commit
that referenced
this pull request
May 6, 2026
…idation (#125) Patch release fixing the v2.5.0 `publish-registry` failure. ## What happened v2.5.0 (PR #124, tag `v2.5.0` pushed) ran `publish.yml`. `publish-pypi` succeeded — `mcp-clipboard 2.5.0` is on PyPI. But `publish-registry` (the first-ever firing of the registry-publish workflow added in #120) failed: ``` Error: publish failed: server returned status 400 {"title":"Bad Request","status":400,"detail":"Failed to publish server", "errors":[{"message":"registry validation failed for package 0 (mcp-clipboard): PyPI package 'mcp-clipboard' ownership validation failed. The server name 'io.github.cmeans/mcp-clipboard' must appear as 'mcp-name: io.github.cmeans/mcp-clipboard' in the package README"}]} ``` The MCP registry validates ownership by looking for that literal token in the README that ships **with the PyPI package**. mcp-synology embeds the same comment at `README.md:31` (`<!-- mcp-name: io.github.cmeans/mcp-synology -->`); we missed mirroring this in the registry-setup PR (#120). Adding the token to `main` alone doesn't satisfy the v2.5.0 validation — the registry check runs against the already-published v2.5.0 PyPI artifact, whose README is fixed at upload time. The only path forward is a new release that bakes the token into a fresh wheel. ## Fix - `README.md`: add `<!-- mcp-name: io.github.cmeans/mcp-clipboard -->` after the intro paragraph, matching mcp-synology's placement. - `pyproject.toml`: `2.5.0 → 2.5.1`. - `server.json`: synced to `2.5.1` via `scripts/sync-server-json.py` per the new release flow from #120. - `CHANGELOG.md`: `[2.5.1]` entry under `### Fixed` explaining the registry validation requirement and the v2.5.0 → v2.5.1 path. `hatchling` re-bundles `README.md` into the wheel's `PKG-INFO`/`long_description` on every build, so v2.5.1's PyPI entry will carry the validating string. ## What stays as-is - **v2.5.0 on PyPI:** stays. PyPI version slots are immutable, and v2.5.0 is functionally correct (all features work). It just doesn't satisfy the registry's ownership check, so it remains unregistered. - **v2.5.1 on PyPI:** will land on tag-push, with the validating README. - **v2.5.1 in the MCP registry:** `publish-registry` will re-run on the new tag and register `io.github.cmeans/mcp-clipboard` for the first time. ## Verification - `uv run pytest -q`: **599 passed**, 19 deselected, 5 xfailed (no test changes). - `uv run ruff check src/ tests/ scripts/`: clean. - `python scripts/sync-server-json.py --check`: server.json in sync (2.5.1). - `uv build --wheel`: builds `dist/mcp_clipboard-2.5.1-py3-none-any.whl` successfully. ## Test plan - [x] CI green across `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, `validate-server-json`. - [x] `uv build --wheel` succeeds; the wheel's `PKG-INFO` / `long_description` includes the `mcp-name` comment. - [ ] Tag-push fires `publish.yml` and all three jobs (validate, publish-pypi, publish-registry) land green. - [ ] `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=mcp-clipboard'` returns a hit afterward. 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>
This was referenced May 6, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #114. Five-file registry-publish setup ported from mcp-synology. No runtime code change.
Files added
server.json(root)io.github.cmeans/mcp-clipboardname (the GitHub-OIDC namespace), starts at version2.4.0to match what just shipped to PyPI.scripts/sync-server-json.pypyproject.toml's[project].versiontoserver.json's two version fields (top-level +packages[0]). Stdlib only — runs before anyuv syncin CI.--checkmode exits 1 on drift..github/actions/install-mcp-publisher/action.ymlmcp-publisher(defaultv1.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 with401 invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry].Workflow changes
ci.ymlgains two jobs:version-sync— runspython scripts/sync-server-json.py --check. Fails PRs that bumppyproject.tomlwithout re-running the sync.validate-server-json— runs./mcp-publisher validate server.jsonagainst the live registry schema. Catches schema drift at PR time rather than at tag-push time.publish.ymlgains:validate-server-json(release-time backstop).publish-pypiis nowneeds: [build, validate-server-json]. Job renamed frompublish→publish-pypifor symmetry with the newpublish-registrysibling. (Workflow runs only on tag pushes, no PR branch protection references this job name, so the rename is safe.)publish-registry(runs afterpublish-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 toexit $statusand 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.ymlPR-time +publish.ymlrelease-time) close that window.Reference for the same pattern landing in mcp-synology:
server.json+ first registration attempt.v1.5.0 → v1.7.6mcp-publisher bump after v0.5.1's registry leg failed on the OIDC audience mismatch.publish-pypiwired toneeds: [build, validate-server-json].sync-server-json.pyergonomics 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.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:v2.4.1,v2.5.0, or whatever comes next) firespublish.yml→validate-server-json→publish-pypi→publish-registry.publish-registryregistersio.github.cmeans/mcp-clipboardwith the registry for the first time.Optionally we could re-tag
v2.4.0after merge to register immediately —publish-pypiwill hit a duplicate-version no-op on PyPI, andpublish-registrywill register the entry. Not strictly necessary; can wait for the next bump.Test plan
lint,typecheck,test (3.11/3.12/3.13),integration-x11,version-sync,validate-server-json.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