Repository navigation
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
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;
| 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) |
unchanged — push: 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 public — no --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
- Branch
main→v1/mainso the v1 line continues to exist and can conceivably take security fixes. - 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 fromv1/main— which doubles as the trusted-publishing rehearsal (§4). Apply the npm-side deprecation handles (§3) after it's live. - PR against
mainthat replaces the entire tree with v2. - Cut release 2.0.0 and publish — after a re-evaluation of the v2 publishing pipeline for confidence.
- 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. - 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.
- 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'spublish-allto publish under--tag v1-latest(both the root and the three workspace packages).⚠️ Notv1— npm rejects any dist-tag that parses as a SemVer range, andv1parses as1.x(npm error Tag name must not be a valid SemVer range: v1). Usingv1does not publish to the wrong tag; it fails the publish outright, at release time. Corrected in fix: v1 dist-tag must bev1-latest— npm rejectsv1#1829. - Order the go-live publishes so
latestlands correctly: publish 1.0.1 first (whilelatestis 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, sonpm i @modelcontextprotocol/inspector@v1-latestkeeps working and the deprecation message has somewhere to point. Applied to all four names, not just the root — the sub-packages'latestis equally stranded, so@v1-latestshould 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. <=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 throwaway1.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'smain.ymlis already authorized to publish the root package name — including tolatest. Anyone who can push tov1/maincan publish the v2 package. That's a concrete argument for protectingv1/mainwith the same rules asmain(§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: build — build 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 a1.0.1tag onv1/mainreaches 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'smain.ymlcan publish the root package name tolatestfrom the moment the branch exists, which is why §10's branch protection onv1/mainis a requirement rather than hygiene.- Do not add
--tag v1-latestyet. 1.0.1 must takelatest, which is the whole point of shipping the deprecation notice tonpxusers. Commit the--tag v1-latestchange topublish-allimmediately after the release, as its own commit — that gap is exactly where a forgotten flag later stealslatestback from 2.0.0 (§1). - Bump all four
package.jsons to 1.0.1, or thecheck-versionstep fails the build. - Partial-publish hazard:
publish-allisnpm 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-latestpublish path, and a publish in the post-2.0.0 world wherelatestis 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 commitThis 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 (relabelv2, 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.
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 for1.xtags. Closes #Nstarts working. Per AGENTS.md, closing keywords only auto-close on the default branch — v2 PRs targetingv2/mainnever auto-closed. After the swap, PRs intomaindo 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/mainas the base branch and describemainas legacy. All of that inverts. The board conventions section needs a full pass. - Labels and boards: once everything is v2, the
v2label is noise — decide whether to retire it or invert to av1label. 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 the1.0.1tag orv1/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
--configsemantics are genuinely different in v2 (read-only session file, with--catalogas 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/mainduring the swap, and pick a low-traffic window. Createv1/main→ deprecation notice + CI-trigger fix on it → publish 1.0.1 fromv1/main(the rehearsal) → add--tag v1-latesttopublish-all→ merge v2 tree tomain→ verify CI green onmain→ 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.
Activity
BobDickinson commentedon Jul 27, 2026
This all makes sense to me.
One note: if we switch steps 1 and 2, then we could validate publishing from the v1/main branch early, and if if fails, easily get back to where we started (just delete the v1/main branch).
docs: refresh V2 board Status option IDs in AGENTS.md (#1823)
docs: refresh V2 board Status option IDs in AGENTS.md (#1823) (#1825)
cliffhall commentedon Jul 28, 2026
State of the runbook: Phase 1 complete
Phase 1 (#1824) is done — all four steps executed and verified. Nothing published, nothing irreversible. Phase 2 (#1815⚠️ ) is next and is the first irreversible one.
What now exists
v1/mainbranch3188ee3a= tag1.0.0(ac3c1a12) + one commit,add v1/main to branches in main.ymlv1/mainpush:trigger now includesv1/main;pull_request:was already unfiltered (verified, not edited)19858989v1/main - full protection— active,refs/heads/v1/main19859230v1 release tags— active,refs/tags/1.*, rulesdeletion+updateThe branch ruleset was cloned from
3187937and diffed against it after creation:rulesandbypass_actorsare byte-identical, including thebuildcheck'sintegration_id: 15368and the fullpull_requestparameter block. Sov1/mainis protected exactly asmainis — no transcription drift.Rules confirmed to actually resolve on the branch (not merely to look right in the ruleset body):
Step 2 was merged before the ruleset went on, per the runbook, so
buildhas registered on the branch and the first protected PR can't deadlock on a check that has never run.Three things worth carrying into phase 2
1. The §10 protection requirement is satisfied, but admins still bypass it. The cloned
bypass_actorsincludeRepositoryRole 5(admin) atbypass_mode: always. So §4's "anyone who can push tov1/maincan publish the v2 package name" still holds for every repo admin — the ruleset closes the unguarded parking branch hole, not the admin hole. That matchesmain's posture and is what "clone the existing ruleset" asked for; flagging it so nobody reads phase 1 as having closed it.A practical consequence: do not verify protection by attempting a direct push. As an admin it succeeds, proves nothing, and writes to
v1/main— andnon_fast_forwardthen blocks a clean undo. #1824's checklist has been corrected to use the API check above instead.2. The tag ruleset has no bypass actors — deliberately stricter than the branch. Nobody, admins included, can move or delete a
1.xtag without first disabling the ruleset.creationis absent, so phase 2 can still cut1.0.1normally. But if phase 2 needs to retag — e.g. the release is cut against the wrong commit — that will be blocked, and the fix is to disable19859230, retag, re-enable. Worth knowing before you're mid-release wondering why the tag won't budge.3. There is no
/rules/tags/API. GitHub exposes/rules/branches/{branch}only; the tag equivalent 404s. Tag rulesets can be verified only by reading the ruleset back:Ground truth unchanged
Nothing in the §"Ground truth" table has moved: npm
latestis still 1.0.0, no other dist-tags exist, andmainis untouched at 1.0.0. Thereleaseenvironment is still No restriction, so a1.0.1tag onv1/mainwill reach the publish job — which is what makes phase 2 a real OIDC rehearsal rather than a blocked run.Next
Phase 2 (#1815)⚠️ — the first irreversible phase. The runbook's own cautions that still apply verbatim: bump all four
package.jsons to 1.0.1 orcheck-versionfails; do not add--tag v1yet (1.0.1 must takelatest); and watch the partial-publish hazard inpublish-all(workspaces succeed, root fails → recover by bumping to 1.0.2).#1824 stays open until someone signs off on the above; say the word and I'll close it and move the card to Done.
chore: v1 deprecation notices in all four packages, bump to 1.0.1
chore: v1 deprecation notices in all four packages, bump to 1.0.1 (#1827
chore: pin v1 publishes to the `v1` dist-tag (#1828)
fix: v1 dist-tag must be `v1-latest` — npm rejects `v1` (#1829)
cliffhall commentedon Jul 28, 2026
State of the runbook: Phases 2 and 3 complete
3 of 9 done — #1824, #1815, #1816 all closed, cards on Done. Next is #1817 (replace
main's tree with v2).Where things stand
latestv1-latest@<2.0.0v1/main47f92841, ruleset-protected, CI greenpublish-allonv1/main--tag v1-latestmainPhase 2 (#1815) — the OIDC rehearsal passed
Trusted publishing works from a non-default branch. npm matched on repo + workflow filename +
releaseenvironment with no branch or ref pinning, exactly as §4 predicted from the docs — now evidence rather than a reading. Thereleaseenvironment let it through with no approval pause, and--provenancewas fine (workflow ref and published tree are the same commit).So the
workflow_dispatchfallback in §4 is not needed. Step 7 of the plan is viable as written.1.0.1 published cleanly — all four packages, no partial publish. Verified from the published tarball, not just registry metadata: the deprecation banner is present in the shipped
cli/build/cli.js.One scope change worth recording: §2 said "README + runtime banner", which understated it. v1 publishes four separately installable names, so a root-README-only notice is invisible to anyone who installed a sub-package — and
cli/andserver/ship no README at all, so npm renders a blank page for both. For those two, the runtime banner and the npm deprecation are the only notice their users ever get. All four now carry one, plus a dismissible in-app banner in the client.Phase 3 (#1816) — two runbook errors found
Both would have surfaced later and worse.
1.
v1is not a legal npm dist-tag. npm rejects any tag that parses as a SemVer range, andv1parses as1.x:§1 specifies
v1in two places. Beyond failingnpm dist-tag add, the--tag v1merged in #1828 would have made the next v1 security release fail to publish outright — at release time, mid-incident, which is the worst possible moment to discover it. The tag is nowv1-latest(#1829), and the workflow comment records both failure modes so neither the flag nor the specific name gets "simplified" later.2. The
<=1.0.0sub-package range was stale. It was written when 1.0.0 was their newest version. Phase 2 published 1.0.1 to all three, so<=1.0.0would have left the versionlatestresolves to — the one everyone actually installs — un-deprecated, making three orphaned packages look healthy. Now<2.0.0for all four, which is safe since no 2.x will ever exist for the sub-packages.Operational note:
npm deprecatehad to be run from a script file, not a pasted command. A long pasted line gets hard-wrapped by the terminal, the newline is stored verbatim in the registry, and npm truncates its warning at the break — hiding the upgrade pointer. It took two attempts to spot; the tell was the break position moving with message length. Anyone re-running it should build the string in a file.Carried forward
--admin, becauserequire_code_owner_review: trueis set onv1/mainwith no CODEOWNERS file on the branch — a required check nothing can satisfy. That is worse than no check: it looks like protection while forcing every merge through bypass, which normalizes bypassing. Worth resolving before go-live 4/9: replace main's tree with v2 #1817 puts the same ruleset shape in front ofmain.v1tag name and<=1.0.0range. go-live 3/9: lock the v1 dist-tag and deprecate all four packages on npm #1816's body has been corrected; this umbrella's text has not.--tag v1-latestpath — phase 3 verified the flag is present and the tag name is legal, not that a publish through it succeeds.chore: replace main's tree with v2
cliffhall commentedon Jul 28, 2026
State of the runbook: Phase 4 complete
4 of 9 done.⚠️ .
mainis now the v2 tree. Next is #1818 (publish 2.0.0)mainhead84456758(swap merge25106dcc+ two follow-ups)mainversionmainmainlatestThe swap (#1830)
Landed as a merge commit with two parents (
ac3c1a12= oldmain,4d30d1cd=v2/main), not a squash. That was load-bearing: squashing would have dropped the second parent and made 2381 v2 commits unreachable frommain.git diff HEAD origin/v2/mainwas empty before commit — the tree is byte-identical tov2/main, 1216 files.Verified after merge: old
mainis still an ancestor, all ofv2/mainis reachable, root version reads 2.0.0, and CI onmainpassed on the first run against the swapped tree.Three corrections to the plan
1. §5's merge command fails as written.
mainandv2/mainhave genuinely unrelated histories — different root commits (996f02c7vs3b583e1f), no merge base at all.--allow-unrelated-historiesis required. This follows from something AGENTS.md already notes (v1.5 was never an ancestor of v2/main), but §5 did not account for it.The flag is only a guardrail — the resulting commit is identical either way. What is permanent is the fact it surfaces:
mainnow has three root commits and two lineages that never touch. Practical consequences worth knowing:git log --followandgit blamewill not cross the boundary (v1'sclient/src/App.tsxand v2'sclients/web/src/App.tsxare unrelated files to git), and a plaingit log maininterleaves two timelines by date — use--first-parentto walk the oldmainline cleanly.2. The open-PR backlog was 125, not ~30.
gh pr listsilently caps at 30, which badly understated it. All 125 were re-pointed atv1/mainbefore the swap rather than left stranded against a tree that no longer exists —v1/mainstrictly descends frommain, so every merge base and diff is preserved (spot-checked #1732, #1696, #1519: all stillMERGEABLE). Triage remains #1819's job; this only preserved the option, and matters for that phase's rate-limit planning.3.
Closes #Nnow auto-closes. #1817 closed itself on merge — first time that has worked, since PRs intomainfinally target the default branch. The manual close-and-move-to-Done ritual can stop (#1821).Security findings surfaced by the swap
mainhad never scanned the v2 tree, so CodeQL reported 11 alerts at once. None were introduced by the swap — all pre-existing onv2/main.test-servers/src/*andclients/web/src/test/*. Confirmed none are in the publishedfilesallowlist, so they never ship. Includes the 1 critical (js/request-forgeryin the OAuth test server) and 9 high.actions/missing-workflow-permissionson.github/workflows/main.yml. Medium by rating, but it is the workflow that publishes to npm under OIDC — an over-permissionedGITHUB_TOKENin a job that mints publish credentials was worth closing before 2.0.0. Added a workflow-levelpermissions: contents: read; both publish jobs keep their own blocks, which override rather than merge — recorded in a comment, since a future edit trimmingcontents: readfrom a publish job "because it is inherited" would break publishing.Dependabot — assessed, not a blocker
72 open alerts (3 critical / 36 high / 26 medium / 7 low). None gate 2.0.0: the release ships the same dependency tree
v2/mainhas always had. All 3 criticals are@vitest/browser, development scope, never shipped.One was worth real investigation — GHSA-frvp-7c67-39w9, path traversal in
@hono/node-server'sserve-static, because the Inspector calls that exact function in its prod server (serveStaticmounted at/*)...escapes, so it only reaches files inside the configured root, by slipping past prefix-mounted middleware. Hererootis the built SPA (public by design) and the only prefix middleware (/api/*) is a separate route, not a subdirectory of it — so there is no auth bypass and no arbitrary read, and it is Windows-only regardless.Upgraded anyway as hygiene (#1832), since it proved free: 1.x → 2.x drops Node 18 (repo needs ≥22.19.0) and removes the unused Vercel adapter;
serve/serveStatic/ServerType— the entire import surface here — are unchanged.npm run validategreen on all four clients.The rest are transitive; letting Dependabot's own PRs land after the release is the sane path.
Carried forward
main. chore: replace main's tree with v2 #1830, ci: set a least-privilege default GITHUB_TOKEN scope #1831 and chore(deps): upgrade @hono/node-server to 2.0.12 #1832 all merged with--admin, becauserequire_code_owner_review: trueis configured with no CODEOWNERS file — a required check nothing can satisfy. It now guards the default branch, where the next action is publishing 2.0.0. Worth resolving before go-live 5/9: publish 2.0.0 (rc under next first; rollback plan written first) #1818 rather than after.v2/mainis now redundant —mainis a strict superset. Anything merged there from here on will not be onmain. Retiring or fast-forwarding it belongs to go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821.blob/main/...permalinks into the v1 tree now silently 404 (§10 anticipated this). Anything that must keep pointing at v1 should be re-pinned to the1.0.1tag orv1/main.chore: 2.0.0 (#1842)
cliffhall commentedon Jul 28, 2026
State of the runbook: Phase 5 complete — v2 is live
5 of 9 done. The irreversible half is finished; everything remaining is reversible cleanup.
npx @modelcontextprotocol/inspectornow resolves to v2 worldwide.latestv1-latestlatest2.0.0, multi-arch1.0.1main7aebf168, v2 tree, 2.0.0v1/main47f92841, protected, publishes underv1-latestThe RC process was not ceremony
The v2 publish job had never executed — every prior release was cut from the v1 workflow. So every defect below was sitting there waiting for whoever cut the first v2 release, and two would have failed 2.0.0 outright:
ENEEDAUTH—NODE_AUTH_TOKENpointed at aNPM_TOKENsecret that does not exist (publishing moved to OIDC trusted publishing).setup-nodestill wrote an empty_authTokeninto its.npmrc, so npm failed before OIDC was ever attempted. (fix(ci): publish via OIDC trusted publishing, not a non-existent NPM_TOKEN #1836)--tagon publish —npm publishdefaults tolatestregardless of semver prerelease status, so §6's own instruction to publish2.0.0-rc.1would have pointedlatestat a release candidate. Caught while preparing the RC; the RCs then proved the fix. (ci: derive the npm dist-tag from the version instead of defaulting to latest #1834)Each RC was validated by installing the published tarball into a cold
node:22container and driving it end to end — install,--help, and a real--cli tools/listagainst a live stdio server.lateststayed on 1.0.1 through all three, which is what proved the tag derivation worked.Three RCs, because the tree kept changing
The discipline held throughout: the tree that ships is the tree that was validated. Any dependency change invalidated the previous RC.
package.jsontouched, no ranges widened. They were all stale pins inside existing caret ranges.clients/webat 8.0.0;npm updatereported "up to date" and only an explicit install moved it. That left three high advisories —server.fs.denybypass and arbitrary file read via the dev server. Worth fixing rather than dismissing: unlike the other dev-scope alerts, those are remotely triggerable against anyone runningnpm run dev. Also raised the declared floor to^8.1.5, since root's^8.0.0still permitted the vulnerable version on a fresh resolve.Corrections to this plan, found in phase 5
latest. Fixed in ci: derive the npm dist-tag from the version instead of defaulting to latest #1834; §6 should be read as depending on that.1-pinned Docker tag §6 asks to confirm does not exist, and never did. The workflow tags by exact version only. Closed as satisfied by1.0.1, which is a working pin; a floating1would need adding tov1/main's workflow, not a one-off push, and adds an ongoing obligation to a deprecated line.Deferred, tracked — not silently dropped
eslint-plugin-react-hooks@7.0.1does not accept eslint 10, proven by Dependabot's own failed attempt (build(deps): bump the npm_and_yarn group across 5 directories with 2 updates #1840, closed).npm ifails ERESOLVE.Both dev-scope, neither shipped. Alerts dismissed with per-alert reasoning —
tolerable_riskfor vitest,not_usedfor esbuild (its dev-server mode is never invoked here; esbuild is reached only as a bundler via tsup and vite). The eslint cluster needed no manual action: GitHub auto-dismissed it, and #1838 records whynpm auditstill reports it.Carried forward
--adminbypass. The rulesets require an approving review the project cannot supply — GitHub forbids self-approval, sorequired_approving_review_count: 1is unsatisfiable for a single maintainer regardless of CODEOWNERS. Now guarding the default branch. Recorded on go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821 with three options; deliberately not changed mid-release.v2/mainis now redundant —mainis a strict superset. Anything merged there will not be onmain. Retiring it belongs to go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821.gh pr listcaps at 30 by default. All were retargeted tov1/mainbefore the tree swap so their diffs survive. §8's secondary-rate-limit warning definitely applies at that size.Remaining — all reversible
CONTRIBUTORS.md,SECURITY.mdcliffhall commentedon Jul 28, 2026
State of the runbook: Phase 6 complete — contribution model closed
6 of 9 done. Both irreversible-in-practice steps that have run (2.0.0 publish, contribution-model change) are behind us. One⚠️ phase remains.
Live state
v1/mainmain#1820 (restrict PRs) now runs before #1819 (triage the backlog). Closing the door first means the 125-PR triage happens against a set that cannot grow while it is being worked. Triaging first would have meant new external PRs arriving into a backlog being closed.
Issue titles, bodies, and the phase table above have been renumbered to match, each with a note recording the swap.
Phase 6 (#1820) — what it did
PRs → Collaborators only flipped. PR tab stays visible, anyone can read and comment, Issues unaffected.
CONTRIBUTORS.mdwas already onmain— it survived the tree swap.SECURITY.mdwas not, and that is the finding worth carrying: it existed on the pre-swapmain(ac3c1a12) and still exists onv1/main, but the v2 tree never had one — so go-live 4/9: replace main's tree with v2 #1817 silently dropped the security policy from the default branch. Nothing alerted on it; it surfaced only from checking this phase's prerequisites.The timing made it matter: the security-report route disappeared in the same window as the PR route being closed. Restored in docs: restore SECURITY.md, lost in the v2 tree swap #1843 with the supported-versions table §9 asks for, plus an explicit carve-out that the no-outside-PRs policy does not apply to security reports — without which the new model reads as "no way to reach us". Private vulnerability reporting verified enabled, so the advisory link is a working route.
§9 asked us to confirm what "collaborator" resolves to. It is 110 accounts with push access — all org-inherited, zero direct, 30 of them admins.
So the setting restricts PRs to everyone with org-wide base write on
modelcontextprotocol, not to the people who maintain the Inspector. It closes the public drive-by firehose, which is the goal, but the correct mental model is "org members only". Tightening further is an org-level base-permission change, out of scope here.Corrections to the plan found in this phase
CONTRIBUTORS.mdhad,SECURITY.mdhad silently been deleted four phases earlier.SECURITY.mdwas found by accident. Anything that existed on the v1 tree and not the v2 tree is gone frommainwith no signal. Worth a deliberate diff during go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821 rather than waiting for the next accident.Deferred work — filed, boarded, not implicit
eslint-plugin-react-hooksrejects eslint 10 (proven by Dependabot's own failed #1840)npm ifails ERESOLVECarried forward
--adminbypass. The rulesets require an approving review the project structurally cannot supply (GitHub forbids self-approval, sorequired_approving_review_count: 1is unsatisfiable for a single maintainer regardless of CODEOWNERS). Options recorded on go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821.v2/mainis redundant —mainis a strict superset; anything merged there will not reachmain. Retirement belongs to go-live 8/9: post-swap docs, labels, boards, and integration hygiene #1821.maintov1/mainbefore the swap, so diffs and merge bases are intact and the port to v2 vs. close as deprecated triage §8 calls for is still possible.Remaining