77 "fmt"
88 "time"
99
10+ "github.com/go-logr/logr"
11+ "k8s.io/apimachinery/pkg/types"
12+ "sigs.k8s.io/controller-runtime/pkg/client"
1013 "sigs.k8s.io/controller-runtime/pkg/log"
1114
1215 mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
@@ -29,18 +32,24 @@ const (
2932// Converter converts VirtualMCPServer CRD specs to vmcp Config
3033type Converter struct {
3134 oidcResolver oidc.Resolver
35+ k8sClient client.Client
3236}
3337
3438// NewConverter creates a new Converter instance.
3539// oidcResolver is required and used to resolve OIDC configuration from various sources
3640// (kubernetes, configMap, inline). Use a mock resolver in tests.
37- // Returns an error if oidcResolver is nil.
38- func NewConverter (oidcResolver oidc.Resolver ) (* Converter , error ) {
41+ // k8sClient is required for resolving MCPToolConfig references.
42+ // Returns an error if oidcResolver or k8sClient is nil.
43+ func NewConverter (oidcResolver oidc.Resolver , k8sClient client.Client ) (* Converter , error ) {
3944 if oidcResolver == nil {
4045 return nil , fmt .Errorf ("oidcResolver is required" )
4146 }
47+ if k8sClient == nil {
48+ return nil , fmt .Errorf ("k8sClient is required" )
49+ }
4250 return & Converter {
4351 oidcResolver : oidcResolver ,
52+ k8sClient : k8sClient ,
4453 }, nil
4554}
4655
@@ -243,12 +252,23 @@ func (*Converter) convertBackendAuthConfig(
243252}
244253
245254// convertAggregation converts AggregationConfig from CRD to vmcp config
246- func (* Converter ) convertAggregation (
247- _ context.Context ,
255+ func (c * Converter ) convertAggregation (
256+ ctx context.Context ,
248257 vmcp * mcpv1alpha1.VirtualMCPServer ,
249258) * vmcpconfig.AggregationConfig {
250259 agg := & vmcpconfig.AggregationConfig {}
251260
261+ c .convertConflictResolution (vmcp , agg )
262+ c .convertToolConfigs (ctx , vmcp , agg )
263+
264+ return agg
265+ }
266+
267+ // convertConflictResolution converts conflict resolution strategy and config
268+ func (* Converter ) convertConflictResolution (
269+ vmcp * mcpv1alpha1.VirtualMCPServer ,
270+ agg * vmcpconfig.AggregationConfig ,
271+ ) {
252272 // Convert conflict resolution strategy
253273 switch vmcp .Spec .Aggregation .ConflictResolution {
254274 case mcpv1alpha1 .ConflictResolutionPrefix :
@@ -273,32 +293,131 @@ func (*Converter) convertAggregation(
273293 PrefixFormat : "{workload}_" ,
274294 }
275295 }
296+ }
276297
277- // Convert per-workload tool configs
278- if len (vmcp .Spec .Aggregation .Tools ) > 0 {
279- agg .Tools = make ([]* vmcpconfig.WorkloadToolConfig , 0 , len (vmcp .Spec .Aggregation .Tools ))
280- for _ , toolConfig := range vmcp .Spec .Aggregation .Tools {
281- wtc := & vmcpconfig.WorkloadToolConfig {
282- Workload : toolConfig .Workload ,
283- Filter : toolConfig .Filter ,
284- }
298+ // convertToolConfigs converts per-workload tool configurations
299+ func (c * Converter ) convertToolConfigs (
300+ ctx context.Context ,
301+ vmcp * mcpv1alpha1.VirtualMCPServer ,
302+ agg * vmcpconfig.AggregationConfig ,
303+ ) {
304+ if len (vmcp .Spec .Aggregation .Tools ) == 0 {
305+ return
306+ }
285307
286- // Convert overrides
287- if len (toolConfig .Overrides ) > 0 {
288- wtc .Overrides = make (map [string ]* vmcpconfig.ToolOverride )
289- for toolName , override := range toolConfig .Overrides {
290- wtc .Overrides [toolName ] = & vmcpconfig.ToolOverride {
291- Name : override .Name ,
292- Description : override .Description ,
293- }
294- }
308+ ctxLogger := log .FromContext (ctx )
309+ agg .Tools = make ([]* vmcpconfig.WorkloadToolConfig , 0 , len (vmcp .Spec .Aggregation .Tools ))
310+
311+ for _ , toolConfig := range vmcp .Spec .Aggregation .Tools {
312+ wtc := & vmcpconfig.WorkloadToolConfig {
313+ Workload : toolConfig .Workload ,
314+ Filter : toolConfig .Filter ,
315+ }
316+
317+ c .applyToolConfigRef (ctx , ctxLogger , vmcp , toolConfig , wtc )
318+ c .applyInlineOverrides (toolConfig , wtc )
319+
320+ agg .Tools = append (agg .Tools , wtc )
321+ }
322+ }
323+
324+ // applyToolConfigRef resolves and applies MCPToolConfig reference
325+ func (c * Converter ) applyToolConfigRef (
326+ ctx context.Context ,
327+ ctxLogger logr.Logger ,
328+ vmcp * mcpv1alpha1.VirtualMCPServer ,
329+ toolConfig mcpv1alpha1.WorkloadToolConfig ,
330+ wtc * vmcpconfig.WorkloadToolConfig ,
331+ ) {
332+ if toolConfig .ToolConfigRef == nil {
333+ return
334+ }
335+
336+ resolvedConfig , err := c .resolveMCPToolConfig (ctx , vmcp .Namespace , toolConfig .ToolConfigRef .Name )
337+ if err != nil {
338+ ctxLogger .Error (err , "failed to resolve MCPToolConfig reference" ,
339+ "workload" , toolConfig .Workload ,
340+ "toolConfigRef" , toolConfig .ToolConfigRef .Name )
341+ return
342+ }
343+
344+ if resolvedConfig == nil {
345+ return
346+ }
347+
348+ c .mergeToolConfigFilter (wtc , resolvedConfig )
349+ c .mergeToolConfigOverrides (wtc , resolvedConfig )
350+ }
351+
352+ // mergeToolConfigFilter merges filter from MCPToolConfig
353+ func (* Converter ) mergeToolConfigFilter (
354+ wtc * vmcpconfig.WorkloadToolConfig ,
355+ resolvedConfig * mcpv1alpha1.MCPToolConfig ,
356+ ) {
357+ if len (wtc .Filter ) == 0 && len (resolvedConfig .Spec .ToolsFilter ) > 0 {
358+ wtc .Filter = resolvedConfig .Spec .ToolsFilter
359+ }
360+ }
361+
362+ // mergeToolConfigOverrides merges overrides from MCPToolConfig
363+ func (* Converter ) mergeToolConfigOverrides (
364+ wtc * vmcpconfig.WorkloadToolConfig ,
365+ resolvedConfig * mcpv1alpha1.MCPToolConfig ,
366+ ) {
367+ if len (resolvedConfig .Spec .ToolsOverride ) == 0 {
368+ return
369+ }
370+
371+ if wtc .Overrides == nil {
372+ wtc .Overrides = make (map [string ]* vmcpconfig.ToolOverride )
373+ }
374+
375+ for toolName , override := range resolvedConfig .Spec .ToolsOverride {
376+ if _ , exists := wtc .Overrides [toolName ]; ! exists {
377+ wtc .Overrides [toolName ] = & vmcpconfig.ToolOverride {
378+ Name : override .Name ,
379+ Description : override .Description ,
295380 }
381+ }
382+ }
383+ }
384+
385+ // applyInlineOverrides applies inline tool overrides
386+ func (* Converter ) applyInlineOverrides (
387+ toolConfig mcpv1alpha1.WorkloadToolConfig ,
388+ wtc * vmcpconfig.WorkloadToolConfig ,
389+ ) {
390+ if len (toolConfig .Overrides ) == 0 {
391+ return
392+ }
296393
297- agg .Tools = append (agg .Tools , wtc )
394+ if wtc .Overrides == nil {
395+ wtc .Overrides = make (map [string ]* vmcpconfig.ToolOverride )
396+ }
397+
398+ for toolName , override := range toolConfig .Overrides {
399+ wtc .Overrides [toolName ] = & vmcpconfig.ToolOverride {
400+ Name : override .Name ,
401+ Description : override .Description ,
298402 }
299403 }
404+ }
300405
301- return agg
406+ // resolveMCPToolConfig fetches an MCPToolConfig resource by name and namespace
407+ func (c * Converter ) resolveMCPToolConfig (
408+ ctx context.Context ,
409+ namespace string ,
410+ name string ,
411+ ) (* mcpv1alpha1.MCPToolConfig , error ) {
412+ toolConfig := & mcpv1alpha1.MCPToolConfig {}
413+ err := c .k8sClient .Get (ctx , types.NamespacedName {
414+ Name : name ,
415+ Namespace : namespace ,
416+ }, toolConfig )
417+ if err != nil {
418+ return nil , fmt .Errorf ("failed to get MCPToolConfig %s/%s: %w" , namespace , name , err )
419+ }
420+ return toolConfig , nil
302421}
303422
304423// convertCompositeTools converts CompositeToolSpec from CRD to vmcp config
0 commit comments