You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
update_server and the stale-notice paths resolve server config differently #150
Found during board review of #147 (round 3). Not a blocker for that PR, but a real inconsistency it leaves behind.
What
gateway.update_server now resolves its target through _resolve_lifecycle_config, which gives .mcp.json precedence over the manifest — the same precedence gateway.restart_server uses. The notice paths were not migrated and still use the manifest-only resolver:
handlers.py:1324 — the background stale sweep
handlers.py:3596 — _get_update_warning, via _get_server_config_for_update (handlers.py:3586)
Why it matters
After a successful update, update_server seeds _stale_check_cache with the override package's (current, latest) pair. That entry is honoured for the TTL (6h). Once it expires, the notice paths recompute — but they resolve the manifest package, and compare its upstream latest against the version recorded from the override package.
When the override merely pins the same package, the two agree and nothing goes wrong. The problem case is an override that changes package identity: the comparison is then between unrelated packages, which can resurface a false "update available" notice or hide a real one.
Fix sketch
Give the notice paths the same effective-config resolution update_server now uses, so all four sites (_get_update_warning, catalog_search's stale_updates, the background sweep, and update_server) agree on which package a server actually runs. Care needed: _get_server_config_for_update returns a manifest ServerConfig while _resolve_lifecycle_config returns a ResolvedServerConfig, so it isn't a drop-in swap.
Blast radius
Only servers whose .mcp.json entry names a different package than the manifest entry of the same name. Unpinned and same-package-pinned overrides are unaffected.
Resolved by #159 (merged as 7318f7e) — by removing the feature, after eight attempts to fix it.
Why the notice could not be made correct
Every attempt failed at the same joint: pmcp cannot observe which package artifact a running server is executing. Each of these was verified against source or a live process, not assumed:
Source tried
Why it fails
descriptions_cache.version
An upstream snapshot written at describe time, not the installed version
serverInfo.version
An implementation version. FastMCP 1.x reports the SDK's; mcp 2.x defaults version to ""
npx / uv runner caches
No server→entry binding. One package was observed cached at 1.2.0, 1.2.1 and 1.2.2 concurrently
Spawn argv
npm resolves the version aftercreate_subprocess_exec returns; no _npx path appears in any live descendant
--version probe
Disqualifying — see below
The --version probe was the last candidate and the board rejected it 3/3. It is a full launch of the server's own argv with sanitized_subprocess_env injecting that server's real credentials, on a schedule. For a manifest entry pinned to @latest, that means executing newly published third-party code with those credentials before the operator chose to update it. Measured cost: probing one flag-ignoring server spawned 7 processes and opened 5 sockets, for ~50% coverage.
So the notice could be wrong in both directions — inventing an update that did not exist, and hiding one that did — with no way to tell which.
What shipped
update_warning is gone from describe, invoke and provision; stale_updates is gone from catalog_search; the hourly background stale-version indexer is gone. The fields are removed from the output schemas, not left empty — they were previously emitted on the wire as explicit nulls, so this is a real change for a client doing key-presence checks. Recorded under ### Removed.
What remains, deliberately:gateway.update_server still probes, reports, restarts and refuses pinned servers. pmcp refresh --check-versions still reports staleness. Both are asked; neither volunteers a claim it cannot substantiate.
Notes for the record
Two things worth keeping, since this issue is mostly a record of being wrong repeatedly:
I miscounted the removal surface twice — 6, then 16, when the true number was 26 update_warning= attachment sites (provision alone held 20). Board review caught the second undercount. The fix was to stop working from a hand-written list and drive grep -c to zero.
The final defect was prose, not code.gateway.update_server's own tool description still read "Use this when invoke/describe/provision warn that a newer version is available" — shipped to every client via tools/list. An identifier-based completeness grep structurally cannot catch that. fix: remove the automatic update notices (#150) #159 adds a guard that scans every exported tool description, closing the class of gap rather than the instance.
Four guard tests now fail if any part of this is reintroduced; all were verified RED against a deliberate re-introduction rather than assumed to work.
If MCP ever exposes trustworthy version metadata without executing the server, the analysis for all eight attempts is committed under .consiliency/plans/ and reinstating this would be well-founded. Nothing today does.
Activity
fix: version comparison fails closed instead of fabricating update no…
fix: update probe no longer orphans its process tree on timeout (#157)
fix: remove the automatic update notices (#150) (#159)
ViperJuice commentedon Aug 20, 2026
Resolved by #159 (merged as
7318f7e) — by removing the feature, after eight attempts to fix it.Why the notice could not be made correct
Every attempt failed at the same joint: pmcp cannot observe which package artifact a running server is executing. Each of these was verified against source or a live process, not assumed:
descriptions_cache.versionserverInfo.versionversionto""create_subprocess_execreturns; no_npxpath appears in any live descendant--versionprobeThe
--versionprobe was the last candidate and the board rejected it 3/3. It is a full launch of the server's own argv withsanitized_subprocess_envinjecting that server's real credentials, on a schedule. For a manifest entry pinned to@latest, that means executing newly published third-party code with those credentials before the operator chose to update it. Measured cost: probing one flag-ignoring server spawned 7 processes and opened 5 sockets, for ~50% coverage.So the notice could be wrong in both directions — inventing an update that did not exist, and hiding one that did — with no way to tell which.
What shipped
update_warningis gone fromdescribe,invokeandprovision;stale_updatesis gone fromcatalog_search; the hourly background stale-version indexer is gone. The fields are removed from the output schemas, not left empty — they were previously emitted on the wire as explicit nulls, so this is a real change for a client doing key-presence checks. Recorded under### Removed.What remains, deliberately:
gateway.update_serverstill probes, reports, restarts and refuses pinned servers.pmcp refresh --check-versionsstill reports staleness. Both are asked; neither volunteers a claim it cannot substantiate.Notes for the record
Two things worth keeping, since this issue is mostly a record of being wrong repeatedly:
update_warning=attachment sites (provisionalone held 20). Board review caught the second undercount. The fix was to stop working from a hand-written list and drivegrep -cto zero.gateway.update_server's own tool description still read "Use this when invoke/describe/provision warn that a newer version is available" — shipped to every client viatools/list. An identifier-based completeness grep structurally cannot catch that. fix: remove the automatic update notices (#150) #159 adds a guard that scans every exported tool description, closing the class of gap rather than the instance.Four guard tests now fail if any part of this is reintroduced; all were verified RED against a deliberate re-introduction rather than assumed to work.
If MCP ever exposes trustworthy version metadata without executing the server, the analysis for all eight attempts is committed under
.consiliency/plans/and reinstating this would be well-founded. Nothing today does.docs(roadmap): add phase-plans-v12 after four-seat panel review (#167)