Repository navigation
docs(plan): version pinning for self-hosted-backed MCP clients (see Consiliency/pmcp#294) - #295
Open
ViperJuice wants to merge 22 commits into
Open
ViperJuice wants to merge 22 commits into
ViperJuice wants to merge 22 commits into
Conversation
) Detailed plan for proposals 1 and 2 of #294: - a first-class `version:` on manifest entries, plus an overlay `server_version:` map (mirroring `server_env:`) that pins a built-in entry without restating its install matrix; - `gateway.update_server` / `pmcp update` reporting "pinned at X, newer available: Y"; - an advisory warning in gateway.health and update_server when an entry's api_key_optional_when relaxer is active and its client is unpinned. Proposals 3 (smoke probe) and 4 (version in error hints) are sequenced as follow-up slices, each with a design note. Design: - Grammar: exactly one SemVer version (is_valid_package_version). Ranges and dist-tags are refused because they re-resolve at every spawn. - Materialisation: one post-overlay pass rewrites the npx package slot of args AND every install argv. It keeps the entry's own package name, so an overlay can pick a version but never a different package. - A pin that cannot be applied is dropped with a warning (all or nothing), and the entry stays honestly unpinned. - uvx/pip/cargo/docker and remote entries are refused with the .mcp.json explicit-args escape hatch. Measured on a spike at 9ca081e, which was then reverted: - tests: 37 new, red on HEAD (36 failed / 1 inertness guard passed), green with the diff; - mutants: 14/14 red, each for the named reason; - full suite: 4108 passed / 0 failed; - CI gates: ruff check, ruff format --check and mypy src/ all clean. Two regressions were caught by measurement and fixed in the embedded diff: - moving update_server's contract docstring broke a test_tools docstring test; - the pinned branch's new registry read made an existing pinned-refusal test go to the network. A no-network plugin found exactly one offender, which is now stubbed offline. 77 of 107 shipped entries are pinnable. Plan-only commit: src/ and tests/ are byte-identical to HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve the board round (see #294) Board round on #295 @ 9d08184 (gemini AGREE, claude PARTIALLY AGREE, codex DISAGREE x3 blocking, grok timed out). Each blocking finding was reproduced first, then fixed with a test that is red on the revision-1 spike and green now, plus a mutant. - codex P1 / claude N1: an alias slot (myalias@npm:x@1) was rewritten to another package. Only a plain registry spec is pinned now: name, or name@<exact version | dist-tag>. Aliases, URLs, git, file and ranges are refused. The real resolver confirms myalias@3.25.5 -> npm:myalias. - codex P2 / claude F1: a range or tag silenced the warning and printed [PINNED]. Now only an exact pin suppresses the warning, and update_server reports floating_selector; pmcp update prints [FLOATING]. The refusal itself is unchanged. - codex P3: one malformed entry aborted the manifest load. Pinning is now contained per entry. main: 108 entries; rev1: LOAD FAILED; now: 108. - claude F2: the advisory relaxer check now reads the environment the child actually spawns with (sanitized_subprocess_env). The credential gates are unchanged. - claude F3: health judges the connected configs and caches manifest facts by a stat-only fingerprint. Steady-state cost went from 220.7 ms to 1.3 ms; WARNING lines over 21 calls went from 42 to 1. - claude N2: build metadata (+...) is refused. - gemini: the PyPI == lookup name and the descriptions-cache label are tabled explicitly. Measured: - tests: 57 (19 new cases red on rev 1); 56 red / 1 guard green on HEAD; - mutants: 24/24 red; - full suite: 4128 passed / 0 failed; - no-network run: 878 passed over 19 files; - CI gates: ruff check, ruff format --check and mypy src/ clean. Plan-only commit: src/ and tests/ are byte-identical to HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve board round 2 (see #294) Board round 2 on 95968c8: the claude seat returned DISAGREE with one blocker. Every item below was reproduced first, then fixed, with a test that is red on the revision-2 spike and green now, plus a mutant. - B1 (blocking): a tarball selector or bare tarball slot (firecrawl-mcp@corp-mcp.TGZ, corp.tgz) was pinned to the public registry package. The fix uses npm's own isFileType rule (/[.](?:tgz|tar\.gz|tar)$/i) on the selector and on an unscoped name, as npa does, and re-derives the whole allowlist against npm-package-arg class by class. The class table is in D3. On a 461-slot corpus against the host's real npa, the old rule had 136 violations and the new one has 0. The rule is pure grammar, so pins still work while identity is disabled. - N1: the project-overlay and trust-store fingerprint components now have tests (mutants M32, M33). - N2: when npm identity is disabled, npx slots are read structurally with the same grammar, and the warning says so. If even that can't decide, it says the pin cannot be verified. Measured with npm_config_cache set: rev 2 gave []; rev 3 warns. - N3: a configured +metadata pin is labelled with what npm runs (3.25.5, "build metadata '+evil' is ignored by npm"). - N4: the tag word uses fullmatch, and x/X/v1.2.x range words are refused. - N5: unchanged, as instructed. Measured: - tests: 89 (15 of them red on rev 2); - mutants: 33/33 red (M26 is red on the bare-tarball cases); - full suite: 4160 passed / 0 failed; - no-network run: 910 passed; - neighbouring suites: 1620 passed; - CI gates: ruff check, ruff format --check and mypy src/ clean. Plan-only commit: src/ and tests/ are byte-identical to HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve board rounds 3-4, re-derived against main (see #294) - Pin grammar: a version-shaped tarball selector (`name@3.25.5-corp.tgz`) or pin value is refused, as npm classifies it; the grammar inherits main's exact-version check (npm 10 and 11 tarball rule, core-part bound) rather than restating it. Reference patch re-derived against main 959d4d4 (the validation change is on main). - Range words with repeated `v`, reserved names, `npm exec` launches with identity disabled (fails loud), and the fallback's stated cause. - Conflict with the derived gateway schemas resolved: update_server validates its input model itself. - Conformance against npm-package-arg 12.0.2 and 13.0.2 (1,617,455 slots, 41,472 pin values): 0 violations on both. 112 tests; 40/40 mutants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
… (see #294) - C1: an exact npm pin is silent only when nothing besides the argv can change what npm runs for it: npm_config_* keys judged by an allowlist derived from the npm 10.9.9 and 11.19.0 config definitions, NODE_OPTIONS, the overlay keys identity selects, a local prefix, and the project, user, global and builtin npmrc files, in both identity modes. - C2: _is_exact_pin decides per launcher; a docker tag floats and only a content digest pins, for the warning and for [PINNED]/[FLOATING]. - NB-1: the fallback cause names a launcher identity does not read. - NB-2: the floating text claims only what holds on every npm release. - NB-3: the #300 test count is attributed correctly (416 for two files). Re-measured on 959d4d4: 153 pin tests (41 red on rev 5), 56/56 mutants red, full suite 4489 passed, conformance 0/0 on npa 12.0.2 and 13.0.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…trust boundary (see #294) Maintainer decision: trust the host. The warning now judges only what a manifest entry or overlay controls -- its argv, its launcher and the env block it injects -- and states the host boundary (npmrc files, shell env, shims, caches, global bin, proxy/CA, uv/cargo/docker config) as a non-goal in the plan and in the README text. - B1: rev 6's host npmrc/local-prefix/env discovery is removed. - B2: the entry-env rule covers uvx (UV_*/PIP_*) and cargo (CARGO_*/RUSTC*), plus uvx/cargo argv flags such as --index-url and --git. - N1/N3/N4: an entry's npm_config_cache and proxy/CA variables are loud; the wording says a pin holds the top-level package. - N5: a uvx URL requirement is never exact; a malformed docker digest is labelled as such; an unreadable docker/uvx argv fails loud. Measured: 186 pin tests (54 red on rev 6), 60/60 mutants red, full suite 4539 passed on 876fd33, conformance 0/0 on npa 12.0.2 and 13.0.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
A cwd that an entry or overlay sets chooses which project configuration the launcher reads (.npmrc / package.json up the tree, uv.toml / [tool.uv], .cargo/config.toml), so an exact pin with an entry-set cwd is "cannot verify" for npm/npx, uvx and cargo. docker is exempt: docker run reads no cwd-relative configuration and a digest is content-addressed. An inherited cwd stays the host's. The shipped manifests set no cwd (0 matches), and shipped coverage is unchanged at 77 pinnable / 30 refused. Measured on origin/main 876fd33: 190 pin tests (3 red on 1073923), 63/63 mutants red, full suite 4543 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
#294) Board round 6 found that both of rev 7's entry rules were denylists, which fail open on every spelling they do not list. Rev 8 inverts both: - ENV: every key the entry injects must be proven inert (the server's own declared keys outside tool namespaces, locale/terminal keys, and short per-launcher logging/timing/credential lists); unknown keys, XDG_*, LD_*, CC and empty values of non-inert keys are "cannot verify" (X2, N1, N4). - ARGV: an exact pin is silent only in a recognised shape where the pinned package is what runs (npx/npm exec, uvx and `uv tool run`, cargo install, docker run with nothing after the image); update_server applies the same check before [PINNED] (X1, N3). - Unmodelled runners and wrappers (bunx, pnpx, pnpm/yarn dlx, uv run, node, shells, env) are "cannot verify"; README and D7 say a docker digest pins the image, not what runs in it (N2). Shipped cost: 0 of 77 pinnable entries. Measured on origin/main 876fd33: 219 pin tests (24 red on 15dae94), 73/73 mutants red, full suite 4572 passed, conformance 0/0 on npa 12.0.2 and 13.0.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ons exempt a key (see #294) Board round 7, both blockers inside the trust boundary: - B1: gateway.provision spawns install[platform] and adopts it as the live server. For a manifest-sourced server every non-empty install argv must now run the same exact pin in a recognised shape with an inert env (the _config_runs_exactly rule), in the warning and before [PINNED]; the message names the argv. Spawn sites are enumerated in the plan. - B2: a key is the server's own only if pmcp's shipped manifest.yaml declares it for that name; overlay/.pmcp.json/.mcp.json declarations exempt nothing, and the namespace denylist is removed. - N1-N3: dependency-scope registry and install argvs documented; docker -it and npx --yes=true modelled; remaining parser spellings documented. Shipped cost: 0 of 77. Measured on origin/main 876fd33: 230 pin tests (11 red on 253445a), 80/80 mutants red, full suite 4583 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294) Board round 8 found a spawn path escaping the check again: the warning's local-binary early return ran before the install argvs were judged, so an overlay with `command: firecrawl-mcp` and the shipped unpinned install argv was silent while gateway.provision spawned and adopted latest. - The warning now computes the whole spawn set first (args, and for a manifest server every non-empty install argv, from _SERVER_SPAWN_SITES) and judges every member before any exemption; the local-binary exemption applies only when every member is literally [command, *args]. - test_every_spawn_site_is_classified walks src/pmcp with ast and fails on any spawn call site the classification table does not list. - The spawn-site table is corrected (N-1: the descriptions refresh spawns the manifest argv with no entry env; N-2: there is no `pmcp install`); the handoff re-read (N-3) is documented as R15. Shipped cost: 0 of 77. Mutants 84/84 red; 236 pin tests (5 red on bdc13ca); full suite 4739 passed on origin/main 260cc1a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 9 found two recurring classes (two readers that disagree; an exemption applied before a check) plus unredacted diagnostics and unguarded exceptions. Structural fixes: - One parser per launcher (_read_argv) is the only reader of a spawning argv for the warning and the [PINNED] label; a repeated single-valued flag (uvx --from, cargo --version, docker --name) is unreadable, and `npm exec` flags after the spec are loud. - One judge (_judge_spawn_set) checks every spawn member's shape, pin, env and cwd before any verdict; the local-binary exemption waives only the pin. - Every argv shown in a diagnostic goes through the secret-safe renderer. - A malformed member or an exception is contained to that server's own "cannot verify"; health catches per server. - _SERVER_SPAWN_SITES now builds the spawn set; the AST spawn-site test resolves imports/aliases and counts references per site (22 of 22 synthetic variants found). Shipped cost: 0 of 77. Measured on origin/main 260cc1a: 263 pin tests (34 red on 0a63441), 84/84 mutants red, full suite 4766 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ce (see #294) Board round 10: every command that was neither a modelled launcher nor a listed runner counted as a "local binary", which waived the pin, so `timeout 600 npx -y firecrawl-mcp`, sudo/nice/busybox/mise/volta/corepack wrappers, pipx/go/python -m runners and npx.js were silent. - A command is a local server binary only when pmcp's shipped manifest names that exact command for the server (read directly, as the declarations are); every other unrecognised command is "cannot verify", named. A known launcher name with an unstripped extension is never local. - The redaction claim is narrowed to this feature's diagnostics (main's unknown-package line is a follow-up); cwd is escaped; a string install argv is one unreadable member; README notes a uv ==X pin is a version, not an artifact. Shipped cost: 0 of 77 (shipped commands are only npx and uvx). Measured on origin/main 260cc1a: 358 pin tests (95 red on ba58ade, incl. 94 generated wrapper x launcher ids), 87/87 mutants red, full suite 4861 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294) Board round 11: - Whether the warning applies was read from the overlay-applied entry's api_key_optional_when in three places (the update wrapper, the health filter, relaxed_by), so an overlay replacing firecrawl without the key, or with requires_api_key: false, silenced it. The relaxer keys are now the shipped manifest's (read directly) union the overlay's; the credential gate is unchanged; the overlay-only-name limit is disclosed. - A modelled launcher must be spelled as itself: the bare platform name, or the path the child's PATH resolves it to. /tmp/x/npx, ./npx, node_modules/.bin/npx, NPX, C:npx and npx.cmd on Linux are "cannot verify". - The plan records a sweep of every input the apply / silent / [PINNED] decisions read and where each comes from. Shipped cost: 0 of 77. Measured on origin/main 260cc1a: 368 pin tests (16 red on 791380c), 92/92 mutants red, full suite 4871 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 12: a relative launcher path was compared with realpath from pmcp's cwd, but the OS resolves it from the child's (entry-set) cwd, so a deep relative docker path plus an entry cwd was silent and [PINNED]. - A relative launcher path is never the launcher, on every platform. - docker with an entry-set cwd is "cannot verify" when the child's PATH would search that cwd (a relative or empty entry, or always on Windows, where CreateProcess searches the cwd first). - R20 widened to .mcp.json/.pmcp.json servers under unshipped names; the input-sweep launcher row corrected. Shipped cost: 0 of 77. 388 pin tests (7 red on 03edb0a), 95/95 mutants red, full suite 4891 passed on origin/main 260cc1a and on 4d2790f. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 13 on 78b21f4. docker's cwd exemption holds only for the bare `docker` spelling; launcher paths are compared literally with the child's PATH search, never resolved in pmcp's process; /proc PATH entries count as searching the cwd; Windows .cmd/.bat argvs refuse cmd.exe metacharacters; docker's relaxer is judged on the container's env (--env-file applies the warning). Windows text corrected: CreateProcess searches the parent's cwd and PATH. 407 tests (15 red on rev 14), 102 of 102 mutants red, shipped cost 0 of 77; re-based on origin/main 7680445. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…e parser (see #294) Board round 14 on 6086712. Whether the warning applies is now decided per spawn member from the one parser's reading: a clean docker run launched as docker itself uses its container env (read by _read_docker, which now parses pflag shorthand clusters); a docker argv pmcp cannot fully read is "may"; every other member uses its own process env. The warning applies if any member applies. The second docker-env reader is deleted. Windows PATHEXT-case text and the sweep table are corrected, and the input sweep lists every decision's reader. 429 tests (20 red on rev 15), 111 of 111 mutants red, shipped cost 0 of 77, on origin/main 7680445. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…passed the judge (see #294) Board round 15 on 56dfca4. A docker member's container env alone decides the relaxer only when that member passed the per-member judge (_member_problems empty, with the shipped declarations); a bare docker the entry can redirect (entry PATH, entry cwd under a cwd-searching PATH) is judged on its process env too. A launcher's identity is trusted only after the judge. Discloses image-ENV relaxers (R22) and parse-clean argvs docker refuses. 435 tests (4 red on rev 16), 114 of 114 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5142 passed (a first -x run under mutant load hit a flake in main's stderr-monitor test). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…nd (see #294) Board round 16 on 9d0a73f. For a server with relaxer keys, any spawn member the one parser cannot fully read (any launcher, any reason: env prefix, sh -c, npx -c, a wrapper, podman, docker by another path, an unread flag) now makes the warning apply as "may talk to a self-hosted backend", naming the member; only fully read members are judged by their env. R21 narrowed and R18 widened accordingly. 449 tests (9 red on rev 17), 116 of 116 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5156 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
#294) Board round 17 on 98b367e. The relaxer decision and the per-member judge now read one result: _judge_members computes each member's problems once (_Judged), _verdict_of builds the verdict from it, and _member_relaxer reads the same object. For a relaxer-bearing server, any member problem (unreadable argv, non-allowlisted env key such as NODE_OPTIONS, container env key, entry cwd, launcher identity) makes the relaxer "may talk", naming the problem; only a problem-free member is judged by its env. The --env-file wording is restored; README notes the project .env case; R18 lists the measured operator cost (13 of 17 vendor-hosted setups warn). 461 tests (9 red on rev 18), 119 of 119 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5168 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294) Board round 18 on a9e53b8 (codex B1). uv hands the --from value or command to PythonRequest::try_from_tool_name first, so `uvx python==3.10` (and --from / uv tool run, cpython/pypy/graalpy/pyodide, any case, any version-request spelling) runs an interpreter whose patch floats, not a PyPI package. _read_uvx now refuses such a component as "uv reads '<x>' as an interpreter request, not a package", never exact or [PINNED]. Every spelling was checked against uv 0.12.19 on this host; uv's ==X for real packages was re-checked (only local labels and zero-padded equal releases, both non-PyPI). 479 tests (14 red on rev 19), 124 of 124 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5186 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
…nly from the judge (see #294) Board round 19 on 23e1e44. uv trims whitespace before a version request and packaging accepts it, so `uvx 'python ==3.10'`, `--from 'python == 3.10'`, `uv tool run 'cpython ==3.10'` and a tab were read as exact pins while uv ran interpreters. _read_uvx now refuses when the parsed requirement name is an interpreter prefix or when uv's own text reading (whitespace skipped) says interpreter. update_server's [PINNED]/pinned_version carry only the judge's selector. Differential against a measured uv 0.12.19 table (3,024 components x 3 forms; 880 interpreter cells): rev 20 read 891 argvs as pins, rev 21 reads 0. 490 tests (8 red on rev 20), 127 of 127 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5197 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
…enderer (see #294) Board round 20 on a49dc28 (codex B1). The interpreter diagnostic quoted the whole uv requirement through _operator_safe, which escapes but never redacts, so a credential in a `--from "python @ https://user:TOKEN@..."` reached the warning; the same construction leaked through the cargo toolchain, the cwd path and a flag name. Every string the parser, the per-member judge and the warning build is now fixed text, a value through a closed grammar (diag_name / diag_flag / diag_selector), or the one renderer; the interpreter message names only the interpreter; a cwd is not echoed; update_server's label text and the loader's pin logs use the same grammars. A property test drives every problem branch (found by ast) with a credential-shaped sentinel; a static test checks every interpolation. Folds in N1 (v-prefixed interpreter requests). 493 tests, 134 of 134 mutants red, shipped cost 0 of 77; full suite with -x on origin/main 7680445: 5200 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
ViperJuice
added a commit
that referenced
this pull request
Sep 30, 2026
Piece 1 of #294. A manifest `version:` or overlay `server_version:` must be one exact version. It is written into `args` and every platform's install argv, all or nothing, and only when: - the launcher is a bare `npx`; - every slot is a plain same-name registry spec; - the entry's own env cannot redirect npm. Refusals are logged as fixed text; server names are shown only if pmcp ships them. `update_server` / `pmcp update` report `[PINNED]` only when the materialised pin is exactly what runs. The self-hosted-drift warning is piece 2 (reference: #295). Approved by the cross-vendor review panel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiabTgWZRfuqiUQ9siEegF
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detailed plan for #294 proposals 1 and 2. Proposals 3 (post-update smoke probe) and 4 (client version in error hints) are sequenced as follow-up slices, with a design note each. Plan-only PR: the plan embeds a measured reference patch, and implementation is a separate PR after this one passes review.
What it plans
version:field on manifest entries, plus an overlay mapserver_version: {firecrawl: "3.25.5"}that pins a built-in entry without restating its install matrix. It followsserver_env:'s rules: it can't create a server, and a project overlay only applies after trust-store approval.is_valid_package_version). Ranges and dist-tags are refused, because npx re-resolves them on every spawn.pmcp update/gateway.update_serverreport "pinned at X, newer available: Y" (pinned_version,latest_available,latest_comparison) instead of failing.gateway.healthandupdate_serverwhen an entry's self-hosted relaxer (api_key_optional_when) is active in its real environment and the launching argv is unpinned (@latestcounts as unpinned).Measured on a reverted spike at
9ca081etests/test_version_pin.py: 37 new tests. On main, 36 fail for the stated reason; the one that passes asserts existing behaviour. All 37 pass with the patch.ruff check,ruff format --checkandmypyare clean.Open questions for the maintainer (the plan picks a conservative default for each)
firecrawlentry? Default: no, because it would pin vendor-hosted users too.servers:entry that carries onlyversion:merge into the built-in entry? Default: no.server_version:does that job instead, and switchingservers:from replace to merge would change the meaning of every existing overlay.pmcp update --allexit nonzero on failure? Default: unchanged..pmcp.json? Default: no, because nothing there declares a self-hostable backend.cmd /c npxas npx? Default: out of scope.See #294.
🤖 Generated with Claude Code
https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.