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.
4547func 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