How the packages in this repository are versioned and published, and what to do when a publish fails.
No workflow computes or stamps a version at release time. The version in each package's manifest is the source of truth, and it changes only through a reviewed pull request on v2/main. The two languages use different schemes, for a reason that comes from their registries.
everything, filesystem, memory and sequentialthinking use semver, managed by changesets.
- A PR that changes what one of them publishes includes a changeset: a small file under
.changeset/, created withnpm run changeset, naming the package, the bump type and the line that becomes its CHANGELOG entry. - Merged changesets accumulate on
v2/main. On every push there,version-packages.ymlcreates or updates one rolling "Version Packages" PR that consumes them: it bumps each affectedpackage.json, writes the package'sCHANGELOG.md, deletes the consumed changeset files and refreshespackage-lock.json. Merging that PR is what changes a version. - Each server reports that same version in its MCP
serverInfo, read frompackage.jsonat run time, so the bump needs no matching source edit.
Semver policy:
The semver line starts at 1.0.0 for all four packages. Their earlier releases were date-stamped (2025.x, 2026.x), which sorts above any semver number, but npm install and npx resolve the latest dist-tag rather than the highest number, so publishing 1.x is what users get. npm will not move latest to a lower version implicitly, so release.yml always passes the dist-tag explicitly, and chooses it at publish time: latest for the move from the date-stamped line to 1.x and for any version higher than the registry's current latest, and backfill otherwise. The second case is an older release's failed leg re-run after a newer version has published: the version becomes installable by number, and latest does not move backward. Once 1.0.0 is live, a scope maintainer deprecates the date-stamped range (>=2025.0.0 <2027.0.0) by hand, once; the release skill has the command and the check to run first.
fetch, git and time use CalVer (2026.8.1). pip and uv have no dist-tags and always install the numerically highest version, so the existing 2026.x line on PyPI has to keep climbing.
A maintainer dispatches the Prepare Python Release workflow (prepare-python-release.yml) on v2/main, giving it the number of the milestone's release issue. It runs scripts/prepare-python-release.mjs, which stamps today's date onto each Python package with a shipped change since its last version bump, refreshes its uv.lock, and opens a PR whose body starts with Closes #<release issue>. A package with no such change is left alone.
GitHub only lists a workflow_dispatch workflow that exists on the default branch (main). Until the first milestone merge carries this one there, run the same script locally on a branch cut from origin/v2/main and open the PR by hand:
node scripts/prepare-python-release.mjs # prints one "name: old -> new" line per stamped packageNote
Both workflows push their PR's branch with the workflow token, and a push made with that token starts no workflow, so each one then dispatches the TypeScript, Python and DCO workflows on that branch, and Version Packages the Everything diff too (workflow_dispatch is the one event the token can start; the Python stamps touch nothing the diff runs on). A PR does not list dispatched checks, so each dispatched run also reports itself on the PR's head commit as a status: TypeScript (dispatched), Python (dispatched) and DCO (dispatched), plus Everything diff (dispatched) on Version Packages; wait for them to pass before merging. Closing and reopening the PR does not start the CI workflows: that fires pull_request, and those run on push. The token's pull_request events do not run DCO or the diff either: GitHub holds those runs as action_required (#5173), so the PR may also list them as pending or failed with no jobs; the (dispatched) statuses are the results. To run one again by hand: gh workflow run typescript.yml --ref <branch> (or python.yml, dco.yml, everything-mcp-diff.yml). The Version Packages PR's body is rewritten on every push to v2/main, so it is linked to the release issue with addCloseIssueReferences (which lives outside the body), carries a Part of #N comment as its textual record, and gets Part of #N as its body's first line only immediately before it is merged. The repository setting Settings → Actions → General → Allow GitHub Actions to create and approve pull requests must be on, or the workflows fail at the step that opens the PR.
Publishing is triggered by a maintainer publishing a GitHub Release whose tag is on main. There is no scheduled or merge-triggered release. The release tag is only a label: it carries no version, and nothing compares it with one. What the workflow does check is where the tag points: a Release whose commit is not on main fails before any publish job can start, since main only receives reviewed milestone merges.
release.yml runs on release: published, checked out at the release tag. It handles every package as its own pair of jobs, a build and a publish (fail-fast: false, so one package's failure never blocks another):
The split is about what runs next to the publish credential. Installing dependencies runs their lifecycle scripts, and a build runs a toolchain; in a job that can mint an OIDC token, any of that code could publish. Here the token only ever sits beside the registry client and SHA-pinned actions. Every action in the build and publish jobs is pinned to a commit SHA, enforced by npm run verify:action-pins.
The build jobs finish before the publish jobs ask for approval, so wait for every build leg to finish before approving the release environment, and read their logs: they say what each package will do. A package whose build leg passed is safe to approve. One failed build leg does not hold the others back: approve the healthy packages, and handle the failed one as described under When a publish fails.
The registry-diff guard is what makes a release idempotent and self-healing. A package whose version is already on the registry is skipped, not failed:
- npm:
scripts/npm-publish-guard.mjsasks the registry whether the version exists, in the build job. For a published version the job builds nothing and uploads aSKIPmarker, and the publish job exits cleanly on it. A package the registry has never seen counts as "publish it", so a new server's first release works. An answer the guard cannot read fails the job rather than guessing. - PyPI:
skip-existingon the upload action.
So a release publishes exactly the packages whose version moved, and a package whose publish failed is picked up by the next release with nothing to clean up.
Authentication is OIDC trusted publishing on both registries. There are no registry tokens.
- npm: each
@modelcontextprotocol/*package is registered on npmjs.com with a trusted publisher bound to this repository, workflow filenamerelease.yml, and environmentrelease(the binding is case-sensitive). Packages publish with provenance attestations. - PyPI: published via PyPI trusted publishing using
pypa/gh-action-pypi-publish, with the samerelease.yml+releaseenvironment binding.
Because of those bindings, the publish jobs must stay in release.yml and keep the release environment.
A release ships one milestone. The step-by-step procedure, with its commands, is the release skill (/release); this is its shape.
-
A release issue,
Release vX.Y.Z, is filed for the milestone. Every PR below references it, and it stays open until the Release is published. -
Preparation PRs land on
v2/main, each starting withCloses #<release issue>:- the audit (
npm audit, andpip-auditover each Python server's locked requirements): the report goes on the release issue, and any fix it forces is its own commit in a PR; - the Version Packages PR (TypeScript), which is linked to the release issue with
addCloseIssueReferencesand aPart of #Ncomment, and getsPart of #Nas its body's first line just before merging, because the changesets action rewrites its body on every push; - the Prepare Python Release PR (Python).
A milestone that touches both languages and needs an audit fix has all three. They are merged before the next step starts.
- the audit (
-
The merge PR takes
v2/mainintomain. It is a pure merge: its branch isorigin/v2/mainwith no commits of its own, and the merged tree equalsorigin/v2/main's. Its body starts withPart of #<release issue>, notCloses, becausemainis the default branch and a closing keyword there would close the issue before anything is published. It is merged with a merge commit, never squashed. -
The release ledger is linked from the merge PR. It records, for the merge branch's tree:
npm run local:gate;npm run pack:verifyfor each package; each server-facing issue in the milestone exercised through a client; and a targeted probe for each issue with no client surface. The maintainers approve the merge on it. A finding is fixed onv2/mainand merged in again, never committed to the merge branch. -
A maintainer publishes a GitHub Release targeting the merge commit on
main(the commit the ledger verified, chosen by SHA rather than by the branch name, which may have moved).scripts/release-notes.mjs(npm run release:notes) writes the notes, one section per published package with its PRs and a Thanks section for the community members whose issues the release addresses, and with--draftcreates the draft Release under a tag named for the milestone (vX.Y.Z). The maintainer reads the draft and publishes it. The tag is a label, not a version. -
The build jobs run, then the maintainer approves the
releaseenvironment deployments. Each package publishes if its version is not on the registry yet; the rest skip cleanly. -
The release issue is closed by hand, and its card moves to Done.
A failed matrix leg means that one package did not publish; everything that succeeded stays published. When a package's build job fails, its publish job still asks for approval and then fails at the download step, having published nothing: there is no artifact for it to publish.
A transient failure (a registry hiccup, a runner fault): re-run the failed jobs on the same run.
gh run rerun <run-id> --failed --repo modelcontextprotocol/servers- A re-run is still a
release.ymlrun in thereleaseenvironment, so it satisfies the trusted-publisher binding. - It re-runs only the failed legs, at the original release tag. It publishes exactly the released code, and the registry-diff guard keeps already-published packages safe. If the failed leg was a build job, re-run it together with its publish job.
- It needs a fresh
releaseenvironment approval, and the run must be complete first (approve or reject any pending deployments). - The build jobs' artifacts are kept for 7 days. Within that time, re-running a failed publish leg alone works, because the artifact it downloads is still there. After that (GitHub allows re-runs for about 30 days), re-run the package's build leg together with its publish leg, or the whole run (
gh run rerun <run-id>): the build recreates the artifact, and the registry-diff guard skips whatever already published.
A defect in the released code or in the workflow: fix it and release again. A release event runs the workflow as it is at the tag, so a re-run repeats the same broken step. Fix it on v2/main through an ordinary PR, merge v2/main into main, and publish a new Release. The guard publishes whatever never reached the registry and skips the rest: no stranded versions, and no version edits to force it.
Never:
- Publish manually with an npm token or from a laptop. There are no registry tokens, and a manual publish would break the provenance chain.
- Edit a version by hand to force a publish. Versions change only through the Version Packages and Prepare Python Release PRs.
The release environment's required-reviewer list is configured in the repository settings (Settings → Environments → release). Reviewer rights come only from that list: repository admin does not confer deployment approval.
