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) 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

  1. Branch main → v1/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: 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 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.

Activity

  1. self-assigned this
    on Jul 27, 2026
  2. BobDickinson commented on Jul 27, 2026

    @BobDickinson
    Contributor

    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).

  3. cliffhall commented on Jul 28, 2026

    @cliffhall
    MemberAuthor

    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

    Thing State
    v1/main branch 3188ee3a = tag 1.0.0 (ac3c1a12) + one commit, add v1/main to branches in main.yml
    CI on v1/main push: trigger now includes v1/main; pull_request: was already unfiltered (verified, not edited)
    Branch ruleset 19858989 v1/main - full protection — active, refs/heads/v1/main
    Tag ruleset 19859230 v1 release tags — active, refs/tags/1.*, rules deletion + update

    The branch ruleset was cloned from 3187937 and diffed against it after creation: rules and bypass_actors are byte-identical, including the build check's integration_id: 15368 and the full pull_request parameter block. So v1/main is protected exactly as main is — no transcription drift.

    Rules confirmed to actually resolve on the branch (not merely to look right in the ruleset body):

    $ gh api 'repos/modelcontextprotocol/inspector/rules/branches/v1%2Fmain' --jq '.[].type'
    deletion
    non_fast_forward
    pull_request
    required_status_checks
    

    Step 2 was merged before the ruleset went on, per the runbook, so build has 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_actors include RepositoryRole 5 (admin) at bypass_mode: always. So §4's "anyone who can push to v1/main can publish the v2 package name" still holds for every repo admin — the ruleset closes the unguarded parking branch hole, not the admin hole. That matches main'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 — and non_fast_forward then 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.x tag without first disabling the ruleset. creation is absent, so phase 2 can still cut 1.0.1 normally. 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 disable 19859230, 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:

    gh api repos/modelcontextprotocol/inspector/rulesets/19859230 \
      --jq '{target, enforcement, include: .conditions.ref_name.include, rules: [.rules[].type]}'
    # → tag, active, ["refs/tags/1.*"], ["deletion","update"]

    Ground truth unchanged

    Nothing in the §"Ground truth" table has moved: npm latest is still 1.0.0, no other dist-tags exist, and main is untouched at 1.0.0. The release environment is still No restriction, so a 1.0.1 tag on v1/main will 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 or check-version fails; do not add --tag v1 yet (1.0.1 must take latest); and watch the partial-publish hazard in publish-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.

  4. cliffhall commented on Jul 28, 2026

    @cliffhall
    MemberAuthor

    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

    State
    npm latest 1.0.1 (was 1.0.0)
    npm v1-latest 1.0.1 — new escape hatch, all four packages
    Deprecated on npm all four names at @<2.0.0
    v1/main 47f92841, ruleset-protected, CI green
    publish-all on v1/main pinned --tag v1-latest
    main untouched, still v1 at 1.0.0

    Phase 2 (#1815) — the OIDC rehearsal passed

    Trusted publishing works from a non-default branch. npm matched on repo + workflow filename + release environment with no branch or ref pinning, exactly as §4 predicted from the docs — now evidence rather than a reading. The release environment let it through with no approval pause, and --provenance was fine (workflow ref and published tree are the same commit).

    So the workflow_dispatch fallback 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/ and server/ 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. v1 is not a legal npm dist-tag. npm rejects any 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
    

    §1 specifies v1 in two places. Beyond failing npm dist-tag add, the --tag v1 merged 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 now v1-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.0 sub-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.0 would have left the version latest resolves to — the one everyone actually installs — un-deprecated, making three orphaned packages look healthy. Now <2.0.0 for all four, which is safe since no 2.x will ever exist for the sub-packages.

    Operational note: npm deprecate had 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

    • The admin-bypass hole from phase 1 is still open, and now demonstrated. Both v1 PRs merged via --admin, because require_code_owner_review: true is set on v1/main with 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 of main.
    • §1 and §3 of this issue still contain the wrong v1 tag name and <=1.0.0 range. 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.
    • The first real v1 security release remains the first true exercise of the --tag v1-latest path — phase 3 verified the flag is present and the tag name is legal, not that a publish through it succeeds.
  5. added a commit that references this issue on Jul 28, 2026
  6. cliffhall commented on Jul 28, 2026

    @cliffhall
    MemberAuthor

    State of the runbook: Phase 4 complete

    4 of 9 done. main is now the v2 tree. Next is #1818 (publish 2.0.0) ⚠️.

    State
    main head 84456758 (swap merge 25106dcc + two follow-ups)
    main version 2.0.0
    History on main 2405 commits — both lineages intact
    Open PRs on main 0
    Open CodeQL alerts 0
    npm latest still 1.0.1 — nothing published this phase

    The swap (#1830)

    Landed as a merge commit with two parents (ac3c1a12 = old main, 4d30d1cd = v2/main), not a squash. That was load-bearing: squashing would have dropped the second parent and made 2381 v2 commits unreachable from main. git diff HEAD origin/v2/main was empty before commit — the tree is byte-identical to v2/main, 1216 files.

    Verified after merge: old main is still an ancestor, all of v2/main is reachable, root version reads 2.0.0, and CI on main passed on the first run against the swapped tree.

    Three corrections to the plan

    1. §5's merge command fails as written.

    fatal: refusing to merge unrelated histories
    

    main and v2/main have genuinely unrelated histories — different root commits (996f02c7 vs 3b583e1f), no merge base at all. --allow-unrelated-histories is 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: main now has three root commits and two lineages that never touch. Practical consequences worth knowing: git log --follow and git blame will not cross the boundary (v1's client/src/App.tsx and v2's clients/web/src/App.tsx are unrelated files to git), and a plain git log main interleaves two timelines by date — use --first-parent to walk the old main line cleanly.

    2. The open-PR backlog was 125, not ~30. gh pr list silently caps at 30, which badly understated it. All 125 were re-pointed at v1/main before the swap rather than left stranded against a tree that no longer exists — v1/main strictly descends from main, so every merge base and diff is preserved (spot-checked #1732, #1696, #1519: all still MERGEABLE). Triage remains #1819's job; this only preserved the option, and matters for that phase's rate-limit planning.

    3. Closes #N now auto-closes. #1817 closed itself on merge — first time that has worked, since PRs into main finally target the default branch. The manual close-and-move-to-Done ritual can stop (#1821).

    Security findings surfaced by the swap

    main had never scanned the v2 tree, so CodeQL reported 11 alerts at once. None were introduced by the swap — all pre-existing on v2/main.

    • 10 dismissed as used in tests: test-servers/src/* and clients/web/src/test/*. Confirmed none are in the published files allowlist, so they never ship. Includes the 1 critical (js/request-forgery in the OAuth test server) and 9 high.
    • 1 was not a test and was fixed (ci: set a least-privilege default GITHUB_TOKEN scope #1831): actions/missing-workflow-permissions on .github/workflows/main.yml. Medium by rating, but it is the workflow that publishes to npm under OIDC — an over-permissioned GITHUB_TOKEN in a job that mints publish credentials was worth closing before 2.0.0. Added a workflow-level permissions: contents: read; both publish jobs keep their own blocks, which override rather than merge — recorded in a comment, since a future edit trimming contents: read from 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/main has 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's serve-static, because the Inspector calls that exact function in its prod server (serveStatic mounted at /*).

    ⚠️ I initially assessed this as an unauthenticated arbitrary-file-read and recommended it block 2.0.0. That was wrong, and the correction matters: the advisory blocks .. escapes, so it only reaches files inside the configured root, by slipping past prefix-mounted middleware. Here root is 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 validate green on all four clients.

    The rest are transitive; letting Dependabot's own PRs land after the release is the sane path.

    Carried forward

  7. added a commit that references this issue on Jul 28, 2026
  8. cliffhall commented on Jul 28, 2026

    @cliffhall
    MemberAuthor

    State of the runbook: Phase 5 complete — v2 is live

    5 of 9 done. The irreversible half is finished; everything remaining is reversible cleanup.

    latest      2.0.0     ← was 1.0.1
    v1-latest   1.0.1
    next        2.0.0-rc.3
    

    npx @modelcontextprotocol/inspector now resolves to v2 worldwide.

    State
    npm latest 2.0.0
    npm v1-latest 1.0.1 (escape hatch intact)
    Docker latest digest identical to 2.0.0, multi-arch
    Docker 1.0.1 intact, own digest — v1 pin
    main 7aebf168, v2 tree, 2.0.0
    v1/main 47f92841, protected, publishes under v1-latest
    Open Dependabot alerts 0
    Open CodeQL alerts 0

    The 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:

    1. ENEEDAUTH — NODE_AUTH_TOKEN pointed at a NPM_TOKEN secret that does not exist (publishing moved to OIDC trusted publishing). setup-node still wrote an empty _authToken into its .npmrc, so npm failed before OIDC was ever attempted. (fix(ci): publish via OIDC trusted publishing, not a non-existent NPM_TOKEN #1836)
    2. No npm CLI upgrade — trusted publishing requires npm >= 11.5.1; Node 22 bundles 10.x. Would have failed even after fixing the above. The v1 workflow has this step; v2 was missing it. (fix(ci): publish via OIDC trusted publishing, not a non-existent NPM_TOKEN #1836)
    3. No --tag on publish — npm publish defaults to latest regardless of semver prerelease status, so §6's own instruction to publish 2.0.0-rc.1 would have pointed latest at 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:22 container and driving it end to end — install, --help, and a real --cli tools/list against a live stdio server. latest stayed 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.

    • rc.1 — caught defects 1 and 2 above.
    • rc.2 — validated the dependency sweep (chore: clear all runtime dependency vulnerabilities; 2.0.0-rc.2 #1837): every prod-scope advisory cleared using lockfile-only changes, no package.json touched, no ranges widened. They were all stale pins inside existing caret ranges.
    • rc.3 — validated the vite bump (fix(deps): raise vite to 8.1.5 to close three dev-server file-read advisories; 2.0.0-rc.3 #1841). The sweep had moved vite to 8.0.16 in root and tui but left clients/web at 8.0.0; npm update reported "up to date" and only an explicit install moved it. That left three high advisories — server.fs.deny bypass 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 running npm run dev. Also raised the declared floor to ^8.1.5, since root's ^8.0.0 still permitted the vulnerable version on a fresh resolve.

    Corrections to this plan, found in phase 5

    • §6's RC step was not achievable as written. There was no mechanism to pass a dist-tag through a release-triggered publish, and the default would have claimed 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.
    • The 1-pinned Docker tag §6 asks to confirm does not exist, and never did. The workflow tags by exact version only. Closed as satisfied by 1.0.1, which is a working pin; a floating 1 would need adding to v1/main's workflow, not a one-off push, and adds an ongoing obligation to a deprecated line.

    Deferred, tracked — not silently dropped

    Both dev-scope, neither shipped. Alerts dismissed with per-alert reasoning — tolerable_risk for vitest, not_used for 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 why npm audit still reports it.

    Carried forward

    Remaining — all reversible

    Phase Issue
    6 #1819 ⚠️ Triage + bulk-close the v1 backlog (125 PRs)
    7 #1820 Restrict PRs to collaborators; CONTRIBUTORS.md, SECURITY.md
    8 #1821 Post-swap docs, labels, boards, integration hygiene
    9 #1822 v1 → v2 migration guide
  9. cliffhall commented on Jul 28, 2026

    @cliffhall
    MemberAuthor

    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

    npm   latest      2.0.0
          v1-latest   1.0.1
          next        2.0.0-rc.3
    
    External pull requests closed — Collaborators only
    Open Dependabot alerts 0
    Open CodeQL alerts 0
    Open PRs on v1/main 125 (awaiting phase 7 triage)
    Open PRs on main 0

    ⚠️ Phases 6 and 7 were swapped

    #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.md was already on main — it survived the tree swap.

    • SECURITY.md was not, and that is the finding worth carrying: it existed on the pre-swap main (ac3c1a12) and still exists on v1/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.

    ⚠️ "Collaborators only" is broader than it sounds

    §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

    Deferred work — filed, boarded, not implicit

    Issue
    #1838 eslint 10 migration — blocked: eslint-plugin-react-hooks rejects eslint 10 (proven by Dependabot's own failed #1840)
    #1839 vitest/Storybook exact-pin knot — blocked: circular exact peers, npm i fails ERESOLVE
    #1844 Issue templates — split out of #1820; additive, since the policy docs are in place

    Carried forward

    Remaining

    Phase Issue
    7 #1819 ⚠️ Triage + bulk-close the v1 backlog (125 PRs) — the last irreversible phase
    8 #1821 Post-swap docs, labels, boards, integration hygiene
    9 #1822 v1 → v2 migration guide
  10. modified the milestones: v2.2.0, v2.1.0 on Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions