@@ -30,9 +30,19 @@ import (
3030 "github.com/stacklok/toolhive/pkg/groups"
3131 vmcptypes "github.com/stacklok/toolhive/pkg/vmcp"
3232 "github.com/stacklok/toolhive/pkg/vmcp/aggregator"
33+ "github.com/stacklok/toolhive/pkg/vmcp/auth/converters"
34+ authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types"
35+ vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
3336 "github.com/stacklok/toolhive/pkg/vmcp/workloads"
3437)
3538
39+ const (
40+ // OutgoingAuthSourceDiscovered indicates that auth configs should be automatically discovered from MCPServers
41+ OutgoingAuthSourceDiscovered = "discovered"
42+ // OutgoingAuthSourceInline indicates that auth configs should be explicitly specified
43+ OutgoingAuthSourceInline = "inline"
44+ )
45+
3646// VirtualMCPServerReconciler reconciles a VirtualMCPServer object
3747//
3848// Resource Cleanup Strategy:
@@ -896,6 +906,169 @@ func createVmcpServiceURL(vmcpName, namespace string, port int32) string {
896906 return fmt .Sprintf ("http://%s.%s.svc.cluster.local:%d" , serviceName , namespace , port )
897907}
898908
909+ // convertExternalAuthConfigToStrategy converts an MCPExternalAuthConfig to a BackendAuthStrategy.
910+ // This uses the converter registry to support all auth types (token exchange, header injection, etc.).
911+ // For ConfigMap mode (inline), secrets are referenced as environment variables that will be
912+ // mounted in the deployment.
913+ func (r * VirtualMCPServerReconciler ) convertExternalAuthConfigToStrategy (
914+ ctx context.Context ,
915+ namespace string ,
916+ externalAuthConfig * mcpv1alpha1.MCPExternalAuthConfig ,
917+ ) (* authtypes.BackendAuthStrategy , error ) {
918+ // Use the converter registry to convert to typed strategy
919+ registry := converters .DefaultRegistry ()
920+ converter , err := registry .GetConverter (externalAuthConfig .Spec .Type )
921+ if err != nil {
922+ return nil , err
923+ }
924+
925+ // Convert to typed BackendAuthStrategy (this will use env var references for secrets)
926+ strategy , err := converter .ConvertToStrategy (externalAuthConfig )
927+ if err != nil {
928+ return nil , fmt .Errorf ("failed to convert external auth config to strategy: %w" , err )
929+ }
930+
931+ // For header injection, resolve secrets from Kubernetes
932+ if externalAuthConfig .Spec .Type == mcpv1alpha1 .ExternalAuthTypeHeaderInjection {
933+ strategy , err = converter .ResolveSecrets (ctx , externalAuthConfig , r .Client , namespace , strategy )
934+ if err != nil {
935+ return nil , fmt .Errorf ("failed to resolve secrets for header injection: %w" , err )
936+ }
937+ }
938+
939+ return strategy , nil
940+ }
941+
942+ // convertBackendAuthConfigToVMCP converts a BackendAuthConfig from CRD to vmcp config.
943+ func (r * VirtualMCPServerReconciler ) convertBackendAuthConfigToVMCP (
944+ ctx context.Context ,
945+ namespace string ,
946+ crdConfig * mcpv1alpha1.BackendAuthConfig ,
947+ ) (* authtypes.BackendAuthStrategy , error ) {
948+ // For type="discovered", return a minimal strategy (will be populated by discovery)
949+ if crdConfig .Type == mcpv1alpha1 .BackendAuthTypeDiscovered {
950+ return & authtypes.BackendAuthStrategy {
951+ Type : crdConfig .Type ,
952+ }, nil
953+ }
954+
955+ // For type="external_auth_config_ref", fetch and convert the referenced config
956+ if crdConfig .ExternalAuthConfigRef != nil {
957+ // Fetch the MCPExternalAuthConfig and convert it
958+ externalAuthConfig , err := ctrlutil .GetExternalAuthConfigByName (
959+ ctx , r .Client , namespace , crdConfig .ExternalAuthConfigRef .Name )
960+ if err != nil {
961+ return nil , fmt .Errorf ("failed to get MCPExternalAuthConfig %s: %w" , crdConfig .ExternalAuthConfigRef .Name , err )
962+ }
963+
964+ // Convert the external auth config to strategy
965+ return r .convertExternalAuthConfigToStrategy (ctx , namespace , externalAuthConfig )
966+ }
967+
968+ // Fallback: return minimal strategy
969+ return & authtypes.BackendAuthStrategy {
970+ Type : crdConfig .Type ,
971+ }, nil
972+ }
973+
974+ // discovers ExternalAuthConfig from MCPServers and adds them to the outgoing config
975+ func (r * VirtualMCPServerReconciler ) discoverExternalAuthConfigs (
976+ ctx context.Context ,
977+ vmcp * mcpv1alpha1.VirtualMCPServer ,
978+ workloadNames []string ,
979+ outgoing * vmcpconfig.OutgoingAuthConfig ,
980+ ) {
981+ ctxLogger := log .FromContext (ctx )
982+
983+ for _ , workloadName := range workloadNames {
984+ mcpServer := & mcpv1alpha1.MCPServer {}
985+ if err := r .Get (ctx , types.NamespacedName {Name : workloadName , Namespace : vmcp .Namespace }, mcpServer ); err != nil {
986+ // Skip if MCPServer not found (might be a different workload type)
987+ continue
988+ }
989+
990+ // Only process if MCPServer has ExternalAuthConfigRef
991+ if mcpServer .Spec .ExternalAuthConfigRef == nil {
992+ continue
993+ }
994+
995+ // Fetch the MCPExternalAuthConfig
996+ externalAuthConfig , err := ctrlutil .GetExternalAuthConfigByName (
997+ ctx , r .Client , vmcp .Namespace , mcpServer .Spec .ExternalAuthConfigRef .Name )
998+ if err != nil {
999+ ctxLogger .V (1 ).Info ("Failed to get MCPExternalAuthConfig for backend, skipping" ,
1000+ "backend" , workloadName ,
1001+ "externalAuthConfig" , mcpServer .Spec .ExternalAuthConfigRef .Name ,
1002+ "error" , err )
1003+ continue
1004+ }
1005+
1006+ // Convert MCPExternalAuthConfig to BackendAuthStrategy
1007+ strategy , err := r .convertExternalAuthConfigToStrategy (ctx , vmcp .Namespace , externalAuthConfig )
1008+ if err != nil {
1009+ ctxLogger .V (1 ).Info ("Failed to convert MCPExternalAuthConfig to strategy, skipping" ,
1010+ "backend" , workloadName ,
1011+ "externalAuthConfig" , externalAuthConfig .Name ,
1012+ "error" , err )
1013+ continue
1014+ }
1015+
1016+ // Only add if not already overridden in inline config
1017+ if vmcp .Spec .OutgoingAuth == nil || vmcp .Spec .OutgoingAuth .Backends == nil {
1018+ outgoing .Backends [workloadName ] = strategy
1019+ } else if _ , exists := vmcp .Spec .OutgoingAuth .Backends [workloadName ]; ! exists {
1020+ // Only add discovered config if not explicitly overridden
1021+ outgoing .Backends [workloadName ] = strategy
1022+ }
1023+ }
1024+ }
1025+
1026+ // buildOutgoingAuthConfig builds an OutgoingAuthConfig from the VirtualMCPServer spec,
1027+ // discovering ExternalAuthConfig from MCPServers when source is "discovered".
1028+ func (r * VirtualMCPServerReconciler ) buildOutgoingAuthConfig (
1029+ ctx context.Context ,
1030+ vmcp * mcpv1alpha1.VirtualMCPServer ,
1031+ workloadNames []string ,
1032+ ) (* vmcpconfig.OutgoingAuthConfig , error ) {
1033+ // Determine source - default to "discovered" if not specified
1034+ source := OutgoingAuthSourceDiscovered
1035+ if vmcp .Spec .OutgoingAuth != nil && vmcp .Spec .OutgoingAuth .Source != "" {
1036+ source = vmcp .Spec .OutgoingAuth .Source
1037+ }
1038+
1039+ outgoing := & vmcpconfig.OutgoingAuthConfig {
1040+ Source : source ,
1041+ Backends : make (map [string ]* authtypes.BackendAuthStrategy ),
1042+ }
1043+
1044+ // Convert Default if specified
1045+ if vmcp .Spec .OutgoingAuth != nil && vmcp .Spec .OutgoingAuth .Default != nil {
1046+ defaultStrategy , err := r .convertBackendAuthConfigToVMCP (ctx , vmcp .Namespace , vmcp .Spec .OutgoingAuth .Default )
1047+ if err != nil {
1048+ return nil , fmt .Errorf ("failed to convert default auth config: %w" , err )
1049+ }
1050+ outgoing .Default = defaultStrategy
1051+ }
1052+
1053+ // Discover ExternalAuthConfig from MCPServers if source is "discovered"
1054+ if source == OutgoingAuthSourceDiscovered {
1055+ r .discoverExternalAuthConfigs (ctx , vmcp , workloadNames , outgoing )
1056+ }
1057+
1058+ // Apply inline overrides (works for all source modes)
1059+ if vmcp .Spec .OutgoingAuth != nil && vmcp .Spec .OutgoingAuth .Backends != nil {
1060+ for backendName , backendAuth := range vmcp .Spec .OutgoingAuth .Backends {
1061+ strategy , err := r .convertBackendAuthConfigToVMCP (ctx , vmcp .Namespace , & backendAuth )
1062+ if err != nil {
1063+ return nil , fmt .Errorf ("failed to convert backend auth config for %s: %w" , backendName , err )
1064+ }
1065+ outgoing .Backends [backendName ] = strategy
1066+ }
1067+ }
1068+
1069+ return outgoing , nil
1070+ }
1071+
8991072// discoverBackends discovers all MCPServers in the referenced MCPGroup and returns
9001073// a list of DiscoveredBackend objects with their current status.
9011074// This reuses the existing workload discovery code from pkg/vmcp/workloads.
@@ -911,22 +1084,35 @@ func (r *VirtualMCPServerReconciler) discoverBackends(
9111084 // Create K8S workload discoverer for the VirtualMCPServer's namespace
9121085 workloadDiscoverer := workloads .NewK8SDiscovererWithClient (r .Client , vmcp .Namespace )
9131086
1087+ // Get all workload names in the group
1088+ workloadNames , err := workloadDiscoverer .ListWorkloadsInGroup (ctx , vmcp .Spec .GroupRef .Name )
1089+ if err != nil {
1090+ return nil , fmt .Errorf ("failed to list workloads in group: %w" , err )
1091+ }
1092+
1093+ // Build outgoing auth config only if OutgoingAuth is explicitly configured
1094+ // This allows the aggregator to apply auth config to backends based on source mode
1095+ var authConfig * vmcpconfig.OutgoingAuthConfig
1096+ if vmcp .Spec .OutgoingAuth != nil {
1097+ var err error
1098+ authConfig , err = r .buildOutgoingAuthConfig (ctx , vmcp , workloadNames )
1099+ if err != nil {
1100+ ctxLogger .V (1 ).Info ("Failed to build outgoing auth config, continuing without auth" ,
1101+ "error" , err )
1102+ // Continue without auth config rather than failing
1103+ authConfig = nil
1104+ }
1105+ }
1106+
9141107 // Use the aggregator's unified backend discoverer to reuse discovery logic
915- // Pass nil for authConfig since we'll extract auth config from MCPServer directly
916- backendDiscoverer := aggregator .NewUnifiedBackendDiscoverer (workloadDiscoverer , groupsManager , nil )
1108+ backendDiscoverer := aggregator .NewUnifiedBackendDiscoverer (workloadDiscoverer , groupsManager , authConfig )
9171109
9181110 // Discover backends using the aggregator
9191111 backends , err := backendDiscoverer .Discover (ctx , vmcp .Spec .GroupRef .Name )
9201112 if err != nil {
9211113 return nil , fmt .Errorf ("failed to discover backends: %w" , err )
9221114 }
9231115
924- // Get all workload names to track backends that weren't accessible
925- workloadNames , err := workloadDiscoverer .ListWorkloadsInGroup (ctx , vmcp .Spec .GroupRef .Name )
926- if err != nil {
927- return nil , fmt .Errorf ("failed to list workloads in group: %w" , err )
928- }
929-
9301116 // Create a map of discovered backend names for quick lookup
9311117 discoveredBackendMap := make (map [string ]* vmcptypes.Backend , len (backends ))
9321118 for i := range backends {
0 commit comments