Skip to content

Commit ccd1b8d

Browse files
yroblataskbotdmjb
authored
fix and test mcptoolconfig for vmcp (#2891)
* fix and test mcptoolconfig for vmcp Add proper reconciliation, dynamic updates and overrides support Add tests to validate it * changes from review * fixes from review --------- Co-authored-by: taskbot <taskbot@users.noreply.github.com> Co-authored-by: Don Browne <dmjb@users.noreply.github.com>
1 parent 7163527 commit ccd1b8d

File tree

4 files changed

+1163
-34
lines changed

4 files changed

+1163
-34
lines changed

cmd/thv-operator/controllers/virtualmcpserver_vmcpconfig_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ func newTestConverter(t *testing.T, resolver *oidcmocks.MockResolver) *vmcpconfi
5252
t.Helper()
5353
scheme := runtime.NewScheme()
5454
_ = mcpv1alpha1.AddToScheme(scheme)
55-
fakeClient := fake.NewClientBuilder().
56-
WithScheme(scheme).
57-
Build()
55+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
5856
converter, err := vmcpconfig.NewConverter(resolver, fakeClient)
5957
require.NoError(t, err)
6058
return converter

cmd/thv-operator/pkg/vmcpconfig/converter.go

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"time"
99

10+
"github.com/go-logr/logr"
1011
"k8s.io/apimachinery/pkg/api/errors"
1112
"k8s.io/apimachinery/pkg/runtime"
1213
"k8s.io/apimachinery/pkg/types"
@@ -40,7 +41,8 @@ type Converter struct {
4041
// NewConverter creates a new Converter instance.
4142
// oidcResolver is required and used to resolve OIDC configuration from various sources
4243
// (kubernetes, configMap, inline). Use a mock resolver in tests.
43-
// k8sClient is required and used to fetch referenced VirtualMCPCompositeToolDefinition resources.
44+
// k8sClient is required for resolving MCPToolConfig references and fetching referenced
45+
// VirtualMCPCompositeToolDefinition resources.
4446
// Returns an error if oidcResolver or k8sClient is nil.
4547
func NewConverter(oidcResolver oidc.Resolver, k8sClient client.Client) (*Converter, error) {
4648
if oidcResolver == nil {
@@ -86,7 +88,11 @@ func (c *Converter) Convert(
8688

8789
// Convert Aggregation - always set with defaults if not specified
8890
if vmcp.Spec.Aggregation != nil {
89-
config.Aggregation = c.convertAggregation(ctx, vmcp)
91+
agg, err := c.convertAggregation(ctx, vmcp)
92+
if err != nil {
93+
return nil, fmt.Errorf("failed to convert aggregation config: %w", err)
94+
}
95+
config.Aggregation = agg
9096
} else {
9197
// Provide default aggregation config with prefix conflict resolution
9298
config.Aggregation = &vmcpconfig.AggregationConfig{
@@ -260,12 +266,25 @@ func (*Converter) convertBackendAuthConfig(
260266
}
261267

262268
// convertAggregation converts AggregationConfig from CRD to vmcp config
263-
func (*Converter) convertAggregation(
264-
_ context.Context,
269+
func (c *Converter) convertAggregation(
270+
ctx context.Context,
265271
vmcp *mcpv1alpha1.VirtualMCPServer,
266-
) *vmcpconfig.AggregationConfig {
272+
) (*vmcpconfig.AggregationConfig, error) {
267273
agg := &vmcpconfig.AggregationConfig{}
268274

275+
c.convertConflictResolution(vmcp, agg)
276+
if err := c.convertToolConfigs(ctx, vmcp, agg); err != nil {
277+
return nil, err
278+
}
279+
280+
return agg, nil
281+
}
282+
283+
// convertConflictResolution converts conflict resolution strategy and config
284+
func (*Converter) convertConflictResolution(
285+
vmcp *mcpv1alpha1.VirtualMCPServer,
286+
agg *vmcpconfig.AggregationConfig,
287+
) {
269288
// Convert conflict resolution strategy
270289
switch vmcp.Spec.Aggregation.ConflictResolution {
271290
case mcpv1alpha1.ConflictResolutionPrefix:
@@ -290,32 +309,137 @@ func (*Converter) convertAggregation(
290309
PrefixFormat: "{workload}_",
291310
}
292311
}
312+
}
293313

294-
// Convert per-workload tool configs
295-
if len(vmcp.Spec.Aggregation.Tools) > 0 {
296-
agg.Tools = make([]*vmcpconfig.WorkloadToolConfig, 0, len(vmcp.Spec.Aggregation.Tools))
297-
for _, toolConfig := range vmcp.Spec.Aggregation.Tools {
298-
wtc := &vmcpconfig.WorkloadToolConfig{
299-
Workload: toolConfig.Workload,
300-
Filter: toolConfig.Filter,
301-
}
314+
// convertToolConfigs converts per-workload tool configurations
315+
func (c *Converter) convertToolConfigs(
316+
ctx context.Context,
317+
vmcp *mcpv1alpha1.VirtualMCPServer,
318+
agg *vmcpconfig.AggregationConfig,
319+
) error {
320+
if len(vmcp.Spec.Aggregation.Tools) == 0 {
321+
return nil
322+
}
302323

303-
// Convert overrides
304-
if len(toolConfig.Overrides) > 0 {
305-
wtc.Overrides = make(map[string]*vmcpconfig.ToolOverride)
306-
for toolName, override := range toolConfig.Overrides {
307-
wtc.Overrides[toolName] = &vmcpconfig.ToolOverride{
308-
Name: override.Name,
309-
Description: override.Description,
310-
}
311-
}
324+
ctxLogger := log.FromContext(ctx)
325+
agg.Tools = make([]*vmcpconfig.WorkloadToolConfig, 0, len(vmcp.Spec.Aggregation.Tools))
326+
327+
for _, toolConfig := range vmcp.Spec.Aggregation.Tools {
328+
wtc := &vmcpconfig.WorkloadToolConfig{
329+
Workload: toolConfig.Workload,
330+
Filter: toolConfig.Filter,
331+
}
332+
333+
if err := c.applyToolConfigRef(ctx, ctxLogger, vmcp, toolConfig, wtc); err != nil {
334+
return err
335+
}
336+
c.applyInlineOverrides(toolConfig, wtc)
337+
338+
agg.Tools = append(agg.Tools, wtc)
339+
}
340+
return nil
341+
}
342+
343+
// applyToolConfigRef resolves and applies MCPToolConfig reference
344+
func (c *Converter) applyToolConfigRef(
345+
ctx context.Context,
346+
ctxLogger logr.Logger,
347+
vmcp *mcpv1alpha1.VirtualMCPServer,
348+
toolConfig mcpv1alpha1.WorkloadToolConfig,
349+
wtc *vmcpconfig.WorkloadToolConfig,
350+
) error {
351+
if toolConfig.ToolConfigRef == nil {
352+
return nil
353+
}
354+
355+
resolvedConfig, err := c.resolveMCPToolConfig(ctx, vmcp.Namespace, toolConfig.ToolConfigRef.Name)
356+
if err != nil {
357+
ctxLogger.Error(err, "failed to resolve MCPToolConfig reference",
358+
"workload", toolConfig.Workload,
359+
"toolConfigRef", toolConfig.ToolConfigRef.Name)
360+
// Fail closed: return error when MCPToolConfig is configured but resolution fails
361+
// This prevents deploying without tool filtering when explicit configuration is requested
362+
return fmt.Errorf("MCPToolConfig resolution failed for %q: %w",
363+
toolConfig.ToolConfigRef.Name, err)
364+
}
365+
366+
// Note: resolveMCPToolConfig never returns (nil, nil) - it either succeeds with
367+
// (toolConfig, nil) or fails with (nil, error), so no nil check needed here
368+
369+
c.mergeToolConfigFilter(wtc, resolvedConfig)
370+
c.mergeToolConfigOverrides(wtc, resolvedConfig)
371+
return nil
372+
}
373+
374+
// mergeToolConfigFilter merges filter from MCPToolConfig
375+
func (*Converter) mergeToolConfigFilter(
376+
wtc *vmcpconfig.WorkloadToolConfig,
377+
resolvedConfig *mcpv1alpha1.MCPToolConfig,
378+
) {
379+
if len(wtc.Filter) == 0 && len(resolvedConfig.Spec.ToolsFilter) > 0 {
380+
wtc.Filter = resolvedConfig.Spec.ToolsFilter
381+
}
382+
}
383+
384+
// mergeToolConfigOverrides merges overrides from MCPToolConfig
385+
func (*Converter) mergeToolConfigOverrides(
386+
wtc *vmcpconfig.WorkloadToolConfig,
387+
resolvedConfig *mcpv1alpha1.MCPToolConfig,
388+
) {
389+
if len(resolvedConfig.Spec.ToolsOverride) == 0 {
390+
return
391+
}
392+
393+
if wtc.Overrides == nil {
394+
wtc.Overrides = make(map[string]*vmcpconfig.ToolOverride)
395+
}
396+
397+
for toolName, override := range resolvedConfig.Spec.ToolsOverride {
398+
if _, exists := wtc.Overrides[toolName]; !exists {
399+
wtc.Overrides[toolName] = &vmcpconfig.ToolOverride{
400+
Name: override.Name,
401+
Description: override.Description,
312402
}
403+
}
404+
}
405+
}
406+
407+
// applyInlineOverrides applies inline tool overrides
408+
func (*Converter) applyInlineOverrides(
409+
toolConfig mcpv1alpha1.WorkloadToolConfig,
410+
wtc *vmcpconfig.WorkloadToolConfig,
411+
) {
412+
if len(toolConfig.Overrides) == 0 {
413+
return
414+
}
415+
416+
if wtc.Overrides == nil {
417+
wtc.Overrides = make(map[string]*vmcpconfig.ToolOverride)
418+
}
313419

314-
agg.Tools = append(agg.Tools, wtc)
420+
for toolName, override := range toolConfig.Overrides {
421+
wtc.Overrides[toolName] = &vmcpconfig.ToolOverride{
422+
Name: override.Name,
423+
Description: override.Description,
315424
}
316425
}
426+
}
317427

318-
return agg
428+
// resolveMCPToolConfig fetches an MCPToolConfig resource by name and namespace
429+
func (c *Converter) resolveMCPToolConfig(
430+
ctx context.Context,
431+
namespace string,
432+
name string,
433+
) (*mcpv1alpha1.MCPToolConfig, error) {
434+
toolConfig := &mcpv1alpha1.MCPToolConfig{}
435+
err := c.k8sClient.Get(ctx, types.NamespacedName{
436+
Name: name,
437+
Namespace: namespace,
438+
}, toolConfig)
439+
if err != nil {
440+
return nil, fmt.Errorf("failed to get MCPToolConfig %s/%s: %w", namespace, name, err)
441+
}
442+
return toolConfig, nil
319443
}
320444

321445
// convertCompositeTools converts CompositeToolSpec from CRD to vmcp config

0 commit comments

Comments
 (0)