Add ListBackends/LookupBackend to the vMCP core interface#5763
Conversation
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>
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
tgrunnagle
left a comment
There was a problem hiding this comment.
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'sListBackendsdoc should distinguish "zero-capability" absence from "all-denied" absence (finding #6, inline below).core_vmcp.go'saggregateOverAllcomment should state the all-unreachable/empty-set failure boundary explicitly rather than claiming unconditional graceful degradation (tied to finding #2).
Generated with Claude Code
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>
Summary
Embedders consuming vMCP through the
pkg/vmcp/coreVMCPinterface have no backend-level view — the only way to approximate one is to reverse-engineer the backend set fromListTools, 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.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.identitymay 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 seamListTools/CallToolenforce) at least one of its discovered capabilities. Aggregates over the full registry, runs admission, unions the surviving capabilities'BackendIDs.LookupBackend(ctx, identity, backendID)— resolves a single id in the authorized view; returnsvmcp.ErrNotFoundfor an unknown, out-of-group, or unauthorized id (mirrorsLookupTool).ListBackendsdoes not applyfilterHealthyBackends, so a degraded/unhealthy backend still appears (badged viaHealthStatus) rather than a transient blip removing a connector from a catalog.aggregateOverAllso 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
Test plan
task test) — newcore_backends_test.gocovers 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, andLookupBackendfound/not-found (unknown + unauthorized). Full./pkg/vmcp/...suite passes.task lint-fix) — 0 issues on the changed packages.Special notes for reviewers
LookupBackendreturnsErrNotFoundfor it, consistently). Backends that need enabling/auth are surfaced via the admin view instead. Flagged in the issue and pinned by a test.filterUnauthorized=trueaggregates 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 assertsAggregateCapabilitiesreceives the unfiltered backend set.codemode/ratelimitdecorators embedcore.VMCP, so they auto-promote the new methods; the two hand-rolledservertest doubles (stubVMCP,fakeCore) get no-op implementations.Generated with Claude Code