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:M with 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 → coreSessionTools calls s.core.ListTools(...) only when the session is registered, and installs the result via setSessionToolsDirect. In optimizer mode serveSessionTools builds the find_tool/call_tool meta-tools over that same one-time snapshot.
- Aggregation has no downstream notification.
pkg/vmcp/aggregator/default_aggregator.go (AggregateCapabilities) and pkg/vmcp/aggregator/caching_aggregator.go compute/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.go tracks status (statusTracker, UpdateBackends, StatusProvider/Reporter interfaces) and logs starting 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_changed is 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 emit notifications/tools/list_changed to all clients via the MCPServer (the same primitive the bridge already uses). Requires the server to advertise tools.listChanged: true.
- Optimizer mode (optimizer on): rebuild the optimizer catalog/index that
find_tool/call_tool resolve 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.Monitor a change-callback / subscription (it already centralizes status via statusTracker); the core/server subscribes, re-aggregates, and performs (1)/(2). Debounce to the existing statusReportingInterval so 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.
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
capability aggregation complete backends:N tools:Mwith the new backend included.call_tool→tool not found) and don't appear in discovery. A freshly connected session B sees them.(In optimizer mode this is extra-confusing: the client's advertised tools are the stable
find_tool/call_toolmeta-tools, so even a client that honorstools/list_changedhas nothing new to re-fetch — the staleness lives inside the optimizer catalog thatfind_tool/call_toolresolve against.)Root cause (verified, v0.34.0)
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.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.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.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-aggregateis a dead end w.r.t. connected sessions.Proposal
On a backend-set change (health transition, or group add/remove), propagate to live sessions:
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.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).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
tools.listChanged; no behavior change otherwise.healthCheckInterval×unhealthyThreshold) keeps this from firing on transient blips.