-
Notifications
You must be signed in to change notification settings - Fork 146
Description
vMCP: Implement lazy per-user capability discovery (MVP)
Problem
The vMCP server discovers backend MCP server capabilities during startup in cmd/vmcp/app/commands.go:290. This calls aggregateCapabilities() which queries each backend's tools, resources, and prompts before any user has connected.
This breaks for backends requiring user-specific authentication. Token exchange strategies need the actual user's token to authorize capability queries. At startup there is no user context, so these backends reject the discovery requests.
Solution
Move capability discovery from startup to request time. Create a DiscoveryManager component that performs aggregation when a user makes their first request. At that point the user has authenticated and their identity is available via auth.IdentityFromContext().
This MVP implementation performs discovery on every request without caching. Caching will be added in a follow-up issue.
Architecture
Create a new pkg/vmcp/discovery/ package with three components:
DiscoveryManager (manager.go): Wraps the existing Aggregator and performs discovery with user context.
type Manager interface {
Discover(ctx context.Context, backends []workloads.Reference) (*aggregator.AggregatedCapabilities, error)
}
type DefaultManager struct {
aggregator aggregator.Aggregator
}
func (m *DefaultManager) Discover(ctx context.Context, backends []workloads.Reference) (*aggregator.AggregatedCapabilities, error) {
return m.aggregator.AggregateCapabilities(ctx, backends)
}Context helpers (context.go): Store and retrieve discovered capabilities in request context.
func WithDiscoveredCapabilities(ctx context.Context, caps *aggregator.AggregatedCapabilities) context.Context
func DiscoveredCapabilitiesFromContext(ctx context.Context) (*aggregator.AggregatedCapabilities, bool)Middleware (middleware.go): HTTP middleware that performs discovery per request and attaches capabilities to context.
func Middleware(manager Manager, backends []workloads.Reference, logger *slog.Logger) func(http.Handler) http.HandlerThe middleware extracts user identity from context (already set by auth middleware), calls manager.Discover(), and attaches the result to context for handlers to use.
Integration Changes
cmd/vmcp/app/commands.go - Remove startup aggregation, create DiscoveryManager:
// Remove this:
caps, err := aggregateCapabilities(cmd.Context(), backendRefs, agg)
// Add this:
discoveryManager := discovery.NewManager(agg)
srv, err := vmcp.NewServer(config, backends, discoveryManager, logger)pkg/vmcp/server/server.go - Accept DiscoveryManager instead of capabilities:
type Server struct {
// Remove: preAggregatedCaps
// Add:
discoveryMgr discovery.Manager
}
func NewServer(config *config.VirtualMCPServerConfig, backends []workloads.Reference, discoveryMgr discovery.Manager, logger *slog.Logger) (*Server, error)In Start() method, add discovery middleware after auth middleware:
handler = discovery.Middleware(s.discoveryMgr, s.backends, s.logger)(handler)Update MCP handlers to get capabilities from context:
caps, ok := discovery.DiscoveredCapabilitiesFromContext(r.Context())
if !ok {
// return error
}pkg/vmcp/router/router.go - If Router stores capabilities at construction, refactor to accept them per-request. Change method signatures from:
func (r *Router) ListTools(ctx context.Context) ([]types.Tool, error)to:
func (r *Router) ListTools(ctx context.Context, caps *aggregator.AggregatedCapabilities) ([]types.Tool, error)Implementation Notes
The existing aggregator.Aggregator already extracts user identity from context and passes it to the backend client. The auth RoundTripper in the backend client applies the appropriate auth strategy (including token exchange). No changes needed to the aggregation pipeline itself.
Middleware ordering matters: Auth → Discovery → Handlers. Discovery must run after auth middleware sets user identity in context.
If discovery fails, return HTTP 503 (Service Unavailable) rather than 500, signaling temporary backend unavailability.