@@ -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
3137 }
32- defer dao .Close ()
3338
34- // Create a temporary WorkingSetConfiguration to load the profile
35- wsConfig := NewWorkingSetConfiguration (
36- Config {WorkingSet : profileName },
37- oci .NewService (),
38- g .docker ,
39- )
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 )
48+ }
49+
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+ }
4064
41- // Load the full profile configuration using the existing readOnce method
42- profileConfig , err := wsConfig .readOnce (ctx , dao )
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+ }
83+
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 {
@@ -104,7 +165,12 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
104165 continue
105166 }
106167
107- configValue := serverConfigMap
168+ // Extract the specific config value for this schema, not the entire map
169+ configValue , exists := serverConfigMap [configName ]
170+ if ! exists {
171+ validation .missingConfig = append (validation .missingConfig , fmt .Sprintf ("%s (missing)" , configName ))
172+ continue
173+ }
108174
109175 // Convert the schema map to a jsonschema.Schema for validation
110176 schemaBytes , err := json .Marshal (schemaMap )
@@ -155,7 +221,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
155221 // If any validation errors, return detailed error message
156222 if len (validationErrors ) > 0 {
157223 var errorMessages []string
158- errorMessages = append (errorMessages , fmt .Sprintf ("Cannot activate profile '%s'. Validation failed for %d server(s):" , profileName , len (validationErrors )))
224+ errorMessages = append (errorMessages , fmt .Sprintf ("Cannot activate profile '%s'. Validation failed for %d server(s):" , ws . Name , len (validationErrors )))
159225
160226 for _ , validation := range validationErrors {
161227 errorMessages = append (errorMessages , fmt .Sprintf ("\n Server '%s':" , validation .serverName ))
@@ -177,7 +243,12 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
177243 }
178244
179245 // All validations passed - merge configuration into current gateway
246+ // Acquire configuration mutex to ensure atomic updates
247+ g .configurationMu .Lock ()
248+ defer g .configurationMu .Unlock ()
249+
180250 var activatedServers []string
251+ var failedServers []string
181252
182253 // Merge secrets once (they're already namespaced in profileConfig)
183254 for secretName , secretValue := range profileConfig .secrets {
@@ -211,6 +282,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
211282 oldCaps , err := g .reloadServerCapabilities (ctx , serverName , nil )
212283 if err != nil {
213284 log .Log (fmt .Sprintf ("Warning: Failed to reload capabilities for server '%s': %v" , serverName , err ))
285+ failedServers = append (failedServers , serverName )
214286 // Continue with other servers even if this one fails
215287 continue
216288 }
@@ -221,6 +293,7 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
221293 if err := g .updateServerCapabilities (serverName , oldCaps , newCaps , nil ); err != nil {
222294 g .capabilitiesMu .Unlock ()
223295 log .Log (fmt .Sprintf ("Warning: Failed to update server capabilities for '%s': %v" , serverName , err ))
296+ failedServers = append (failedServers , serverName )
224297 // Continue with other servers even if this one fails
225298 continue
226299 }
@@ -229,10 +302,20 @@ func (g *Gateway) ActivateProfile(ctx context.Context, profileName string) error
229302 activatedServers = append (activatedServers , serverName )
230303 }
231304
232- log .Log (fmt .Sprintf ("- Successfully activated profile '%s' with %d server(s): %s" , profileName , len (activatedServers ), strings .Join (activatedServers , ", " )))
305+ // Log results
306+ if len (activatedServers ) > 0 {
307+ log .Log (fmt .Sprintf ("- Successfully activated profile '%s' with %d server(s): %s" , ws .Name , len (activatedServers ), strings .Join (activatedServers , ", " )))
308+ }
233309 if len (skippedServers ) > 0 {
234310 log .Log (fmt .Sprintf ("- Skipped %d already-active server(s): %s" , len (skippedServers ), strings .Join (skippedServers , ", " )))
235311 }
312+ if len (failedServers ) > 0 {
313+ log .Log (fmt .Sprintf ("- Failed to activate %d server(s): %s" , len (failedServers ), strings .Join (failedServers , ", " )))
314+ // Return error if all servers failed to activate
315+ if len (activatedServers ) == 0 {
316+ return fmt .Errorf ("failed to activate any servers from profile '%s'" , ws .Name )
317+ }
318+ }
236319
237320 return nil
238321}
@@ -271,8 +354,49 @@ func activateProfileHandler(g *Gateway, _ *clientConfig) mcp.ToolHandler {
271354
272355 profileName := strings .TrimSpace (params .Name )
273356
274- // Use the ActivateProfile method
275- err = g .ActivateProfile (ctx , profileName )
357+ // Load the profile from either profiles.json or database
358+ var ws * workingset.WorkingSet
359+
360+ // First, try to load from project's profiles.json
361+ projectProfile , err := loadProfileFromProject (ctx , profileName )
362+ if err != nil && ! errors .Is (err , errProfileNotFound ) {
363+ log .Log (fmt .Sprintf ("Warning: Failed to check project profiles: %v" , err ))
364+ }
365+
366+ if projectProfile != nil {
367+ // Found in project's profiles.json
368+ log .Log (fmt .Sprintf ("- Found profile '%s' in project's profiles.json" , profileName ))
369+ ws = projectProfile
370+ } else {
371+ // Not found in project, try database
372+ log .Log (fmt .Sprintf ("- Profile '%s' not found in project's profiles.json, checking database" , profileName ))
373+
374+ dao , err := db .New ()
375+ if err != nil {
376+ return nil , fmt .Errorf ("failed to create database client: %w" , err )
377+ }
378+ defer dao .Close ()
379+
380+ dbProfile , err := dao .GetWorkingSet (ctx , profileName )
381+ if err != nil {
382+ if errors .Is (err , sql .ErrNoRows ) {
383+ return & mcp.CallToolResult {
384+ Content : []mcp.Content {& mcp.TextContent {
385+ Text : fmt .Sprintf ("Error: Profile '%s' not found in project or database" , profileName ),
386+ }},
387+ IsError : true ,
388+ }, nil
389+ }
390+ return nil , fmt .Errorf ("failed to load profile from database: %w" , err )
391+ }
392+
393+ log .Log (fmt .Sprintf ("- Found profile '%s' in database" , profileName ))
394+ wsFromDb := workingset .NewFromDb (dbProfile )
395+ ws = & wsFromDb
396+ }
397+
398+ // Activate the profile
399+ err = g .ActivateProfile (ctx , * ws )
276400 if err != nil {
277401 return & mcp.CallToolResult {
278402 Content : []mcp.Content {& mcp.TextContent {
@@ -284,7 +408,7 @@ func activateProfileHandler(g *Gateway, _ *clientConfig) mcp.ToolHandler {
284408
285409 return & mcp.CallToolResult {
286410 Content : []mcp.Content {& mcp.TextContent {
287- Text : fmt .Sprintf ("Successfully activated profile '%s'" , profileName ),
411+ Text : fmt .Sprintf ("Successfully activated profile '%s'" , ws . Name ),
288412 }},
289413 }, nil
290414 }
0 commit comments