Skip to content

Add ListBackends/LookupBackend to the vMCP core interface#5763

Merged
ChrisJBurns merged 3 commits into
mainfrom
cburns/vmcp-core-list-backends
Jul 8, 2026
Merged

Add ListBackends/LookupBackend to the vMCP core interface#5763
ChrisJBurns merged 3 commits into
mainfrom
cburns/vmcp-core-list-backends

Conversation

@ChrisJBurns

Copy link
Copy Markdown
Collaborator

Summary

Embedders consuming vMCP through the pkg/vmcp/core VMCP interface have no backend-level view — the only way to approximate one is to reverse-engineer the backend set from ListTools, which is lossy (a backend advertising zero visible tools disappears) and the wrong shape (the surface is about backends, not tools). This adds first-class backend enumeration, the interface extension anticipated by the THV-0076 vMCP Core Interface RFC.

  • Add ListBackends(ctx, identity, filterUnauthorized bool) — one method, two views:
    • false (admin): all group-scoped backends straight from the registry — no aggregation, no admission, no health filter. identity may be nil; authorizing the caller for this view is the API layer's job (an admin-gated endpoint).
    • true (user-discovery): a backend appears iff the identity is admitted (the same seam ListTools/CallTool enforce) at least one of its discovered capabilities. Aggregates over the full registry, runs admission, unions the surviving capabilities' BackendIDs.
  • Add LookupBackend(ctx, identity, backendID) — resolves a single id in the authorized view; returns vmcp.ErrNotFound for an unknown, out-of-group, or unauthorized id (mirrors LookupTool).
  • Health is a status, not a visibility filter: ListBackends does not apply filterHealthyBackends, so a degraded/unhealthy backend still appears (badged via HealthStatus) rather than a transient blip removing a connector from a catalog.
  • Extract aggregateOverAll so the health-filtered data path and the full-registry backend derivation share one aggregation error-wrap.

There is no backend-level authorization entity, so the authorized view is a derived rule over per-capability admission — keeping list and call from drifting. Per-user enable/disable state is intentionally left to a decorator on top of the core, not a parameter here.

Closes #5741

Type of change

  • New feature

Test plan

  • Unit tests (task test) — new core_backends_test.go covers both modes, the tools/resources/prompts union, the "aggregate the full registry (no health filter)" guarantee, the zero-capability corner case, fail-closed on aggregation error, and LookupBackend found/not-found (unknown + unauthorized). Full ./pkg/vmcp/... suite passes.
  • Linting (task lint-fix) — 0 issues on the changed packages.

Special notes for reviewers

  • Decided corner case: in the authorized view, a backend with genuinely zero discovered capabilities is vacuously hidden by the ≥1-admitted-capability rule (LookupBackend returns ErrNotFound for it, consistently). Backends that need enabling/auth are surfaced via the admin view instead. Flagged in the issue and pinned by a test.
  • filterUnauthorized=true aggregates over the full registry, including unhealthy backends — a deliberate choice to honor "health is not a visibility filter." Unreachable backends fail their live capability query and simply contribute nothing, so this adds latency (live calls that time out) on a discovery/catalog surface, not the hot request path; the caching aggregator's TTL absorbs repeats. The load-bearing test asserts AggregateCapabilities receives the unfiltered backend set.
  • Nil identity in authorized mode is treated as anonymous (consistent with the interface-wide "nil is anonymous" contract), not a hard error.
  • The codemode/ratelimit decorators embed core.VMCP, so they auto-promote the new methods; the two hand-rolled server test doubles (stubVMCP, fakeCore) get no-op implementations.
  • No MCP-protocol/transport surface is added — this is an embedder-facing Go API. No operator/CRD API surface touched.

Generated with Claude Code

Add backend-level enumeration to the pkg/vmcp/core VMCP interface so embedders
get a first-class backend view instead of reverse-engineering it (lossily) from
ListTools. One ListBackends method serves two surfaces via a filterUnauthorized
bool, plus LookupBackend for a single authorized resolve.

  - false (admin): all group-scoped backends from the registry, no aggregation,
    no admission, no health filter. identity may be nil; caller-authz is the API
    layer's concern.
  - true (user-discovery): a backend appears iff the identity is admitted at
    least one of its discovered capabilities. Aggregates over the full registry
    (health is a status, not a visibility filter), runs the existing admission
    seam, and unions the surviving capabilities' BackendIDs. A zero-capability
    backend is vacuously hidden.

LookupBackend resolves in the authorized view and returns vmcp.ErrNotFound for an
unknown, out-of-group, or unauthorized id, mirroring LookupTool.

Extract aggregateOverAll so the health-filtered data path and the full-registry
backend derivation share one aggregation error-wrap. The codemode/ratelimit
decorators embed core.VMCP and auto-promote the new methods; the two hand-rolled
server test doubles (stubVMCP, fakeCore) gain no-op implementations.

Closes #5741

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Jul 8, 2026
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 8, 2026
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.39535% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.78%. Comparing base (34a2169) to head (87b53fd).

Files with missing lines Patch % Lines
pkg/vmcp/core/core_vmcp.go 81.39% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5763   +/-   ##
=======================================
  Coverage   70.77%   70.78%           
=======================================
  Files         683      683           
  Lines       69194    69236   +42     
=======================================
+ Hits        48975    49011   +36     
- Misses      16621    16622    +1     
- Partials     3598     3603    +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tgrunnagle
tgrunnagle previously approved these changes Jul 8, 2026

@tgrunnagle tgrunnagle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-Agent Consensus Review

Agents consulted: rev-arch, rev-errhandling, rev-security, rev-tests, rev-friendliness, rev-general

Consensus Summary

# Finding Consensus Severity Action
1 Admin-mode ListBackends relies on an unenforced slice freshness/mutability contract 9/10 MEDIUM Fix
2 Authorized ListBackends/LookupBackend fail closed with a hard error on an empty/fully-unreachable backend set 8/10 MEDIUM Fix
3 Admin view has no code-level authorization guardrail; unfiltered view is the bool zero-value 8/10 MEDIUM Fix
4 Authorized-mode nil-identity (anonymous) semantic is untested 8/10 MEDIUM Fix
5 aggregateOverAll is misleadingly named for its health-filtered caller 8/10 LOW Fix
6 Doc comment conflates "zero-capability backend" with "all-denied backend" 7/10 LOW Fix
7 Private helper authorizedBackends breaks the file's public/private ordering 7/10 LOW Fix
8 Composite-tool exclusion from the authorized derivation is untested 7/10 LOW Fix

Overall

This is a well-scoped, single-package interface extension adding ListBackends/LookupBackend to pkg/vmcp/core.VMCP, closing #5741. The approach mirrors the established ListTools/LookupTool pattern exactly — deriving an "authorized" view from the existing per-capability Cedar admission seam, plus a raw "admin" view whose caller-authorization is deferred to the API layer — avoiding any list/call authorization drift. Test coverage is thorough and specifically pins the two behaviors the linked issue called out as decisions needing a test.

The findings below are edge cases and hardening gaps rather than defects in the mainline path: the authorized path returns a hard error (verified against pkg/vmcp/aggregator/default_aggregator.go:184-185) instead of an empty result when the backend set is empty or fully unreachable; the admin view's "no authorization" default is reached via a bool's zero value with no code-level guardrail; and the documented "fresh, non-nil, mutable" slice contract for the admin view is honored only by coincidence of the current registry implementations (verified against pkg/vmcp/registry.go:39-41) rather than enforced by core itself. None of these block the primary code path, which is sound and well-tested.

Documentation

  • core.go's ListBackends doc should distinguish "zero-capability" absence from "all-denied" absence (finding #6, inline below).
  • core_vmcp.go's aggregateOverAll comment should state the all-unreachable/empty-set failure boundary explicitly rather than claiming unconditional graceful degradation (tied to finding #2).

Generated with Claude Code

Comment thread pkg/vmcp/core/core_vmcp.go
Comment thread pkg/vmcp/core/core_vmcp.go Outdated
Comment thread pkg/vmcp/core/core_vmcp.go
Comment thread pkg/vmcp/core/core_backends_test.go
Comment thread pkg/vmcp/core/core_vmcp.go Outdated
Comment thread pkg/vmcp/core/core.go Outdated
Comment thread pkg/vmcp/core/core_vmcp.go Outdated
Comment thread pkg/vmcp/core/core_backends_test.go
Follow-up to review on #5763:

  - ListBackends(authorized) now returns an empty (non-nil) slice for an empty
    group instead of diving into aggregation and surfacing the aggregator's
    "no backends returned capabilities" sentinel as an error. A non-empty group
    that is entirely unreachable still errors (consistent with ListTools) — that
    is a live failure, not an empty answer. Fixes the empty-group divergence
    between the admin and authorized views.
  - Add tests: empty group returns empty in both modes (no aggregation),
    nil identity in authorized mode is anonymous (not an error), and a
    backendless (composite-shaped) tool does not surface a backend.
  - Clarify the ListBackends doc so a zero-capability (tool-less) backend's
    absence is not read as a per-identity authorization-denial signal.
  - Rename aggregateOverAll -> aggregateBackends (the "All" suffix was wrong for
    its health-filtered caller) and move private authorizedBackends to the file's
    bottom half per go-style ordering.
  - Tighten BackendRegistry.List's godoc to contractually promise the fresh,
    non-nil, mutable slice ListBackends relies on (both impls already do this).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 8, 2026
@ChrisJBurns ChrisJBurns merged commit 709c918 into main Jul 8, 2026
59 of 61 checks passed
@ChrisJBurns ChrisJBurns deleted the cburns/vmcp-core-list-backends branch July 8, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vmcp core: add ListBackends / LookupBackend to the VMCP interface (THV-0076 extension)

2 participants