Skip to content

Commit 3d3eff9

Browse files
JAORMXclaude
andcommitted
Support MCP servers with partial capabilities in vmcp
The vmcp was rejecting backends that don't implement all three MCP capabilities (tools, resources, prompts). This violated the MCP specification, which explicitly makes all capabilities optional. The oci-registry MCP server only implements tools, causing vmcp to fail during aggregation when it tried to unconditionally query resources and prompts. Changes: - Query server capabilities during MCP initialization handshake - Conditionally query only advertised capabilities - Return empty results for unsupported capabilities - Add tests for backends with partial capability support This allows vmcp to successfully aggregate backends that implement any subset of tools, resources, and prompts, as intended by the MCP specification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent aa48db2 commit 3d3eff9

File tree

2 files changed

+114
-20
lines changed

2 files changed

+114
-20
lines changed

pkg/vmcp/client/client.go

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,15 @@ func defaultClientFactory(ctx context.Context, target *vmcp.BackendTarget) (*cli
118118
return nil, fmt.Errorf("failed to start client connection: %w", err)
119119
}
120120

121-
// Initialize the MCP connection
122-
if err := initializeClient(ctx, c); err != nil {
123-
_ = c.Close()
124-
return nil, fmt.Errorf("failed to initialize MCP connection: %w", err)
125-
}
126-
121+
// Note: Initialization is deferred to the caller (e.g., ListCapabilities)
122+
// so that ServerCapabilities can be captured and used for conditional querying
127123
return c, nil
128124
}
129125

130-
// initializeClient performs MCP protocol initialization handshake.
131-
func initializeClient(ctx context.Context, c *client.Client) error {
132-
_, err := c.Initialize(ctx, mcp.InitializeRequest{
126+
// initializeClient performs MCP protocol initialization handshake and returns server capabilities.
127+
// This allows the caller to determine which optional features the server supports.
128+
func initializeClient(ctx context.Context, c *client.Client) (*mcp.ServerCapabilities, error) {
129+
result, err := c.Initialize(ctx, mcp.InitializeRequest{
133130
Params: mcp.InitializeParams{
134131
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
135132
ClientInfo: mcp.Implementation{
@@ -146,37 +143,88 @@ func initializeClient(ctx context.Context, c *client.Client) error {
146143
},
147144
},
148145
})
149-
return err
146+
if err != nil {
147+
return nil, err
148+
}
149+
return &result.Capabilities, nil
150+
}
151+
152+
// queryTools queries tools from a backend if the server advertises tool support.
153+
func queryTools(ctx context.Context, c *client.Client, supported bool, backendID string) (*mcp.ListToolsResult, error) {
154+
if supported {
155+
result, err := c.ListTools(ctx, mcp.ListToolsRequest{})
156+
if err != nil {
157+
return nil, fmt.Errorf("failed to list tools from backend %s: %w", backendID, err)
158+
}
159+
return result, nil
160+
}
161+
logger.Debugf("Backend %s does not advertise tools capability, skipping tools query", backendID)
162+
return &mcp.ListToolsResult{Tools: []mcp.Tool{}}, nil
163+
}
164+
165+
// queryResources queries resources from a backend if the server advertises resource support.
166+
func queryResources(ctx context.Context, c *client.Client, supported bool, backendID string) (*mcp.ListResourcesResult, error) {
167+
if supported {
168+
result, err := c.ListResources(ctx, mcp.ListResourcesRequest{})
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to list resources from backend %s: %w", backendID, err)
171+
}
172+
return result, nil
173+
}
174+
logger.Debugf("Backend %s does not advertise resources capability, skipping resources query", backendID)
175+
return &mcp.ListResourcesResult{Resources: []mcp.Resource{}}, nil
176+
}
177+
178+
// queryPrompts queries prompts from a backend if the server advertises prompt support.
179+
func queryPrompts(ctx context.Context, c *client.Client, supported bool, backendID string) (*mcp.ListPromptsResult, error) {
180+
if supported {
181+
result, err := c.ListPrompts(ctx, mcp.ListPromptsRequest{})
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to list prompts from backend %s: %w", backendID, err)
184+
}
185+
return result, nil
186+
}
187+
logger.Debugf("Backend %s does not advertise prompts capability, skipping prompts query", backendID)
188+
return &mcp.ListPromptsResult{Prompts: []mcp.Prompt{}}, nil
150189
}
151190

152191
// ListCapabilities queries a backend for its MCP capabilities.
153192
// Returns tools, resources, and prompts exposed by the backend.
193+
// Only queries capabilities that the server advertises during initialization.
154194
func (h *httpBackendClient) ListCapabilities(ctx context.Context, target *vmcp.BackendTarget) (*vmcp.CapabilityList, error) {
155195
logger.Debugf("Querying capabilities from backend %s (%s)", target.WorkloadName, target.BaseURL)
156196

157-
// Create a client for this backend
197+
// Create a client for this backend (not yet initialized)
158198
c, err := h.clientFactory(ctx, target)
159199
if err != nil {
160200
return nil, fmt.Errorf("failed to create client for backend %s: %w", target.WorkloadID, err)
161201
}
162202
defer c.Close()
163203

164-
// Query tools
165-
toolsResp, err := c.ListTools(ctx, mcp.ListToolsRequest{})
204+
// Initialize the client and get server capabilities
205+
serverCaps, err := initializeClient(ctx, c)
166206
if err != nil {
167-
return nil, fmt.Errorf("failed to list tools from backend %s: %w", target.WorkloadID, err)
207+
return nil, fmt.Errorf("failed to initialize client for backend %s: %w", target.WorkloadID, err)
168208
}
169209

170-
// Query resources
171-
resourcesResp, err := c.ListResources(ctx, mcp.ListResourcesRequest{})
210+
logger.Debugf("Backend %s capabilities: tools=%v, resources=%v, prompts=%v",
211+
target.WorkloadID, serverCaps.Tools != nil, serverCaps.Resources != nil, serverCaps.Prompts != nil)
212+
213+
// Query each capability type based on server advertisement
214+
// Check for nil BEFORE passing to functions to avoid interface{} nil pointer issues
215+
toolsResp, err := queryTools(ctx, c, serverCaps.Tools != nil, target.WorkloadID)
172216
if err != nil {
173-
return nil, fmt.Errorf("failed to list resources from backend %s: %w", target.WorkloadID, err)
217+
return nil, err
174218
}
175219

176-
// Query prompts
177-
promptsResp, err := c.ListPrompts(ctx, mcp.ListPromptsRequest{})
220+
resourcesResp, err := queryResources(ctx, c, serverCaps.Resources != nil, target.WorkloadID)
178221
if err != nil {
179-
return nil, fmt.Errorf("failed to list prompts from backend %s: %w", target.WorkloadID, err)
222+
return nil, err
223+
}
224+
225+
promptsResp, err := queryPrompts(ctx, c, serverCaps.Prompts != nil, target.WorkloadID)
226+
if err != nil {
227+
return nil, err
180228
}
181229

182230
// Convert MCP types to vmcp types
@@ -266,6 +314,11 @@ func (h *httpBackendClient) CallTool(
266314
}
267315
defer c.Close()
268316

317+
// Initialize the client
318+
if _, err := initializeClient(ctx, c); err != nil {
319+
return nil, fmt.Errorf("failed to initialize client for backend %s: %w", target.WorkloadID, err)
320+
}
321+
269322
// Call the tool
270323
result, err := c.CallTool(ctx, mcp.CallToolRequest{
271324
Params: mcp.CallToolParams{
@@ -337,6 +390,11 @@ func (h *httpBackendClient) ReadResource(ctx context.Context, target *vmcp.Backe
337390
}
338391
defer c.Close()
339392

393+
// Initialize the client
394+
if _, err := initializeClient(ctx, c); err != nil {
395+
return nil, fmt.Errorf("failed to initialize client for backend %s: %w", target.WorkloadID, err)
396+
}
397+
340398
// Read the resource
341399
result, err := c.ReadResource(ctx, mcp.ReadResourceRequest{
342400
Params: mcp.ReadResourceParams{
@@ -387,6 +445,11 @@ func (h *httpBackendClient) GetPrompt(
387445
}
388446
defer c.Close()
389447

448+
// Initialize the client
449+
if _, err := initializeClient(ctx, c); err != nil {
450+
return "", fmt.Errorf("failed to initialize client for backend %s: %w", target.WorkloadID, err)
451+
}
452+
390453
// Get the prompt
391454
// Convert map[string]any to map[string]string
392455
stringArgs := make(map[string]string)

pkg/vmcp/client/client_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ func TestHTTPBackendClient_ListCapabilities_WithMockFactory(t *testing.T) {
4343
})
4444
}
4545

46+
func TestQueryHelpers_PartialCapabilities(t *testing.T) {
47+
t.Parallel()
48+
49+
t.Run("queryTools with unsupported capability returns empty slice", func(t *testing.T) {
50+
t.Parallel()
51+
52+
result, err := queryTools(context.Background(), nil, false, "test-backend")
53+
require.NoError(t, err)
54+
assert.NotNil(t, result)
55+
assert.Empty(t, result.Tools)
56+
})
57+
58+
t.Run("queryResources with unsupported capability returns empty slice", func(t *testing.T) {
59+
t.Parallel()
60+
61+
result, err := queryResources(context.Background(), nil, false, "test-backend")
62+
require.NoError(t, err)
63+
assert.NotNil(t, result)
64+
assert.Empty(t, result.Resources)
65+
})
66+
67+
t.Run("queryPrompts with unsupported capability returns empty slice", func(t *testing.T) {
68+
t.Parallel()
69+
70+
result, err := queryPrompts(context.Background(), nil, false, "test-backend")
71+
require.NoError(t, err)
72+
assert.NotNil(t, result)
73+
assert.Empty(t, result.Prompts)
74+
})
75+
}
76+
4677
func TestDefaultClientFactory_UnsupportedTransport(t *testing.T) {
4778
t.Parallel()
4879

0 commit comments

Comments
 (0)