All three hit during the 0.8.2 release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.7 KiB
Release checklist
Every release ships in three places that must stay in lockstep: git (tag + GitHub Release), PyPI (the artifact) and the official MCP Registry (metadata pointing at that artifact). Skipping the third one leaves the registry advertising an older version.
Verified end to end on 2026-09-12 (0.8.0 → 0.8.1) and 2026-09-14 (0.8.2).
0. Before you start
- Working tree clean, CI green,
CHANGELOG.mdsection for the new version written. - Decide the version (SemVer). New user-facing features → minor bump.
1. Bump the version in three files
pyproject.toml version = "X.Y.Z"
nickol_knx_mcp/__init__.py __version__ = "X.Y.Z"
server.json "version" AND "packages"[0]."version"
server.json carries the version twice. The registry rejects a publish whose package version
does not exist on PyPI, so both must match the artifact you are about to upload.
2. Build and check
rm -rf dist build *.egg-info
uv build --out-dir dist .
uvx twine check dist/*
Both artifacts must say PASSED.
Run the local corpus guard before the build — it is the only check that sees real ETS projects:
python tools/corpus_check.py
It must exit 0. If it reports drift, either the change is a regression, or the new numbers are
intended and tools/corpus_check.py --update belongs in a commit of its own before the release.
Do not release on unexplained drift.
3. Upload to PyPI (owner, needs the API token)
TWINE_USERNAME=__token__ uvx twine upload dist/nickol_knx_mcp-X.Y.Z*
Token lives in 1Password. A version can never be re-uploaded — a mistake in the package metadata costs a new patch version, so verify step 4 before uploading when possible.
4. Ownership marker (check before uploading)
The registry proves ownership of a PyPI package by finding this exact line in the package
description, i.e. in README.md:
<!-- mcp-name: io.github.NickoScope/nickol-knx-mcp -->
- The name is case-sensitive and must equal
nameinserver.jsoncharacter for character. GitHub auth grantsio.github.NickoScope/*— lowercasenickoscopeis rejected with 403. - Verify it survived into the built artifact before uploading:
python -c "import zipfile;z=zipfile.ZipFile('dist/nickol_knx_mcp-X.Y.Z-py3-none-any.whl');\
print('mcp-name: io.github.NickoScope/nickol-knx-mcp' in z.read([n for n in z.namelist() if n.endswith('METADATA')][0]).decode())"
5. Publish to the official MCP Registry
mcp-publisher validate # schema + field limits
mcp-publisher login github # device flow in the browser, owner action; token cached in ~/.config/mcp-publisher
mcp-publisher publish
Confirm:
curl -s "https://registry.modelcontextprotocol.io/v0/servers/io.github.NickoScope%2Fnickol-knx-mcp/versions" | python3 -m json.tool | head -40
Expect the new version with "status": "active" and "isLatest": true. A plain ?search= without
&version=latest returned the previous version for a while after a successful publish (2026-09-14).
6. Git side
Tag, GitHub Release, submodule pointer bump in the workspace repo. Per the standing rule this
part is delegated to the github-manager agent (version / CHANGELOG / tag / release / PII scan).
Field limits and traps (hit for real)
| Trap | Symptom | Fix |
|---|---|---|
description in server.json over 100 chars |
422 expected length <= 100 on validate |
shorten; the long text stays in the README |
| server name in the wrong case | 403 You have permission to publish: io.github.NickoScope/* |
match the GitHub login exactly |
| marker in the published description in the wrong case | 400 … must appear as 'mcp-name: …' in the package README |
fix README, bump patch version, re-upload (PyPI forbids overwriting) |
| PyPI JSON API lags a few minutes | latest still shows the old version |
check https://pypi.org/simple/nickol-knx-mcp/ instead |
| registry login token expired (it lasts days, not weeks) | 401 Invalid or expired Registry JWT token on publish |
owner runs mcp-publisher login github again (device code in the browser), then publish |
| registry search shows the old version right after publish | ?search= returns the previous version |
query /v0/servers/<name>/versions or add &version=latest |
mcp-publisher not on PATH |
the binary was only extracted to a temp dir last time | re-download from the registry's GitHub releases, or keep it at a fixed path |
Downstream
PulseMCP ingests from the official registry automatically (their manual submission has been paused since mid-2026). mcp.so is paid-only since 2026 and is deliberately skipped. awesome-mcp-servers already lists the project; only description edits are needed there.