Skip to content

Commit db5d68d

Browse files
Merge pull request #202 from docker/slim/dynamic-tools
Slim/dynamic tools
2 parents cd0118e + a84ba7e commit db5d68d

File tree

13 files changed

+936
-23
lines changed

13 files changed

+936
-23
lines changed

cmd/docker-mcp/commands/feature.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,19 @@ func isFeatureEnabledFromCli(dockerCli command.Cli, feature string) bool {
199199

200200
// isFeatureEnabledFromConfig checks if a feature is enabled from a config file
201201
func isFeatureEnabledFromConfig(configFile *configfile.ConfigFile, feature string) bool {
202+
// Features that are enabled by default
203+
defaultEnabledFeatures := map[string]bool{
204+
"mcp-oauth-dcr": true,
205+
"dynamic-tools": true,
206+
}
207+
202208
if configFile.Features == nil {
203-
return feature == "mcp-oauth-dcr" // mcp-oauth-dcr is enabled by default
209+
return defaultEnabledFeatures[feature]
204210
}
205211

206212
value, exists := configFile.Features[feature]
207213
if !exists {
208-
return feature == "mcp-oauth-dcr" // mcp-oauth-dcr is enabled by default
214+
return defaultEnabledFeatures[feature]
209215
}
210216

211217
// Handle both boolean string values and "enabled"/"disabled" strings

cmd/docker-mcp/commands/feature_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@ func TestIsFeatureEnabledDynamicTools(t *testing.T) {
6363
Features: make(map[string]string),
6464
}
6565
enabled := isFeatureEnabledFromConfig(configFile, "dynamic-tools")
66-
assert.False(t, enabled, "missing features should default to disabled")
66+
assert.True(t, enabled, "dynamic-tools should default to enabled when missing")
67+
})
68+
69+
t.Run("nil features map", func(t *testing.T) {
70+
configFile := &configfile.ConfigFile{
71+
Features: nil,
72+
}
73+
enabled := isFeatureEnabledFromConfig(configFile, "dynamic-tools")
74+
assert.True(t, enabled, "dynamic-tools should default to enabled when Features is nil")
6775
})
6876
}
6977

cmd/docker-mcp/commands/gateway.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
177177
runCmd.Flags().StringVar(&options.Memory, "memory", options.Memory, "Memory allocated to each MCP Server (default is 2Gb)")
178178
runCmd.Flags().BoolVar(&options.Static, "static", options.Static, "Enable static mode (aka pre-started servers)")
179179
runCmd.Flags().StringVar(&options.LogFilePath, "log", options.LogFilePath, "Path to log file for stderr output (relative or absolute)")
180+
runCmd.Flags().StringVar(&options.SessionName, "session", "", "Session name for loading and persisting configuration from ~/.docker/mcp/{SessionName}/")
180181

181182
// Very experimental features
182183
_ = runCmd.Flags().MarkHidden("log")
@@ -295,12 +296,12 @@ func isMcpOAuthDcrFeatureEnabled(dockerCli command.Cli) bool {
295296
func isDynamicToolsFeatureEnabled(dockerCli command.Cli) bool {
296297
configFile := dockerCli.ConfigFile()
297298
if configFile == nil || configFile.Features == nil {
298-
return false
299+
return true // Default enabled when no config exists
299300
}
300301

301302
value, exists := configFile.Features["dynamic-tools"]
302303
if !exists {
303-
return false
304+
return true // Default enabled when not in config
304305
}
305306

306307
return value == "enabled"

cmd/docker-mcp/commands/gateway_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,31 +88,31 @@ func TestIsDynamicToolsFeatureEnabled(t *testing.T) {
8888
Features: map[string]string{},
8989
}
9090
enabled := isDynamicToolsFeatureEnabledFromConfig(configFile)
91-
assert.False(t, enabled, "should return false when dynamic-tools is not set")
91+
assert.True(t, enabled, "should return true when dynamic-tools is not set")
9292
})
9393

9494
t.Run("nil config", func(t *testing.T) {
9595
enabled := isDynamicToolsFeatureEnabledFromConfig(nil)
96-
assert.False(t, enabled, "should return false when config is nil")
96+
assert.True(t, enabled, "should return true when config is nil")
9797
})
9898

9999
t.Run("nil features", func(t *testing.T) {
100100
configFile := &configfile.ConfigFile{
101101
Features: nil,
102102
}
103103
enabled := isDynamicToolsFeatureEnabledFromConfig(configFile)
104-
assert.False(t, enabled, "should return false when features is nil")
104+
assert.True(t, enabled, "should return true when features is nil")
105105
})
106106
}
107107

108108
// Helper function for testing (extract logic from isDynamicToolsFeatureEnabled)
109109
func isDynamicToolsFeatureEnabledFromConfig(configFile *configfile.ConfigFile) bool {
110110
if configFile == nil || configFile.Features == nil {
111-
return false
111+
return true // Default enabled when no config exists
112112
}
113113
value, exists := configFile.Features["dynamic-tools"]
114114
if !exists {
115-
return false
115+
return true // Default enabled when not in config
116116
}
117117
return value == "enabled"
118118
}

docs/generator/reference/docker_mcp_gateway_run.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@ options:
242242
experimentalcli: false
243243
kubernetes: false
244244
swarm: false
245+
- option: session
246+
value_type: string
247+
description: |
248+
Session name for loading and persisting configuration from ~/.docker/mcp/{SessionName}/
249+
deprecated: false
250+
hidden: false
251+
experimental: false
252+
experimentalcli: false
253+
kubernetes: false
254+
swarm: false
245255
- option: static
246256
value_type: bool
247257
default_value: "false"

docs/generator/reference/mcp_gateway_run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Run the gateway
2929
| `--registry` | `stringSlice` | `[registry.yaml]` | Paths to the registry files (absolute or relative to ~/.docker/mcp/) |
3030
| `--secrets` | `string` | `docker-desktop` | Colon separated paths to search for secrets. Can be `docker-desktop` or a path to a .env file (default to using Docker Desktop's secrets API) |
3131
| `--servers` | `stringSlice` | | Names of the servers to enable (if non empty, ignore --registry flag) |
32+
| `--session` | `string` | | Session name for loading and persisting configuration from ~/.docker/mcp/{SessionName}/ |
3233
| `--static` | `bool` | | Enable static mode (aka pre-started servers) |
3334
| `--tools` | `stringSlice` | | List of tools to enable |
3435
| `--tools-config` | `stringSlice` | `[tools.yaml]` | Paths to the tools files (absolute or relative to ~/.docker/mcp/) |

pkg/config/readwrite.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"regexp"
@@ -111,6 +112,33 @@ func writeConfigFile(name string, content []byte) error {
111112
return os.WriteFile(path, content, 0o644)
112113
}
113114

115+
// WriteConfigFileToSession writes a config file to a session directory
116+
func WriteConfigFileToSession(sessionName, name string, content []byte) error {
117+
sessionPath, err := SessionFilePath(sessionName, name)
118+
if err != nil {
119+
return err
120+
}
121+
122+
if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {
123+
return err
124+
}
125+
return os.WriteFile(sessionPath, content, 0o644)
126+
}
127+
128+
// SessionFilePath returns the file path within a session directory
129+
func SessionFilePath(sessionName, name string) (string, error) {
130+
if sessionName == "" {
131+
return "", fmt.Errorf("session name is required")
132+
}
133+
134+
homeDir, err := user.HomeDir()
135+
if err != nil {
136+
return "", err
137+
}
138+
139+
return filepath.Join(homeDir, ".docker", "mcp", sessionName, name), nil
140+
}
141+
114142
func FilePath(name string) (string, error) {
115143
if filepath.IsAbs(name) {
116144
return name, nil

pkg/gateway/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Config struct {
1010
RegistryPath []string
1111
ToolsPath []string
1212
SecretsPath string
13+
SessionName string // Session name for persisting configuration
1314
MCPRegistryServers []catalog.Server // catalog.Server objects from MCP registries
1415
}
1516

pkg/gateway/configuration.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/fsnotify/fsnotify"
14+
"gopkg.in/yaml.v3"
1415

1516
"github.com/docker/mcp-gateway/pkg/catalog"
1617
"github.com/docker/mcp-gateway/pkg/config"
@@ -29,6 +30,7 @@ type Configuration struct {
2930
config map[string]map[string]any
3031
tools config.ToolsConfig
3132
secrets map[string]string
33+
SessionName string
3234
}
3335

3436
func (c *Configuration) ServerNames() []string {
@@ -90,6 +92,51 @@ func (c *Configuration) Find(serverName string) (*catalog.ServerConfig, *map[str
9092
return nil, &byName, true
9193
}
9294

95+
// Persist writes the configuration files to the session directory if SessionName is set
96+
func (c *Configuration) Persist() error {
97+
if c.SessionName == "" {
98+
return nil // No session name set, nothing to persist
99+
}
100+
101+
// Serialize and write registry.yaml
102+
registry := config.Registry{
103+
Servers: make(map[string]config.Tile),
104+
}
105+
for _, serverName := range c.serverNames {
106+
registry.Servers[serverName] = config.Tile{
107+
Ref: serverName,
108+
}
109+
}
110+
registryBytes, err := yaml.Marshal(registry)
111+
if err != nil {
112+
return fmt.Errorf("failed to marshal registry: %w", err)
113+
}
114+
if err := config.WriteConfigFileToSession(c.SessionName, "registry.yaml", registryBytes); err != nil {
115+
return fmt.Errorf("failed to write registry.yaml: %w", err)
116+
}
117+
118+
// Serialize and write config.yaml
119+
configBytes, err := yaml.Marshal(c.config)
120+
if err != nil {
121+
return fmt.Errorf("failed to marshal config: %w", err)
122+
}
123+
if err := config.WriteConfigFileToSession(c.SessionName, "config.yaml", configBytes); err != nil {
124+
return fmt.Errorf("failed to write config.yaml: %w", err)
125+
}
126+
127+
// Serialize and write tools.yaml
128+
toolsBytes, err := yaml.Marshal(c.tools)
129+
if err != nil {
130+
return fmt.Errorf("failed to marshal tools: %w", err)
131+
}
132+
if err := config.WriteConfigFileToSession(c.SessionName, "tools.yaml", toolsBytes); err != nil {
133+
return fmt.Errorf("failed to write tools.yaml: %w", err)
134+
}
135+
136+
log.Log(fmt.Sprintf(" - Configuration persisted to session '%s'", c.SessionName))
137+
return nil
138+
}
139+
93140
type FileBasedConfiguration struct {
94141
CatalogPath []string
95142
ServerNames []string // Takes precedence over the RegistryPath
@@ -101,6 +148,7 @@ type FileBasedConfiguration struct {
101148
MCPRegistryServers []catalog.Server // Servers fetched from MCP registries
102149
Watch bool
103150
McpOAuthDcrEnabled bool
151+
sessionName string // Session name for persisting configuration
104152

105153
docker docker.Client
106154
}

0 commit comments

Comments
 (0)