Skip to content

cli: flag unpinned npx/uvx package specs in mcp list - #275

Merged
Zongwei9888 merged 3 commits into
HKUDS:mainfrom
raymondginger2018-sudo:pr/mcp-list-supply-chain-concern
Oct 26, 2026
Merged

Zongwei9888 merged 3 commits into
HKUDS:mainfrom
raymondginger2018-sudo:pr/mcp-list-supply-chain-concern

Conversation

@raymondginger2018-sudo

Copy link
Copy Markdown
Contributor

Responds to the closing invitation on #269: "a small PR that adds a concern to
mcp list itself, for example an unpinned package (npx/uvx with @latest
or no version) or a server that receives credential environment variables."

Of those two examples the credential-env concern already exists on main
(cred -> sends-credential, merged via #241 / 2fafe40), so this PR adds the
remaining one: unpinned-package. It replaces #269's separate mcp audit
command with a concern on mcp list itself, as suggested.

What it does

mcp list now reports unpinned-package on a stdio server row when it is
launched through npx/uvx and its package argument floats -- no version at
all (npx -y left-pad) or a floating tag (npx -y left-pad@latest,
pkg@next). Those resolve whatever the registry serves at each start, so the
effective code was never chosen by the operator.

Recognition rules

command line reading
npx -y left-pad unpinned-package
npx -y left-pad@latest unpinned-package
npx -y left-pad@next unpinned-package
uvx mcp-server-time unpinned-package
npx -y left-pad@1.2.3 pinned
uvx mcp-server-time==0.3.0 pinned (PEP 508)
npx @modelcontextprotocol/server-filesystem unpinned (scoped name; the @ delimiter is only searched past position 0)
npx --index-url https://... pkg flag/URL skipped; pkg is the package token
npx ./server.js, npx C:\x.js local file -- no version to pin, not scanned
npx --version (flags only) nothing to scan
python3 server.py not a launcher, not scanned
  • Windows launcher suffixes (.cmd/.exe/.bat/.ps1) are stripped before
    matching, so C:\Program Files\nodejs\npx.cmd -y left-pad is caught.
  • Only the first package token decides; later positional arguments are the
    server's own operands (npx -y pkg /data).
  • An optional leading v in the version (pkg@v1.2.3) counts as pinned.

Scope

  • Reads only the existing command/args keys of mcp_server_view -- no
    serializer, McpServerInfo, or JSON-output change (consistent with how the
    existing concerns are table-only).
  • Files: cli/mcp_cli.py (+65), tests/test_mcp_cli.py (+88); pure additions.

Testing

  • New test_mcp_list_flags_unpinned_launcher_packages covers the rows above.
  • tests/test_mcp_cli.py: 8 passed; pinned ruff 0.15.21 check + format clean;
    local CI preflight KNOWN-RED | new=0 (the 2 known reds are the documented
    Windows environment baselines: job-windows-lifecycle:group2_rc,
    job-desktop-quality:test_rc).

@Zongwei9888

Copy link
Copy Markdown
Collaborator

Thank you, @raymondginger2018-sudo, and thanks for picking up the suggestion from #269. Keeping it as a concern on mcp list is the right shape, and the scoped-name and Windows-launcher handling look good. Two changes before merge; I checked each case against _launcher_package_spec and _package_spec_is_unpinned on this branch:

  1. Option values are read as the package. The scan skips flags but not their values, so the value decides:

    • uvx --python 3.12 mcp-server-time==0.3.0 → spec 3.12 → unpinned-package (false alarm)
    • uvx --with httpx mcp-server-time==0.3.0 → spec httpx → unpinned-package (false alarm)
    • npx --cache /tmp/c pkg → the path stops the scan → no concern (missed)

    Please read the package from --package/-p for npx and --from for uvx when present, skip the value of other options that take one (for example npx --cache, --registry, --call/-c; uvx --python/-p, --with, --index-url), and add these cases to the test.

  2. Partial versions count as pinned. pkg@1 and pkg@1.x pass because the version starts with a digit, but npm reads them as ranges. For npx, please count only a full x.y.z version (optionally with a pre-release suffix) as pinned; for uvx, == with a full version, as now.

The posture line counts only servers whose configuration resolves, and
launcher availability differs by host (Windows path / uvx missing on CI),
so a hardcoded 9 failed upstream while passing locally. Derive the count
from the CONFIG column instead; the concerns map assertion is unchanged.
The scan skipped flags but consumed their values as positionals: `uvx
--python 3.12 pkg==0.3.0` and `uvx --with httpx pkg==0.3.0` were flagged
for the option value instead of the package, and `npx --cache /tmp/c pkg`
stopped at the path and missed the real concern. Read the package from
`--package`/`-p` (npx) and `--from` (uvx) when present, and consume the
values of other value-taking options (`--cache`, `--registry`, `--call`/
`-c`; `--python`, `-p`, `--with`, `--index-url`).

npm reads a partial version as a range (`pkg@1` means every `1.x`), so
count only a full `x.y.z` with an optional pre-release suffix as pinned
for npx; uvx's `==` matches exactly at any precision and stays as it was.
Per review comment 6336671036.
@raymondginger2018-sudo
raymondginger2018-sudo force-pushed the pr/mcp-list-supply-chain-concern branch from f302ad8 to 1767abb Compare October 23, 2026 19:49
@raymondginger2018-sudo

Copy link
Copy Markdown
Contributor Author

@Zongwei9888 — both changes are pushed as 1767abb4 (3 commits, rebased onto current main):

  1. Option values are read as the package. _launcher_package_spec now resolves the package from --package/-p for npx and --from for uvx when present, and skips the value of every other value-taking option (npx --cache, --registry, --call/-c; uvx --python/-p, --with, --index-url) before falling back to positionals. Your three cases are in the table test: npx --cache /tmp/c -y left-pad (was missed → now unpinned-package), uvx --python 3.12 mcp-server-time==0.3.0 and uvx --with httpx mcp-server-time==0.3.0 (were false alarms → clean), plus --package/-p/--from package reads.
  2. Partial versions are not pinned. npx pinning now requires a full x.y.z version (optionally with a pre-release suffix): left-pad@1 and left-pad@1.x raise unpinned-package, left-pad@1.2.3-beta.1 stays clean; uvx keeps == with a full version as before.

Local CI-equivalent preflight of this exact head (Python 3.12/3.13/3.14, lint, package, desktop, secret scan): no new failures — the only red is the already-tracked Windows-only lifecycle flake. CI re-runs on the new head; dependency review, secret scan and lint are already green there.

@Zongwei9888
Zongwei9888 merged commit befd393 into HKUDS:main Oct 26, 2026
12 checks passed
@Zongwei9888

Copy link
Copy Markdown
Collaborator

Thank you, @raymondginger2018-sudo, both changes work. Merged on 2026-10-27 as befd393.

I ran every case against _launcher_package_spec / _package_spec_is_unpinned: --python/-p/--with values are skipped, npx --cache /tmp/c pkg is flagged, -p/--package/--from (including = forms and scoped names) are read, and npm @1, @1.x, ^, ~ and @latest are flagged while @1.2.3 and @1.2.3-beta.1 are clean.

One small gap, fixed on top in f427a77: a local path given through --from/-p (uvx --from . my-mcp, npx -p ./local cmd) was flagged, while the same path as a positional argument is skipped. The path check now applies to option values too, with two new test rows. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants