Skip to content

vMCP: backend health/set changes are not propagated to live sessions (stale tool catalog until reconnect; no tools/list_changed) #5786

Description

@premctl

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

  1. Run a vMCP fronting ≥2 backends; connect a client (session A).
  2. 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.
  3. From session A: the new backend's tools are not callable (call_tooltool not found) and don't appear in discovery. A freshly connected session B sees them.
  4. 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.goinjectCoreSessionCapabilitiescoreSessionTools 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.goSendNotificationToAllClients), 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:

  1. 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.
  2. 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).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fields

    No fields configured for Task 📋.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions