vMCP: backend health/set changes are not propagated to live sessions (stale tool catalog until reconnect; no tools/list_changed)
@premctl is already working on this.
Since Jul 17, 2026.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
vMCP: backend health/set changes are not propagated to live sessions (stale tool catalog until client reconnect; no tools/list_changed)
Type: bug / enhancement · Area: vMCP (aggregator / server / health) · Version: v0.34.0 (latest release; also on main)
Related: companion to the SSRF-allowlist request (co-located backends) — same deployment.
Summary
When a vMCP backend's availability changes at runtime — a backend recovers (unhealthy→healthy), or is added/removed from the group — already-connected client sessions do not see the change. Their tool catalog stays as it was when the session was created, and the vMCP never emits notifications/tools/list_changed. Clients must reconnect (re-initialize the session) to pick up a newly-healthy backend's tools. New sessions are fine; existing ones are stuck.
Repro
- Run a vMCP fronting ≥2 backends; connect a client (session A).
- A backend that was unhealthy at A's connect time becomes healthy (fix its endpoint, restart it, etc.). The vMCP re-aggregates — logs
capability aggregation complete backends:N tools:Mwith the new backend included. - From session A: the new backend's tools are not callable (
call_tool→tool not found) and don't appear in discovery. A freshly connected session B sees them. - Only reconnecting session A surfaces them.
(In optimizer mode this is extra-confusing: the client's advertised tools are the stable find_tool/call_tool meta-tools, so even a client that honors tools/list_changed has nothing new to re-fetch — the staleness lives inside the optimizer catalog that find_tool/call_tool resolve against.)
Root cause (verified, v0.34.0)
- Per-session snapshot, taken once at registration.
pkg/vmcp/server/serve_handlers.go→injectCoreSessionCapabilities→coreSessionToolscallss.core.ListTools(...)only when the session is registered, and installs the result viasetSessionToolsDirect. In optimizer modeserveSessionToolsbuilds thefind_tool/call_toolmeta-tools over that same one-time snapshot. - Aggregation has no downstream notification.
pkg/vmcp/aggregator/default_aggregator.go(AggregateCapabilities) andpkg/vmcp/aggregator/caching_aggregator.gocompute/refresh the merged set, but neither notifies sessions nor the server of a change. - Health monitor detects transitions but doesn't fan out.
pkg/vmcp/health/monitor.gotracks status (statusTracker,UpdateBackends,StatusProvider/Reporterinterfaces) and logsstarting health monitoring for new backend, but there is no callback/subscription that re-injects live sessions or triggers a client notification on a state change. - No
tools/list_changedis ever emitted on the vMCP path. The method is parsed (pkg/mcp/parser.go) and the primitive exists (pkg/transport/bridge.go→SendNotificationToAllClients), but it is only used by the single-server proxy bridge, not wired into the vMCP aggregation/session layer.
Net: health monitor → re-aggregate is a dead end w.r.t. connected sessions.
Proposal
On a backend-set change (health transition, or group add/remove), propagate to live sessions:
- Passthrough mode (optimizer off): update each connected session's advertised tools (the go-sdk supports per-session
AddSessionTools/SetSessionTools/DeleteSessionTools) and emitnotifications/tools/list_changedto all clients via theMCPServer(the same primitive the bridge already uses). Requires the server to advertisetools.listChanged: true. - Optimizer mode (optimizer on): rebuild the optimizer catalog/index that
find_tool/call_toolresolve against so existing sessions resolve the new backend without reconnect (either refresh the per-session optimizer state, or have the optimizer resolve against the live core catalog on each call / on a change signal). - Wiring: give
health.Monitora change-callback / subscription (it already centralizes status viastatusTracker); the core/server subscribes, re-aggregates, and performs (1)/(2). Debounce to the existingstatusReportingIntervalso a flapping backend doesn't storm sessions.
Notes
- Gate strictly on the server advertising
tools.listChanged; no behavior change otherwise. - The debounce already present for health (
healthCheckInterval×unhealthyThreshold) keeps this from firing on transient blips. - Happy to help / open a PR — a sketch of the integration points is written up alongside this.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.