Skip to content

Commit 4ee67ef

Browse files
JAORMXclaude
andcommitted
Add BackendRegistry interface for thread-safe backend access
Introduce BackendRegistry as a shared kernel component in pkg/vmcp to provide thread-safe access to discovered backends across bounded contexts (aggregator, router, health monitoring). This implementation addresses the requirement to store full backend information in routing tables, enabling the router to forward requests without additional backend lookups. Key changes: - Create pkg/vmcp/registry.go with BackendRegistry interface * Get(ctx, backendID) - retrieve backend by ID * List(ctx) - get all backends * Count() - efficient backend count - Implement immutableRegistry for Phase 1 * Thread-safe for concurrent reads * Built once from discovered backends, never modified * Suitable for static backend lists in Phase 1 - Add BackendToTarget() helper function * Converts Backend to BackendTarget with full information * Populates WorkloadID, WorkloadName, BaseURL, TransportType, HealthStatus, AuthStrategy, and AuthMetadata - Update aggregator to use BackendRegistry * Modify Aggregator.MergeCapabilities() to accept registry parameter * Refactor AggregateCapabilities() to create registry from backends * Populate routing table with complete BackendTarget information - Enhance test coverage * Update TestDefaultAggregator_MergeCapabilities with registry * Add assertions verifying full BackendTarget population in routing table * Generate mocks for BackendRegistry interface Design rationale: Following DDD principles, BackendRegistry is placed in pkg/vmcp root as a shared kernel component (like types.go and errors.go) to: - Avoid circular dependencies between aggregator and router - Provide single source of truth for backend information - Enable reuse across multiple bounded contexts - Support future evolution to mutable registry with health monitoring The routing table now contains complete backend information needed for request forwarding, eliminating the need for additional lookups during routing (required for Issue #147). Phase 1 uses immutableRegistry (read-only). Future phases can swap to mutexRegistry for dynamic backend updates without API changes. Related to Issue #148 (vMCP Capability Discovery & Querying) Prepares for Issue #147 (Request Routing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 30bdce8 commit 4ee67ef

File tree

6 files changed

+308
-23
lines changed

6 files changed

+308
-23
lines changed

pkg/vmcp/aggregator/aggregator.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ type Aggregator interface {
4545
ResolveConflicts(ctx context.Context, capabilities map[string]*BackendCapabilities) (*ResolvedCapabilities, error)
4646

4747
// MergeCapabilities creates the final unified capability view and routing table.
48-
MergeCapabilities(ctx context.Context, resolved *ResolvedCapabilities) (*AggregatedCapabilities, error)
48+
// Uses the backend registry to populate full BackendTarget information for routing.
49+
MergeCapabilities(
50+
ctx context.Context,
51+
resolved *ResolvedCapabilities,
52+
registry vmcp.BackendRegistry,
53+
) (*AggregatedCapabilities, error)
4954

5055
// AggregateCapabilities is a convenience method that performs the full aggregation pipeline:
5156
// 1. Query all backends

pkg/vmcp/aggregator/default_aggregator.go

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,11 @@ func (*defaultAggregator) ResolveConflicts(
172172
}
173173

174174
// MergeCapabilities creates the final unified capability view and routing table.
175+
// Uses the backend registry to populate full BackendTarget information for routing.
175176
func (*defaultAggregator) MergeCapabilities(
176-
_ context.Context,
177+
ctx context.Context,
177178
resolved *ResolvedCapabilities,
179+
registry vmcp.BackendRegistry,
178180
) (*AggregatedCapabilities, error) {
179181
logger.Debugf("Merging capabilities into final view")
180182

@@ -195,25 +197,45 @@ func (*defaultAggregator) MergeCapabilities(
195197
BackendID: resolvedTool.BackendID,
196198
})
197199

198-
// Add to routing table (we'll need to look up the backend target)
199-
// For now, we'll create a minimal target with just the backend ID
200-
// In a full implementation, we'd need to store backend targets during discovery
201-
routingTable.Tools[resolvedTool.ResolvedName] = &vmcp.BackendTarget{
202-
WorkloadID: resolvedTool.BackendID,
200+
// Look up full backend information from registry
201+
backend := registry.Get(ctx, resolvedTool.BackendID)
202+
if backend == nil {
203+
logger.Warnf("Backend %s not found in registry for tool %s, creating minimal target",
204+
resolvedTool.BackendID, resolvedTool.ResolvedName)
205+
routingTable.Tools[resolvedTool.ResolvedName] = &vmcp.BackendTarget{
206+
WorkloadID: resolvedTool.BackendID,
207+
}
208+
} else {
209+
// Use the backendToTarget helper from registry package
210+
routingTable.Tools[resolvedTool.ResolvedName] = vmcp.BackendToTarget(backend)
203211
}
204212
}
205213

206214
// Add resources to routing table
207215
for _, resource := range resolved.Resources {
208-
routingTable.Resources[resource.URI] = &vmcp.BackendTarget{
209-
WorkloadID: resource.BackendID,
216+
backend := registry.Get(ctx, resource.BackendID)
217+
if backend == nil {
218+
logger.Warnf("Backend %s not found in registry for resource %s, creating minimal target",
219+
resource.BackendID, resource.URI)
220+
routingTable.Resources[resource.URI] = &vmcp.BackendTarget{
221+
WorkloadID: resource.BackendID,
222+
}
223+
} else {
224+
routingTable.Resources[resource.URI] = vmcp.BackendToTarget(backend)
210225
}
211226
}
212227

213228
// Add prompts to routing table
214229
for _, prompt := range resolved.Prompts {
215-
routingTable.Prompts[prompt.Name] = &vmcp.BackendTarget{
216-
WorkloadID: prompt.BackendID,
230+
backend := registry.Get(ctx, prompt.BackendID)
231+
if backend == nil {
232+
logger.Warnf("Backend %s not found in registry for prompt %s, creating minimal target",
233+
prompt.BackendID, prompt.Name)
234+
routingTable.Prompts[prompt.Name] = &vmcp.BackendTarget{
235+
WorkloadID: prompt.BackendID,
236+
}
237+
} else {
238+
routingTable.Prompts[prompt.Name] = vmcp.BackendToTarget(backend)
217239
}
218240
}
219241

@@ -241,26 +263,31 @@ func (*defaultAggregator) MergeCapabilities(
241263
}
242264

243265
// AggregateCapabilities is a convenience method that performs the full aggregation pipeline:
244-
// 1. Query all backends
245-
// 2. Resolve conflicts
246-
// 3. Merge into final view
266+
// 1. Create backend registry
267+
// 2. Query all backends
268+
// 3. Resolve conflicts
269+
// 4. Merge into final view with full backend information
247270
func (a *defaultAggregator) AggregateCapabilities(ctx context.Context, backends []vmcp.Backend) (*AggregatedCapabilities, error) {
248271
logger.Infof("Starting capability aggregation for %d backends", len(backends))
249272

250-
// Step 1: Query all backends
273+
// Step 1: Create registry from discovered backends
274+
registry := vmcp.NewImmutableRegistry(backends)
275+
logger.Debugf("Created backend registry with %d backends", registry.Count())
276+
277+
// Step 2: Query all backends
251278
capabilities, err := a.QueryAllCapabilities(ctx, backends)
252279
if err != nil {
253280
return nil, fmt.Errorf("failed to query backends: %w", err)
254281
}
255282

256-
// Step 2: Resolve conflicts
283+
// Step 3: Resolve conflicts
257284
resolved, err := a.ResolveConflicts(ctx, capabilities)
258285
if err != nil {
259286
return nil, fmt.Errorf("failed to resolve conflicts: %w", err)
260287
}
261288

262-
// Step 3: Merge into final view
263-
aggregated, err := a.MergeCapabilities(ctx, resolved)
289+
// Step 4: Merge into final view with full backend information
290+
aggregated, err := a.MergeCapabilities(ctx, resolved, registry)
264291
if err != nil {
265292
return nil, fmt.Errorf("failed to merge capabilities: %w", err)
266293
}

pkg/vmcp/aggregator/default_aggregator_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,27 @@ func TestDefaultAggregator_MergeCapabilities(t *testing.T) {
313313
SupportsSampling: false,
314314
}
315315

316+
// Create registry with test backends
317+
backends := []vmcp.Backend{
318+
{
319+
ID: "backend1",
320+
Name: "Backend 1",
321+
BaseURL: "http://backend1:8080",
322+
TransportType: "streamable-http",
323+
HealthStatus: vmcp.BackendHealthy,
324+
},
325+
{
326+
ID: "backend2",
327+
Name: "Backend 2",
328+
BaseURL: "http://backend2:8080",
329+
TransportType: "sse",
330+
HealthStatus: vmcp.BackendHealthy,
331+
},
332+
}
333+
registry := vmcp.NewImmutableRegistry(backends)
334+
316335
agg := NewDefaultAggregator(nil)
317-
aggregated, err := agg.MergeCapabilities(context.Background(), resolved)
336+
aggregated, err := agg.MergeCapabilities(context.Background(), resolved, registry)
318337

319338
require.NoError(t, err)
320339
assert.Len(t, aggregated.Tools, 2)
@@ -330,6 +349,22 @@ func TestDefaultAggregator_MergeCapabilities(t *testing.T) {
330349
assert.Contains(t, aggregated.RoutingTable.Resources, "test://resource1")
331350
assert.Contains(t, aggregated.RoutingTable.Prompts, "prompt1")
332351

352+
// Verify routing table has full backend information
353+
tool1Target := aggregated.RoutingTable.Tools["tool1"]
354+
assert.NotNil(t, tool1Target)
355+
assert.Equal(t, "backend1", tool1Target.WorkloadID)
356+
assert.Equal(t, "Backend 1", tool1Target.WorkloadName)
357+
assert.Equal(t, "http://backend1:8080", tool1Target.BaseURL)
358+
assert.Equal(t, "streamable-http", tool1Target.TransportType)
359+
assert.Equal(t, vmcp.BackendHealthy, tool1Target.HealthStatus)
360+
361+
tool2Target := aggregated.RoutingTable.Tools["tool2"]
362+
assert.NotNil(t, tool2Target)
363+
assert.Equal(t, "backend2", tool2Target.WorkloadID)
364+
assert.Equal(t, "Backend 2", tool2Target.WorkloadName)
365+
assert.Equal(t, "http://backend2:8080", tool2Target.BaseURL)
366+
assert.Equal(t, "sse", tool2Target.TransportType)
367+
333368
// Check metadata
334369
assert.Equal(t, 2, aggregated.Metadata.ToolCount)
335370
assert.Equal(t, 1, aggregated.Metadata.ResourceCount)

pkg/vmcp/aggregator/mocks/mock_interfaces.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/vmcp/mocks/mock_backend_registry.go

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)