npx -y ssh-mcp resolves latest on every launch. In a single working session the package went v1 → v2 and the exposed tool set changed under a running agent, mid-task, with no warning — "my AI tools broke today for no reason". This detects the unpinned spec and turns it into a comprehensible pin/upgrade prompt.
Core (pure bcc_core, no network — reuses parse_version / is_newer_version / the catalog spec parsers)
server_package_spec / server_package_name — the npm spec an npx-style server launches.
is_unpinned_spec — a bare name or dist-tag (@latest/@next) is unpinned; an exact numeric version is pinned.
resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins), degrades to None cleanly. find/read are injectable so tests drive it with fixtures — never shells out, never hits the network, never touches a real node_modules in CI.
pin_spec_transform — rewrites the spec to name@version (mirrors pin_command_path's (new_data, note) contract). No-op when already pinned / bad version / not npx.
version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version (so 2 < 10, never lexical).
version_status — the badge's high-level dict (unpinned / pinned_version / resolved_version / can_pin / drift). A _UNSET sentinel lets a caller force an explicit resolved=None ("unknown") vs. omitting it to run the local lookup.
GUI (minimal, mirrors the dependency-status surface)
A version badge row under the dependency status, shown only for npx servers: the currently-resolved version, an "unpinned (resolves latest each launch)" warning, a drift note when a pin has been overtaken, and a one-click "Pin to <version>" button (only when unpinned and a resolved version is known). Smoke-tested headlessly with QT_QPA_PLATFORM=offscreen.
Testing
ruff + format --check clean; pytest green (520 passed). Fixture-driven tests for spec detection, unpinned classification, package.json parsing, cache resolution (highest-wins + degrade-to-unknown), the pin transform (incl. no-op cases), numeric drift, and every version_status branch.
Stacked PR — base is feat/90 (depends only on the ServerSpec spine — sibling of #91, not built on it). Review/merge #90 first, then this (in either order relative to #91). 🤖 Generated with Claude Code
## What (P0 — correctness)
`npx -y ssh-mcp` resolves **latest on every launch**. In a single working session the package went v1 → v2 and the exposed tool set changed **under a running agent, mid-task**, with no warning — "my AI tools broke today for no reason". This detects the unpinned spec and turns it into a comprehensible pin/upgrade prompt.
### Core (pure `bcc_core`, **no network** — reuses `parse_version` / `is_newer_version` / the catalog spec parsers)
- **`server_package_spec` / `server_package_name`** — the npm spec an npx-style server launches.
- **`is_unpinned_spec`** — a bare name or dist-tag (`@latest`/`@next`) is unpinned; an exact numeric version is pinned.
- **`resolved_npx_version`** — reads the local `~/.npm/_npx` cache (highest version wins), degrades to `None` cleanly. `find`/`read` are injectable so tests drive it with fixtures — **never shells out, never hits the network, never touches a real `node_modules` in CI**.
- **`pin_spec_transform`** — rewrites the spec to `name@version` (mirrors `pin_command_path`'s `(new_data, note)` contract). No-op when already pinned / bad version / not npx.
- **`version_drift_note`** — *"moved X → Y since you pinned"* via numeric `is_newer_version` (so 2 < 10, never lexical).
- **`version_status`** — the badge's high-level dict (`unpinned` / `pinned_version` / `resolved_version` / `can_pin` / `drift`). A `_UNSET` sentinel lets a caller force an explicit `resolved=None` ("unknown") vs. omitting it to run the local lookup.
### GUI (minimal, mirrors the dependency-status surface)
A version badge row under the dependency status, shown **only for npx servers**: the currently-resolved version, an "unpinned (resolves latest each launch)" warning, a drift note when a pin has been overtaken, and a one-click **"Pin to `<version>`"** button (only when unpinned *and* a resolved version is known). Smoke-tested headlessly with `QT_QPA_PLATFORM=offscreen`.
### Testing
`ruff` + `format --check` clean; `pytest` green (520 passed). Fixture-driven tests for spec detection, unpinned classification, package.json parsing, cache resolution (highest-wins + degrade-to-unknown), the pin transform (incl. no-op cases), numeric drift, and every `version_status` branch.
Closes #92. Part of epic #94.
---
**Stacked PR — base is `feat/90`** (depends only on the ServerSpec spine — sibling of #91, **not** built on it). Review/merge **#90 first**, then this (in either order relative to #91).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went
v1 → v2 and the tool set changed under a running agent, mid-task, with no warning.
This detects that and turns it into a comprehensible pin/upgrade prompt.
Core (pure, no network — reuses parse_version / is_newer_version / the catalog
spec parsers; the resolved-version lookup is fully injectable for tests):
- server_package_spec / server_package_name — the npm spec an npx-style server runs.
- is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact
numeric version is pinned.
- resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins),
degrades to None cleanly. NO network. find/read injectable.
- pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's
(new_data, note) contract). No-op when already pinned / bad version / not npx.
- version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version.
- version_status — the badge's high-level dict (unpinned / pinned_version /
resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an
explicit resolved=None ("unknown") vs. omitting it to do the local lookup.
GUI: a version badge row under the dependency status (mirrors that surface) with a
one-click "Pin to <version>" button, shown only for npx servers; a drift note when
a pinned version has been overtaken. Smoke-tested headlessly.
pytest green (520 passed), ruff + format clean. Closes#92. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What (P0 — correctness)
npx -y ssh-mcpresolves latest on every launch. In a single working session the package went v1 → v2 and the exposed tool set changed under a running agent, mid-task, with no warning — "my AI tools broke today for no reason". This detects the unpinned spec and turns it into a comprehensible pin/upgrade prompt.Core (pure
bcc_core, no network — reusesparse_version/is_newer_version/ the catalog spec parsers)server_package_spec/server_package_name— the npm spec an npx-style server launches.is_unpinned_spec— a bare name or dist-tag (@latest/@next) is unpinned; an exact numeric version is pinned.resolved_npx_version— reads the local~/.npm/_npxcache (highest version wins), degrades toNonecleanly.find/readare injectable so tests drive it with fixtures — never shells out, never hits the network, never touches a realnode_modulesin CI.pin_spec_transform— rewrites the spec toname@version(mirrorspin_command_path's(new_data, note)contract). No-op when already pinned / bad version / not npx.version_drift_note— "moved X → Y since you pinned" via numericis_newer_version(so 2 < 10, never lexical).version_status— the badge's high-level dict (unpinned/pinned_version/resolved_version/can_pin/drift). A_UNSETsentinel lets a caller force an explicitresolved=None("unknown") vs. omitting it to run the local lookup.GUI (minimal, mirrors the dependency-status surface)
A version badge row under the dependency status, shown only for npx servers: the currently-resolved version, an "unpinned (resolves latest each launch)" warning, a drift note when a pin has been overtaken, and a one-click "Pin to
<version>" button (only when unpinned and a resolved version is known). Smoke-tested headlessly withQT_QPA_PLATFORM=offscreen.Testing
ruff+format --checkclean;pytestgreen (520 passed). Fixture-driven tests for spec detection, unpinned classification, package.json parsing, cache resolution (highest-wins + degrade-to-unknown), the pin transform (incl. no-op cases), numeric drift, and everyversion_statusbranch.Closes #92. Part of epic #94.
Stacked PR — base is
feat/90(depends only on the ServerSpec spine — sibling of #91, not built on it). Review/merge #90 first, then this (in either order relative to #91).🤖 Generated with Claude Code
`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went v1 → v2 and the tool set changed under a running agent, mid-task, with no warning. This detects that and turns it into a comprehensible pin/upgrade prompt. Core (pure, no network — reuses parse_version / is_newer_version / the catalog spec parsers; the resolved-version lookup is fully injectable for tests): - server_package_spec / server_package_name — the npm spec an npx-style server runs. - is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact numeric version is pinned. - resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins), degrades to None cleanly. NO network. find/read injectable. - pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's (new_data, note) contract). No-op when already pinned / bad version / not npx. - version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version. - version_status — the badge's high-level dict (unpinned / pinned_version / resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an explicit resolved=None ("unknown") vs. omitting it to do the local lookup. GUI: a version badge row under the dependency status (mirrors that surface) with a one-click "Pin to <version>" button, shown only for npx servers; a drift note when a pinned version has been overtaken. Smoke-tested headlessly. pytest green (520 passed), ruff + format clean. Closes #92. Part of epic #94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Pull request closed