@@ -2,46 +2,105 @@ package gateway
22
33import (
44 "context"
5+ "database/sql"
56 "encoding/json"
7+ "errors"
68 "fmt"
79 "slices"
810 "strings"
911
1012 "github.com/google/jsonschema-go/jsonschema"
1113 "github.com/modelcontextprotocol/go-sdk/mcp"
1214
15+ "github.com/docker/mcp-gateway/pkg/catalog"
16+ "github.com/docker/mcp-gateway/pkg/config"
1317 "github.com/docker/mcp-gateway/pkg/db"
18+ "github.com/docker/mcp-gateway/pkg/gateway/project"
1419 "github.com/docker/mcp-gateway/pkg/log"
1520 "github.com/docker/mcp-gateway/pkg/oci"
21+ "github.com/docker/mcp-gateway/pkg/workingset"
1622)
1723
18- // ActivateProfileResult contains the result of profile activation
19- type ActivateProfileResult struct {
20- ActivatedServers []string
21- SkippedServers []string
22- ErrorMessage string
23- }
24+ var errProfileNotFound = errors .New ("profile not found" )
2425
25- // ActivateProfile activates a profile by name, loading its servers into the gateway
26- func ( g * Gateway ) ActivateProfile ( ctx context. Context , profileName string ) error {
27- // Create database connection
28- dao , err := db . New ( )
26+ // loadProfileFromProject attempts to load a profile from the project's profiles.json
27+ // Returns the WorkingSet if found, or errProfileNotFound if not found
28+ func loadProfileFromProject ( ctx context. Context , profileName string ) ( * workingset. WorkingSet , error ) {
29+ profiles , err := project . LoadProfiles ( ctx )
2930 if err != nil {
30- return fmt .Errorf ("failed to create database client: %w" , err )
31+ return nil , fmt .Errorf ("failed to load profiles.json: %w" , err )
32+ }
33+
34+ if profile , found := profiles [profileName ]; found {
35+ log .Log (fmt .Sprintf ("- Found profile '%s' in project's profiles.json" , profileName ))
36+ return & profile , nil
37+ }
38+
39+ return nil , errProfileNotFound
40+ }
41+
42+ // convertWorkingSetToConfiguration converts a WorkingSet to a Configuration object
43+ func (g * Gateway ) convertWorkingSetToConfiguration (ctx context.Context , ws workingset.WorkingSet ) (Configuration , error ) {
44+ // Ensure snapshots are resolved
45+ ociService := oci .NewService ()
46+ if err := ws .EnsureSnapshotsResolved (ctx , ociService ); err != nil {
47+ return Configuration {}, fmt .Errorf ("failed to resolve snapshots: %w" , err )
3148 }
32- defer dao .Close ()
3349
34- // Create a temporary WorkingSetConfiguration to load the profile
35- wsConfig := NewWorkingSetConfiguration (
36- Config {WorkingSet : profileName },
37- oci .NewService (),
38- g .docker ,
39- )
50+ // Build configuration similar to WorkingSetConfiguration.readOnce
51+ cfg := make (map [string ]map [string ]any )
52+ configs := make ([]ServerSecretConfig , 0 , len (ws .Servers ))
53+ toolsConfig := config.ToolsConfig {ServerTools : make (map [string ][]string )}
54+ serverNames := make ([]string , 0 )
55+ servers := make (map [string ]catalog.Server )
56+
57+ for _ , server := range ws .Servers {
58+ // Skip non-image/remote/registry servers
59+ if server .Type != workingset .ServerTypeImage &&
60+ server .Type != workingset .ServerTypeRemote &&
61+ server .Type != workingset .ServerTypeRegistry {
62+ continue
63+ }
64+
65+ serverName := server .Snapshot .Server .Name
66+ servers [serverName ] = server .Snapshot .Server
67+ serverNames = append (serverNames , serverName )
68+ cfg [serverName ] = server .Config
69+
70+ // Build secrets configs
71+ namespace := ""
72+ configs = append (configs , ServerSecretConfig {
73+ Secrets : server .Snapshot .Server .Secrets ,
74+ OAuth : server .Snapshot .Server .OAuth ,
75+ Namespace : namespace ,
76+ })
77+
78+ // Add tools
79+ if server .Tools != nil {
80+ toolsConfig .ServerTools [serverName ] = server .Tools
81+ }
82+ }
4083
41- // Load the full profile configuration using the existing readOnce method
42- profileConfig , err := wsConfig .readOnce (ctx , dao )
84+ secrets := BuildSecretsURIs (ctx , configs )
85+
86+ return Configuration {
87+ serverNames : serverNames ,
88+ servers : servers ,
89+ config : cfg ,
90+ tools : toolsConfig ,
91+ secrets : secrets ,
92+ }, nil
93+ }
94+
95+ // ActivateProfile activates a profile by merging its servers into the gateway
96+ // The WorkingSet should be loaded by the caller before calling this method
97+ func (g * Gateway ) ActivateProfile (ctx context.Context , ws workingset.WorkingSet ) error {
98+ log .Log (fmt .Sprintf ("- Activating profile '%s'" , ws .Name ))
99+
100+ // Convert WorkingSet to Configuration
101+ profileConfig , err := g .convertWorkingSetToConfiguration (ctx , ws )
43102 if err != nil {
44- return fmt .Errorf ("failed to load profile '%s': %w" , profileName , err )
103+ return fmt .Errorf ("failed to convert profile '%s': %w" , ws . Name , err )
45104 }
46105
47106 // Filter servers: only activate servers that are not already active
@@ -59,14 +118,16 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
59118 // If no servers to activate, return early
60119 if len (serversToActivate ) == 0 {
61120 if len (skippedServers ) > 0 {
62- log .Log (fmt .Sprintf ("- All servers from profile '%s' are already active: %s" , profileName , strings .Join (skippedServers , ", " )))
121+ log .Log (fmt .Sprintf ("- All servers from profile '%s' are already active: %s" , ws . Name , strings .Join (skippedServers , ", " )))
63122 } else {
64- log .Log (fmt .Sprintf ("- No new servers to activate from profile '%s'" , profileName ))
123+ log .Log (fmt .Sprintf ("- No new servers to activate from profile '%s'" , ws . Name ))
65124 }
66125 return nil
67126 }
68127
69- // Validate ALL servers before activating any (all-or-nothing)
128+ // Validate ALL servers before activating any
129+ // Note: Validation ensures prerequisites (secrets, config, images) are met.
130+ // Actual capability loading happens during activation and may partially succeed.
70131 var validationErrors []serverValidation
71132
72133 for _ , serverName := range serversToActivate {
@@ -86,54 +147,75 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
86147 serverConfigMap := profileConfig .config [serverName ]
87148
88149 for _ , configItem := range serverConfig .Config {
89- // Config items should be schema objects with a "name" property
150+ // Config items are object schemas with a "properties" map.
151+ // The "name" field is just an identifier, not a key in serverConfigMap.
90152 schemaMap , ok := configItem .(map [string ]any )
91153 if ! ok {
92154 continue
93155 }
94156
95- // Get the name field - this identifies which config to validate
96- configName , ok := schemaMap ["name" ].(string )
97- if ! ok || configName == "" {
157+ properties , ok := schemaMap ["properties" ].(map [string ]any )
158+ if ! ok {
98159 continue
99160 }
100161
101- // Get the actual config value to validate
102- if serverConfigMap == nil {
103- validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (missing)" , configName ))
104- continue
162+ // Build a set of required property names
163+ requiredProps := make (map [string ]bool )
164+ if requiredList , ok := schemaMap ["required" ].([]any ); ok {
165+ for _ , r := range requiredList {
166+ if s , ok := r .(string ); ok {
167+ requiredProps [s ] = true
168+ }
169+ }
105170 }
106171
107- configValue := serverConfigMap
172+ // Validate each property individually
173+ for propName , propSchema := range properties {
174+ propSchemaMap , ok := propSchema .(map [string ]any )
175+ if ! ok {
176+ continue
177+ }
108178
109- // Convert the schema map to a jsonschema.Schema for validation
110- schemaBytes , err := json .Marshal (schemaMap )
111- if err != nil {
112- validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (invalid schema)" , configName ))
113- continue
114- }
179+ // Get the value from the user-provided config
180+ configValue , exists := serverConfigMap [propName ]
181+ if ! exists {
182+ // If the property has a default, the server will use it
183+ if _ , hasDefault := propSchemaMap ["default" ]; hasDefault {
184+ continue
185+ }
186+ // Only flag as missing if explicitly required
187+ if requiredProps [propName ] {
188+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (missing)" , propName ))
189+ }
190+ continue
191+ }
115192
116- var schema jsonschema.Schema
117- if err := json .Unmarshal (schemaBytes , & schema ); err != nil {
118- validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (invalid schema)" , configName ))
119- continue
120- }
193+ // Validate the value against the property schema
194+ schemaBytes , err := json .Marshal (propSchemaMap )
195+ if err != nil {
196+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (invalid schema)" , propName ))
197+ continue
198+ }
121199
122- // Resolve the schema
123- resolved , err := schema .Resolve (nil )
124- if err != nil {
125- validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (schema resolution failed)" , configName ))
126- continue
127- }
200+ var propSchemaObj jsonschema.Schema
201+ if err := json .Unmarshal (schemaBytes , & propSchemaObj ); err != nil {
202+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (invalid schema)" , propName ))
203+ continue
204+ }
128205
129- // Validate the config value against the schema
130- if err := resolved .Validate (configValue ); err != nil {
131- // Extract a helpful error message
132- errMsg := err .Error ()
133- if len (errMsg ) > 100 {
134- errMsg = errMsg [:97 ] + "..."
206+ resolved , err := propSchemaObj .Resolve (nil )
207+ if err != nil {
208+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (schema resolution failed)" , propName ))
209+ continue
210+ }
211+
212+ if err := resolved .Validate (configValue ); err != nil {
213+ errMsg := err .Error ()
214+ if len (errMsg ) > 100 {
215+ errMsg = errMsg [:97 ] + "..."
216+ }
217+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (%s)" , propName , errMsg ))
135218 }
136- validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (%s)" , configName , errMsg ))
137219 }
138220 }
139221 }
@@ -155,7 +237,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
155237 // If any validation errors, return detailed error message
156238 if len (validationErrors ) > 0 {
157239 var errorMessages []string
158- errorMessages = append (errorMessages , fmt .Sprintf ("Cannot activate profile '%s'. Validation failed for %d server(s):" , profileName , len (validationErrors )))
240+ errorMessages = append (errorMessages , fmt .Sprintf ("Cannot activate profile '%s'. Validation failed for %d server(s):" , ws . Name , len (validationErrors )))
159241
160242 for _ , validation := range validationErrors {
161243 errorMessages = append (errorMessages , fmt .Sprintf ("\n Server '%s':" , validation .serverName ))
@@ -177,7 +259,12 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
177259 }
178260
179261 // All validations passed - merge configuration into current gateway
262+ // Acquire configuration mutex to ensure atomic updates
263+ g .configurationMu .Lock ()
264+ defer g .configurationMu .Unlock ()
265+
180266 var activatedServers []string
267+ var failedServers []string
181268
182269 // Merge secrets once (they're already namespaced in profileConfig)
183270 for secretName , secretValue := range profileConfig .secrets {
@@ -211,6 +298,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
211298 oldCaps , err := g .reloadServerCapabilities (ctx , serverName , nil )
212299 if err != nil {
213300 log .Log (fmt .Sprintf ("Warning: Failed to reload capabilities for server '%s': %v" , serverName , err ))
301+ failedServers = append (failedServers , serverName )
214302 // Continue with other servers even if this one fails
215303 continue
216304 }
@@ -221,6 +309,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
221309 if err := g .updateServerCapabilities (serverName , oldCaps , newCaps , nil ); err != nil {
222310 g .capabilitiesMu .Unlock ()
223311 log .Log (fmt .Sprintf ("Warning: Failed to update server capabilities for '%s': %v" , serverName , err ))
312+ failedServers = append (failedServers , serverName )
224313 // Continue with other servers even if this one fails
225314 continue
226315 }
@@ -229,10 +318,20 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
229318 activatedServers = append (activatedServers , serverName )
230319 }
231320
232- log .Log (fmt .Sprintf ("- Successfully activated profile '%s' with %d server(s): %s" , profileName , len (activatedServers ), strings .Join (activatedServers , ", " )))
321+ // Log results
322+ if len (activatedServers ) > 0 {
323+ log .Log (fmt .Sprintf ("- Successfully activated profile '%s' with %d server(s): %s" , ws .Name , len (activatedServers ), strings .Join (activatedServers , ", " )))
324+ }
233325 if len (skippedServers ) > 0 {
234326 log .Log (fmt .Sprintf ("- Skipped %d already-active server(s): %s" , len (skippedServers ), strings .Join (skippedServers , ", " )))
235327 }
328+ if len (failedServers ) > 0 {
329+ log .Log (fmt .Sprintf ("- Failed to activate %d server(s): %s" , len (failedServers ), strings .Join (failedServers , ", " )))
330+ // Return error if all servers failed to activate
331+ if len (activatedServers ) == 0 {
332+ return fmt .Errorf ("failed to activate any servers from profile '%s'" , ws .Name )
333+ }
334+ }
236335
237336 return nil
238337}
@@ -271,8 +370,49 @@ func activateProfileHandler(g *Gateway, _ *clientConfig) mcp.ToolHandler {
271370
272371 profileName := strings .TrimSpace (params .Name )
273372
274- // Use the ActivateProfile method
275- err = g .ActivateProfile (ctx , profileName )
373+ // Load the profile from either profiles.json or database
374+ var ws * workingset.WorkingSet
375+
376+ // First, try to load from project's profiles.json
377+ projectProfile , err := loadProfileFromProject (ctx , profileName )
378+ if err != nil && ! errors .Is (err , errProfileNotFound ) {
379+ log .Log (fmt .Sprintf ("Warning: Failed to check project profiles: %v" , err ))
380+ }
381+
382+ if projectProfile != nil {
383+ // Found in project's profiles.json
384+ log .Log (fmt .Sprintf ("- Found profile '%s' in project's profiles.json" , profileName ))
385+ ws = projectProfile
386+ } else {
387+ // Not found in project, try database
388+ log .Log (fmt .Sprintf ("- Profile '%s' not found in project's profiles.json, checking database" , profileName ))
389+
390+ dao , err := db .New ()
391+ if err != nil {
392+ return nil , fmt .Errorf ("failed to create database client: %w" , err )
393+ }
394+ defer dao .Close ()
395+
396+ dbProfile , err := dao .GetWorkingSet (ctx , profileName )
397+ if err != nil {
398+ if errors .Is (err , sql .ErrNoRows ) {
399+ return & mcp.CallToolResult {
400+ Content : []mcp.Content {& mcp.TextContent {
401+ Text : fmt .Sprintf ("Error: Profile '%s' not found in project or database" , profileName ),
402+ }},
403+ IsError : true ,
404+ }, nil
405+ }
406+ return nil , fmt .Errorf ("failed to load profile from database: %w" , err )
407+ }
408+
409+ log .Log (fmt .Sprintf ("- Found profile '%s' in database" , profileName ))
410+ wsFromDb := workingset .NewFromDb (dbProfile )
411+ ws = & wsFromDb
412+ }
413+
414+ // Activate the profile
415+ err = g .ActivateProfile (ctx , * ws )
276416 if err != nil {
277417 return & mcp.CallToolResult {
278418 Content : []mcp.Content {& mcp.TextContent {
@@ -284,7 +424,7 @@ func activateProfileHandler(g *Gateway, _ *clientConfig) mcp.ToolHandler {
284424
285425 return & mcp.CallToolResult {
286426 Content : []mcp.Content {& mcp.TextContent {
287- Text : fmt .Sprintf ("Successfully activated profile '%s'" , profileName ),
427+ Text : fmt .Sprintf ("Successfully activated profile '%s'" , ws . Name ),
288428 }},
289429 }, nil
290430 }
0 commit comments