Skip to content

plan: v2 go-live runbook — deprecate v1 (1.0.1), branch v1/main, swap main to v2, publish 2.0.0, close v1 backlog, restrict external PRs #1804

Description

@cliffhall

Summary

Produce (and then execute) the plan for going live with v2 — making v2 the main branch and the latest npm release, deprecating the v1 line while keeping it alive for security fixes only, and tightening the contribution model to issues-from-external-contributors.

This issue is for writing and agreeing the plan. Each phase below should become its own tracked issue once the plan is signed off; nothing here should be executed ad hoc, since several steps are irreversible (npm publishes, bulk issue closure).

Phase issues

Filed on board #28 under the V2 Go Live column. Execute in order; ⚠️ marks the irreversible ones.

Phase Issue Covers
1 #1824 Create v1/main, protect it, give it CI (§2, §10)
2 #1815 ⚠️ v1 deprecation notice + publish 1.0.1 from v1/main — the OIDC rehearsal (§4)
3 #1816 --tag v1-latest on publish-all, v1-latest dist-tag, npm deprecate all four names (§1, §3)
4 #1817 Replace main's tree with v2 (§5)
5 #1818 ⚠️ Publish 2.0.0 — rc under next first, rollback plan written first (§6, §7)
6 #1820 Restrict PRs to collaborators; CONTRIBUTORS.md (#1517), SECURITY.md (§9)
7 #1819 ⚠️ Triage + bulk-close the v1 backlog — 125 PRs, not ~30 (§8)
8 #1821 Post-swap docs, labels, boards, integration hygiene (§10)
9 #1822 v1 → v2 migration guide (§10, pairs with #1803)

This umbrella issue stays open until all nine are Done.

Ground truth (re-verified 2026-07-28, after phases 1–3)

Thing Current state
npm @modelcontextprotocol/inspector latest = 1.0.1, v1-latest = 1.0.1
npm sub-packages (-client, -server, -cli) all at latest = 1.0.1, v1-latest = 1.0.1
npm deprecation all four names deprecated at @<2.0.0
origin/main root package.json unchanged — version 1.0.0, engines.node >=22.7.5, publish-all still has no --tag
v1/main root package.json version 1.0.1; publish-all pinned --tag v1-latest
v2/main root package.json version 2.0.0 already, engines.node >=22.19.0
v1/main branch exists, tag 1.0.1 at tip, ruleset-protected, CI green
Rulesets v1/main - full protection (branch) + v1 release tags (tag, refs/tags/1.*, deletion+update) — both active
v1 workflow triggers (v1/main) push: branches: [main, v1/main] + bare pull_request: + release: [published]
v1 workflow triggers (main) unchangedpush: branches: [main] + pull_request: + release: [published]
v2 workflow triggers push: (all branches) + release: [published]; publish job asserts release tag == root version, runs pack:verify, then npm publish --access public --provenance
Original ground truth, as verified 2026-07-27 (the state this plan was written against)
Thing State at planning time
npm @modelcontextprotocol/inspector latest = 1.0.0; no other dist-tags
origin/main root package.json version 1.0.0, tag 1.0.0 at branch tip, engines.node >=22.7.5
v2/main root package.json version 2.0.0 already, engines.node >=22.19.0
v1 publish publish-all = npm publish --workspaces --access public && npm publish --access publicno --tag
v1 also publishes 3 sub-packages inspector-client, inspector-server, inspector-cli, all at latest = 1.0.0
v1 workflow triggers push: branches: [main] + release: [published]
Tag rulesets none existed
main branch protection via rulesets targeting ~DEFAULT_BRANCH — so v1/main inherited nothing

Both branches' workflows live at .github/workflows/main.yml, and the release's target commit selects which one runs — that's the mechanism the whole plan leans on.

Proven since: trusted publishing works from a non-default branch (the 1.0.1 publish from v1/main, §4). npm matched on repo + workflow filename + release environment, with no branch or ref pinning — so the workflow_dispatch fallback below is not needed.

The plan as proposed

  1. Branch mainv1/main so the v1 line continues to exist and can conceivably take security fixes.
  2. On v1/main, merge a PR adding a prominent deprecated notice (README + runtime banner) plus the CI-trigger fix (§2), then publish 1.0.1 from v1/main — which doubles as the trusted-publishing rehearsal (§4). Apply the npm-side deprecation handles (§3) after it's live.
  3. PR against main that replaces the entire tree with v2.
  4. Cut release 2.0.0 and publish — after a re-evaluation of the v2 publishing pipeline for confidence.
  5. Close all open issues and PRs against v1 (anything not labeled v2) with a note that v1 is deprecated and takes only required security fixes.
  6. Close off external PRs so only contributors with write access can open them. External contributors file issues; we triage, label the ones with merit, and implement them ourselves to hold the quality gates and architectural integrity.
  7. Update the publishing pipeline so v1 updates can be published from v1/main, avoiding a future emergency branch-swap to ship a v1 security fix.

Opinion — extra steps, and four things that will bite

§1–§4 are blockers or near-blockers for step 7 specifically; §5 onward are sequencing and hygiene.

🔴 1. A v1 security release will silently steal the latest tag

This is the most important finding, and it makes step 7 mandatory, not optional. publish-all has no --tag, so npm assigns latest to whatever it publishes. Once 2.0.0 is out, publishing a 1.0.2 security fix from v1/main would move latest back to 1.0.2 — every npx @modelcontextprotocol/inspector in the world silently reverts to deprecated v1.

Fixes, all needed:

  • Change v1/main's publish-all to publish under --tag v1-latest (both the root and the three workspace packages). ⚠️ Not v1 — npm rejects any dist-tag that parses as a SemVer range, and v1 parses as 1.x (npm error Tag name must not be a valid SemVer range: v1). Using v1 does not publish to the wrong tag; it fails the publish outright, at release time. Corrected in fix: v1 dist-tag must be v1-latest — npm rejects v1 #1829.
  • Order the go-live publishes so latest lands correctly: publish 1.0.1 first (while latest is still the v1 line), then 2.0.0. If 1.0.1 slips to after 2.0.0, it must go out as --tag v1-latest.
  • After 2.0.0 is live, add the escape hatch: npm dist-tag add @modelcontextprotocol/inspector@1.0.1 v1-latest, so npm i @modelcontextprotocol/inspector@v1-latest keeps working and the deprecation message has somewhere to point. Applied to all four names, not just the root — the sub-packages' latest is equally stranded, so @v1-latest should resolve consistently whichever name someone installed.

🔴 2. v1/main gets no CI at all, so step 7 is half-done

v1's workflow triggers on push: branches: [main]. The moment v1/main exists, pushes to it match nothing — no build, no tests, no gate on the branch we're reserving for security fixes. Step 7 must also add v1/main to that trigger (and to the pull_request gate), otherwise a security fix would be published from an unvalidated branch.

Whether v1 can be published from v1/main at all under trusted publishing is a separate and more serious question — see §4.

🔴 3. The three v1 sub-packages need deprecating too

v1 publishes inspector-client, inspector-server, and inspector-cli alongside the root package. v2 publishes only the root package as a single tarball — so those three become permanently orphaned at 1.0.0 with a live latest tag. Step 2's npm deprecation must cover all four names, or people keep installing sub-packages that will never be updated again.

On the npm deprecation mechanics: use a version range, not the whole package —

npm deprecate "@modelcontextprotocol/inspector@<2.0.0" "v1 is deprecated; upgrade to v2 (npm i @modelcontextprotocol/inspector@latest). v1 receives security fixes only, published under the 'v1' tag."

npm deprecate pkg@"*" would warn on v2 installs too. For the three sub-packages the range is <2.0.0 as well. ⚠️ An earlier draft said <=1.0.0, written when 1.0.0 was their newest version — phase 2 published 1.0.1 to all three, so <=1.0.0 would leave the version latest resolves to (the one everyone installs) un-deprecated. <2.0.0 is safe for them since no 2.x will ever exist: v2 ships only the root package. Note deprecation is retroactive-but-mutable — it can be lifted with an empty message, so it's the one reversible step here.

🟡 4. Can v1 publish from v1/main at all, given trusted publishing? — probably yes, but prove it

The concern raised: we use npm trusted publishing (OIDC), and the belief is that a publish must come from main.yml on the default branch. If that were true, step 7 would be impossible as written and a v1 security fix really would require swapping v1/main into main — the exact scenario step 7 exists to prevent.

Per npm's documentation, the trusted publisher config does not pin a branch or ref. The fields are:

Field Required Our value
Organization or user modelcontextprotocol
Repository inspector
Workflow filename (filename only, not a path) main.yml
Environment name optional release
Allowed actions npm publish

There is no branch or ref field — authorization is repo + workflow filename (+ environment), and npm's own documented example is triggered from a tag push rather than a branch. So a release cut from a v1/main commit, running v1/main's .github/workflows/main.yml under environment: release, should satisfy the same trusted publisher, because both branches use the identical workflow filename main.yml. That coincidence is what makes step 7 viable at all.

Two consequences worth pulling out:

  • Verify this empirically — do not ship on my reading of the docs. npm explicitly does not validate a trusted publisher config when you save it, and every field is a case-sensitive exact match, so a mismatch surfaces only as a failed publish. The 1.0.1 publish is the rehearsal — cut it from v1/main (plan step 2) rather than burning a throwaway 1.0.1-rc.0. Same evidence, one fewer publish; see the rehearsal notes below.
  • It cuts the other way, security-wise. Because npm matches on filename alone, v1/main's main.yml is already authorized to publish the root package name — including to latest. Anyone who can push to v1/main can publish the v2 package. That's a concrete argument for protecting v1/main with the same rules as main (§10), not leaving it as an unguarded parking branch.

Fallback if the rehearsal fails — a dispatchable publish job on the default branch. This satisfies the "must run from main.yml on the default branch" constraint literally, and is worth considering even if the rehearsal passes, since it centralizes the release button on one branch:

# on main (the default branch), in main.yml
on:
  workflow_dispatch:
    inputs:
      ref:     { description: 'Ref to build and publish', default: 'v1/main' }
      npm_tag: { description: 'dist-tag',                  default: 'v1-latest' }

…with the job checking out inputs.ref, building it, and running npm publish --tag ${{ inputs.npm_tag }}. The OIDC identity then comes from main.yml on main — unambiguously matching the trusted publisher — while the published code comes from v1/main.

Keep the publish job inline in main.yml: npm validates the calling workflow's filename, so factoring it out into a reusable workflow_call workflow breaks OIDC (and fails looking like a config mismatch).

One caveat, only if we take this fallback route: --provenance builds its attestation from the running workflow's OIDC claims (which reference main), while the packed tree came from a different ref. Confirm npm accepts that rather than rejecting it as a source mismatch. If it doesn't, decide consciously whether a v1 security patch ships without provenance (acceptable) or via the branch-swap of last resort (much worse) — and write that decision down. This does not apply to the 1.0.1 rehearsal: there the workflow runs from the same ref it publishes.

Rehearsing via the real 1.0.1 publish

Verified against origin/main's workflow: v1 already uses OIDC (id-token: write, environment: release, no NPM_TOKEN) with NPM_CONFIG_PROVENANCE: "true", and publish is if: github.event_name == 'release' with needs: buildbuild has no branch condition, so a release cut from a v1/main commit runs the whole workflow and reaches publish. Five things to get right:

  • Settings → Environments → release — verified 2026-07-27: Deployment branches and tags is "No restriction". So a 1.0.1 tag on v1/main reaches the publish job; this is not a blocker. (If it had been restricted to selected refs, the job would have been blocked before npm was ever contacted, failing in a way that looks nothing like an OIDC rejection.) If required reviewers are configured, the run pauses for approval rather than failing — we have someone with approval rights, so that's a pause, not a risk. Note the flip side: with no restriction, v1/main's main.yml can publish the root package name to latest from the moment the branch exists, which is why §10's branch protection on v1/main is a requirement rather than hygiene.
  • Do not add --tag v1-latest yet. 1.0.1 must take latest, which is the whole point of shipping the deprecation notice to npx users. Commit the --tag v1-latest change to publish-all immediately after the release, as its own commit — that gap is exactly where a forgotten flag later steals latest back from 2.0.0 (§1).
  • Bump all four package.jsons to 1.0.1, or the check-version step fails the build.
  • Partial-publish hazard: publish-all is npm publish --workspaces && npm publish. If the workspaces succeed and the root fails, 1.0.1 lands for the three sub-packages with the root still at 1.0.0. Recoverable (bump to 1.0.2, go again), not clean.
  • What this doesn't prove: the --tag v1-latest publish path, and a publish in the post-2.0.0 world where latest is genuinely at risk. The first real v1 security fix is still the first true exercise of that.

Also confirm npm ≥ 11.5.1 is in use (v1's workflow already has a step for this, since Node 22 bundles npm 10.x).

Sources: npm trusted publishers · npm trusted publishing GA changelog

5. How to actually do the tree replacement (step 3)

main and v2/main have diverged enormously; a plain git merge is an unresolvable conflict field. I'd recommend a true merge that takes v2's tree wholesale, not a reset:

git checkout -b chore/v2-golive main
git merge --no-commit -s ours v2/main     # record v2/main as a parent, keep no tree yet
git rm -rf . && git checkout v2/main -- . # tree becomes exactly v2/main's
git commit

This preserves main's history (no force-push to the default branch, PR review trail intact) and makes v2/main a merge parent — so main is a true superset and v2/main can afterwards be fast-forwarded to main or retired cleanly. A git reset --hard v2/main + force-push would look simpler but discards main's history and requires disabling branch protection on the default branch. Verify after merge: git diff main v2/main is empty.

6. Re-validating the publishing pipeline before 2.0.0 (step 4)

The strongest available confidence check, and it's already built: npm run pack:verify builds, packs, installs the real tarball into a clean throwaway consumer, and drives the installed bin (--help, a real --cli tools/list over stdio, a prod --web boot from the shipped dist). The publish job already runs it as a gate.

Beyond that, I'd add one thing the repo can't self-test: publish a 2.0.0-rc.1 under --tag next to the real registry and install it via npx @modelcontextprotocol/inspector@next on a clean machine. That is the only way to validate the parts that only exist against live npm — provenance/OIDC minting, the files allowlist as npm actually packs it, the postinstall cascade's early-exit under a real dependency install, and the Docker job. A wasted prerelease version number is cheap next to a broken 2.0.0.

Also confirm before cutting: the release tag is v2.0.0 or 2.0.0 (the assert tolerates the leading v) targeting the post-merge main commit, and the Docker latest tag moves to 2.0.0 while a 1-pinned tag is preserved for v1 users.

7. Rollback plan (there needs to be one in writing)

npm unpublish is effectively unavailable (72h window, and disallowed once others depend on it). So the rollback for a broken 2.0.0 is dist-tag surgery, not unpublish: npm dist-tag add @modelcontextprotocol/inspector@1.0.1 latest, then npm deprecate the bad 2.0.0, then ship 2.0.1. Write this down before publishing, and make sure whoever cuts the release has the npm rights to do it.

8. Bulk-closing v1 issues and PRs (step 5) — do it auditably

  • Script it, but add a label (e.g. closed-v1-deprecated) rather than only a comment, so the set stays findable later. A comment alone is very hard to query.
  • Don't blanket-close by "not labeled v2" without a review pass: some open v1 issues may describe real bugs that still exist in v2, or security items we actually want. Triage into port to v2 (relabel v2, add to board Add tab and approval flow for server -> client sampling #28) vs. close as deprecated before running the script.
  • Expect GitHub secondary rate limits on a few hundred closures — throttle, don't hammer.
  • Consider whether to lock those threads. I'd say no — leave comments open so someone can say "this still reproduces in v2."

9. Restricting PRs to write-access contributors (step 6)

Settings → Features → Pull requests → "Collaborators only". The PR tab stays visible and anyone can read and comment, but only users with write access can open new ones. Issues are unaffected — exactly the model step 6 describes. One click, reversible.

Before relying on it, confirm what "collaborator" resolves to here — it means write/maintain/admin on the repo, which may include org-wide base write, not just the maintainer team:

gh api repos/modelcontextprotocol/inspector/collaborators --jq '.[] | select(.permissions.push) | .login'

The setting gives the contributor no explanation — they just find no "Create pull request" button. So CONTRIBUTORS.md (issue #1517) and issue templates still need to land, and land first.

Docs · changelog

Also decide the security-report path explicitly while we're here: SECURITY.md should carry a supported-versions table (v2 = supported, v1 = security fixes only, everything below 1.0.0 = unsupported) and private reporting should be enabled.

10. Things easy to forget in the swap

  • Flip the PR-access setting (§9) to Collaborators only, after the Add CONTRIBUTORS.md: issues-only policy (share prompts, not PRs) #1517 docs land. Manual, one click, reversible.
  • Branch protection / rulesets on the new v1/main (protect it, or the "it can take security fixes" story is fiction), and tag protection for 1.x tags.
  • Closes #N starts working. Per AGENTS.md, closing keywords only auto-close on the default branch — v2 PRs targeting v2/main never auto-closed. After the swap, PRs into main do auto-close their issues. Update AGENTS.md, and stop the manual close-and-move-to-Done ritual.
  • Docs churn: AGENTS.md and every README reference v2/main as the base branch and describe main as legacy. All of that inverts. The board conventions section needs a full pass.
  • Labels and boards: once everything is v2, the v2 label is noise — decide whether to retire it or invert to a v1 label. Archive project get tools working #11; Add tab and approval flow for server -> client sampling #28 becomes the only board.
  • Dependabot / CodeQL / any workflow or external integration pinned to a branch name.
  • External permalinks: links into blob/main/... will silently resolve to the v2 tree. Anything that must keep pointing at v1 should be re-pinned to the 1.0.1 tag or v1/main.
  • Node engine bump (>=22.7.5 → >=22.19.0) is a real, if mild, breaking change for 2.0.0 release notes.
  • A v1 → v2 migration guide is the highest-value doc we could ship alongside: CLI flags changed substantially, and --config semantics are genuinely different in v2 (read-only session file, with --catalog as the writable one). Without this, the deprecation notice sends people somewhere confusing. Pairs with the docs-site rewrite in docs: new modelcontextprotocol.io Inspector documentation for v2 (legacy vs. modern era, more screenshots) #1803.
  • Sequencing / freeze: freeze merges to v2/main during the swap, and pick a low-traffic window. Create v1/main → deprecation notice + CI-trigger fix on it → publish 1.0.1 from v1/main (the rehearsal) → add --tag v1-latest to publish-all → merge v2 tree to main → verify CI green on main → publish 2.0.0 → then the cleanup steps (5, 6, 7), which are all reversible.
  • Announcement: GitHub release notes, the MCP community channels, and the docs site landing together.

Deliverable

An agreed, written go-live runbook (ordered, with the irreversible steps flagged and a rollback line for each), broken into per-phase tracked issues on board #28.

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions