Problem
npm deprecate is a point-in-time write to the versions matching the range when you run it — not a standing rule. Nothing carries it forward to versions published later, and there is no deprecation metadata in package.json.
Result: every v1 release lands on npm un-deprecated. This is not hypothetical — 1.0.2 shipped that way and had to be fixed by hand after the fact. The repo-side notices from #1827 (READMEs, runtime stderr banners) are a separate mechanism and do not touch registry metadata; #1816 tracked the npm deprecate run as a one-off manual step, and there is nothing to make it repeat.
The same trap already bit once before, silently: 2.0.0-rc.1/2/3 were published after #1816's run and are un-deprecated to this day. That one is benign (they are v2 prereleases and should not carry a v1 notice) but it is the same root cause.
Proposal
Add a deprecate step to the publish job in .github/workflows/main.yml, after npm run publish-all, so a v1 release is deprecated the moment it is published.
Deprecate the exact version, not a range
The range recorded in #1816 is @"<2.0.0". Do not automate that range. In semver a prerelease sorts below its release, so <2.0.0 matches 2.0.0-rc.1, 2.0.0-rc.2 and 2.0.0-rc.3. Verified against the live registry — a <2.0.0 deprecate today would stamp all three v2 release candidates with "v1 is deprecated. Upgrade to v2."
Automate on the version being published instead. It is exact, idempotent, and immune to this class of range rot:
- name: Deprecate the published v1 packages
run: |
set -euo pipefail
VERSION="$(node -p 'require("./package.json").version')"
MSG="v1 is deprecated. Upgrade to v2: npm i @modelcontextprotocol/inspector@latest. v1 gets security fixes only, published under the v1-latest tag."
# An empty message LIFTS deprecation — fail loudly rather than silently un-deprecating.
[ -n "$MSG" ] || { echo "refusing to run with an empty message"; exit 1; }
for pkg in inspector inspector-cli inspector-client inspector-server; do
npm deprecate "@modelcontextprotocol/${pkg}@${VERSION}" "$MSG"
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_DEPRECATE_TOKEN }}
If the range form is ever wanted again, the correct spelling is @"<2.0.0-0" — the -0 excludes prereleases of 2.0.0.
The message must stay in sync
The wording above is byte-identical to what is already on 1.0.0 / 1.0.1 / 1.0.2 across all four names, and to the runtime banners added in #1827. #1827 called out deliberately that the banner and the npm warning use identical wording. If this step is added, that string now lives in a third place — worth a comment pointing at cli/src/cli.ts, server/src/index.ts and client/bin/start.js.
The catch: this needs a stored npm token
Publishing currently uses OIDC trusted publishing (id-token: write, NPM_CONFIG_PROVENANCE: "true", no NODE_AUTH_TOKEN anywhere). The repo has no Actions secrets at all — neither repo-level nor on the release environment. That is a genuinely good posture.
OIDC covers publish. It does not cover npm deprecate, which is an ordinary authenticated registry write. Per the npm trusted publishers docs: "OIDC authentication supports the npm publish and npm stage publish commands. [...] Other npm commands such as install, view, or access still require traditional authentication methods." Confirmed empirically both ways during the 1.0.2 release: the publish job succeeded carrying only id-token: write and no NODE_AUTH_TOKEN, while an interactive npm deprecate authenticated as a logged-in user and then failed the registry write with HttpErrorAuthOTP: OTP required (EOTP). So automating this means introducing the first long-lived npm credential to a repo that currently has none, and that is a real security trade-off, not a formality — it should be a deliberate decision rather than a side effect of convenience.
If accepted, limit the blast radius:
- Use a granular access token scoped to only the four
@modelcontextprotocol/inspector* packages, with write permission and an expiry.
- Store it as an environment secret on
release, not repo-wide, so it inherits the existing required-reviewer gate (dsp-ant, pcarleton, olaservo, cliffhall).
- A granular/automation token also sidesteps the 2FA prompt. The publishing account is set to 2FA for authorization and writes, so an interactive
npm deprecate fails with EOTP — which is exactly why this cannot simply be scripted locally without a human present.
Alternative if the token is unacceptable
Keep it manual but stop relying on memory: add the four commands to the v1 release checklist so they run immediately after the release is approved. Cheaper and introduces no credential, but it will be forgotten again — it already has been, twice.
Acceptance criteria
Context
Problem
npm deprecateis a point-in-time write to the versions matching the range when you run it — not a standing rule. Nothing carries it forward to versions published later, and there is no deprecation metadata inpackage.json.Result: every v1 release lands on npm un-deprecated. This is not hypothetical — 1.0.2 shipped that way and had to be fixed by hand after the fact. The repo-side notices from #1827 (READMEs, runtime stderr banners) are a separate mechanism and do not touch registry metadata; #1816 tracked the
npm deprecaterun as a one-off manual step, and there is nothing to make it repeat.The same trap already bit once before, silently:
2.0.0-rc.1/2/3were published after #1816's run and are un-deprecated to this day. That one is benign (they are v2 prereleases and should not carry a v1 notice) but it is the same root cause.Proposal
Add a deprecate step to the
publishjob in.github/workflows/main.yml, afternpm run publish-all, so a v1 release is deprecated the moment it is published.Deprecate the exact version, not a range
The range recorded in #1816 is
@"<2.0.0". Do not automate that range. In semver a prerelease sorts below its release, so<2.0.0matches2.0.0-rc.1,2.0.0-rc.2and2.0.0-rc.3. Verified against the live registry — a<2.0.0deprecate today would stamp all three v2 release candidates with "v1 is deprecated. Upgrade to v2."Automate on the version being published instead. It is exact, idempotent, and immune to this class of range rot:
If the range form is ever wanted again, the correct spelling is
@"<2.0.0-0"— the-0excludes prereleases of 2.0.0.The message must stay in sync
The wording above is byte-identical to what is already on 1.0.0 / 1.0.1 / 1.0.2 across all four names, and to the runtime banners added in #1827. #1827 called out deliberately that the banner and the npm warning use identical wording. If this step is added, that string now lives in a third place — worth a comment pointing at
cli/src/cli.ts,server/src/index.tsandclient/bin/start.js.The catch: this needs a stored npm token
Publishing currently uses OIDC trusted publishing (
id-token: write,NPM_CONFIG_PROVENANCE: "true", noNODE_AUTH_TOKENanywhere). The repo has no Actions secrets at all — neither repo-level nor on thereleaseenvironment. That is a genuinely good posture.OIDC covers
publish. It does not covernpm deprecate, which is an ordinary authenticated registry write. Per the npm trusted publishers docs: "OIDC authentication supports thenpm publishandnpm stage publishcommands. [...] Other npm commands such asinstall,view, oraccessstill require traditional authentication methods." Confirmed empirically both ways during the 1.0.2 release: thepublishjob succeeded carrying onlyid-token: writeand noNODE_AUTH_TOKEN, while an interactivenpm deprecateauthenticated as a logged-in user and then failed the registry write withHttpErrorAuthOTP: OTP required(EOTP). So automating this means introducing the first long-lived npm credential to a repo that currently has none, and that is a real security trade-off, not a formality — it should be a deliberate decision rather than a side effect of convenience.If accepted, limit the blast radius:
@modelcontextprotocol/inspector*packages, with write permission and an expiry.release, not repo-wide, so it inherits the existing required-reviewer gate (dsp-ant,pcarleton,olaservo,cliffhall).npm deprecatefails withEOTP— which is exactly why this cannot simply be scripted locally without a human present.Alternative if the token is unacceptable
Keep it manual but stop relying on memory: add the four commands to the v1 release checklist so they run immediately after the release is approved. Cheaper and introduces no credential, but it will be forgotten again — it already has been, twice.
Acceptance criteria
v1/mainends with all four packages deprecated at that exact version, with no manual stepreleaseenvironmentContext
<2.0.0range originatev1-latest— npm rejectsv1#1829 — why the dist-tag isv1-latestand notv1