Skip to content

vMCP dispatch aborts instead of falling back to Legacy on an inconclusive Modern probe #6000

Description

@jhrozek

Summary

dispatch in pkg/vmcp/client/client.go aborts the entire backend call when the Modern MCP revision probe returns an error, instead of falling back to the Legacy protocol path. A backend that is up and serving Legacy is therefore reported unhealthy whenever its Modern server/discover probe happens to land on a transient status.

Root cause

Merge 885e8b1 ("Handle per-backend MCP revision in vMCP (client-side dual protocol)") routed ListCapabilities through the new dispatch seam. On a revision-cache miss dispatch probes first, and treats any probe error as fatal:

rev, cached := h.cachedRevision(target.WorkloadID)
if !cached {
    probed, err := h.probeRevision(ctx, target)
    if err != nil {
        return wrapBackendError(err, target.WorkloadID, "probe revision")  // aborts; no Legacy fallback
    }
    rev = probed
}

probeRevision (client.go:1060) deliberately returns its error uncached for the two inconclusive classes, so as not to poison the revision cache:

  • errModernAuth — HTTP 401/403/407
  • errModernTransient — HTTP 408/429/5xx, mid-stream read failure, or any transport error (connection refused, timeout)

See pkg/vmcp/client/modern.go:273-281 for where those statuses become the sentinels.

An inconclusive probe is absence of evidence about the backend's revision. Turning it into a hard failure is wrong on its own terms — and it is a regression: before 885e8b1, ListCapabilities was a plain Legacy initialize + enumerate with no probe gate, so a backend that was serving simply succeeded.

Impact

ListCapabilities is both the health-check probe (pkg/vmcp/health/checker.go:99) and the startup aggregation call (pkg/vmcp/aggregator/default_aggregator.go:95). A single transient probe therefore marks a healthy backend unhealthy, which the existing readiness machinery amplifies:

  • pkg/vmcp/server/status_reporting.go + pkg/vmcp/health/monitor.go gate vMCP Ready on all backends being Routable, so one stuck probe delays readiness.
  • pkg/vmcp/core/core_vmcp.go (ListTools) filters unhealthy backends out of the tool list, producing an empty tools list.

(Those two are pre-existing amplifiers — see #3100 — not part of this defect.)

The concrete nondeterministic case: a ToolHive proxy is listening while the MCP container behind it is still starting, so it answers the probe with 502/503. The probe consumes that window and aborts before the Legacy initialize — which would have run after the container came up — is ever attempted.

Symptoms

vMCP e2e suites flake on bring-up:

  • test/e2e/thv-operator/virtualmcp/VirtualMCPServer Yardstick Base and Global ExcludeAllTools fail in [BeforeAll], either while aggregating tools or waiting on the Ready condition.
  • Core vmcp e2e bucket — failing-spec count swung 1 → 12 → 12 across three runs of an identical commit.

Fingerprint in a failing run's logs: "probe revision" plus backend_unavailable on a backend that is actually up.

Expected behaviour

An inconclusive probe should fall back to Legacy — the pre-dual-era default — without caching the revision, so that:

  • a backend reachable on the Legacy path is served rather than failed;
  • a transient outage never pins a revision;
  • a genuinely Modern backend is corrected by the next call's re-probe.

A truly-down backend still fails fast, because the Legacy initialize hits the same dead socket.

Scope note

This does not cover the dial tcp …: connect: connection refused signature — there the socket is genuinely dead and the Legacy path fails identically; the health monitor's retry interval absorbs it. The defect here is specifically the 5xx/408/429/auth window that 885e8b1 made fatal.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

needs-triageIssue needs initial triage by a maintainer

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions