Skip to content

Commit c6f02e9

Browse files
authored
Merge pull request #69 from docker/slim/multi-catalogs
Support additional catalogs
2 parents a629b08 + 0e317b6 commit c6f02e9

File tree

4 files changed

+129
-63
lines changed

4 files changed

+129
-63
lines changed

cmd/docker-mcp/commands/gateway.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
1919

2020
// Have different defaults for the on-host gateway and the in-container gateway.
2121
var options gateway.Config
22+
var additionalCatalogs []string
23+
var additionalRegistries []string
24+
var additionalConfigs []string
2225
if os.Getenv("DOCKER_MCP_IN_CONTAINER") == "1" {
2326
// In-container.
2427
options = gateway.Config{
25-
CatalogPath: catalog.DockerCatalogURL,
28+
CatalogPath: []string{catalog.DockerCatalogURL},
2629
SecretsPath: "docker-desktop:/run/secrets/mcp_secret:/.env",
2730
Options: gateway.Options{
2831
Cpus: 1,
@@ -37,9 +40,9 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
3740
} else {
3841
// On-host.
3942
options = gateway.Config{
40-
CatalogPath: "docker-mcp.yaml",
41-
RegistryPath: "registry.yaml",
42-
ConfigPath: "config.yaml",
43+
CatalogPath: []string{"docker-mcp.yaml"},
44+
RegistryPath: []string{"registry.yaml"},
45+
ConfigPath: []string{"config.yaml"},
4346
SecretsPath: "docker-desktop",
4447
Options: gateway.Options{
4548
Cpus: 1,
@@ -65,14 +68,21 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
6568
options.Port = 8811
6669
}
6770

71+
// Append additional catalogs to the main catalog path
72+
options.CatalogPath = append(options.CatalogPath, additionalCatalogs...)
73+
options.RegistryPath = append(options.RegistryPath, additionalRegistries...)
74+
options.ConfigPath = append(options.ConfigPath, additionalConfigs...)
6875
return gateway.NewGateway(options, docker).Run(cmd.Context())
6976
},
7077
}
7178

7279
runCmd.Flags().StringSliceVar(&options.ServerNames, "servers", nil, "names of the servers to enable (if non empty, ignore --registry flag)")
73-
runCmd.Flags().StringVar(&options.CatalogPath, "catalog", options.CatalogPath, "path to the docker-mcp.yaml catalog (absolute or relative to ~/.docker/mcp/catalogs/)")
74-
runCmd.Flags().StringVar(&options.RegistryPath, "registry", options.RegistryPath, "path to the registry.yaml (absolute or relative to ~/.docker/mcp/)")
75-
runCmd.Flags().StringVar(&options.ConfigPath, "config", options.ConfigPath, "path to the config.yaml (absolute or relative to ~/.docker/mcp/)")
80+
runCmd.Flags().StringSliceVar(&options.CatalogPath, "catalog", options.CatalogPath, "paths to docker catalogs (absolute or relative to ~/.docker/mcp/catalogs/)")
81+
runCmd.Flags().StringSliceVar(&additionalCatalogs, "additional-catalog", nil, "additional catalog paths to append to the default catalogs")
82+
runCmd.Flags().StringSliceVar(&options.RegistryPath, "registry", options.RegistryPath, "paths to the registry files (absolute or relative to ~/.docker/mcp/)")
83+
runCmd.Flags().StringSliceVar(&additionalRegistries, "additional-registry", nil, "additional registry paths to merge with the default registry.yaml")
84+
runCmd.Flags().StringSliceVar(&options.ConfigPath, "config", options.ConfigPath, "paths to the config files (absolute or relative to ~/.docker/mcp/)")
85+
runCmd.Flags().StringSliceVar(&additionalConfigs, "additional-config", nil, "additional config paths to merge with the default config.yaml")
7686
runCmd.Flags().StringVar(&options.SecretsPath, "secrets", options.SecretsPath, "colon separated paths to search for secrets. Can be `docker-desktop` or a path to a .env file (default to using Docker Deskop's secrets API)")
7787
runCmd.Flags().StringSliceVar(&options.ToolNames, "tools", options.ToolNames, "List of tools to enable")
7888
runCmd.Flags().StringArrayVar(&options.Interceptors, "interceptor", options.Interceptors, "List of interceptors to use (format: when:type:path, e.g. 'before:exec:/bin/path')")

cmd/docker-mcp/internal/catalog/catalog.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"log"
78
"net/http"
89
"os"
910
"path/filepath"
@@ -15,20 +16,29 @@ import (
1516
)
1617

1718
func Get(ctx context.Context) (Catalog, error) {
18-
return ReadFrom(ctx, "docker-mcp.yaml")
19+
return ReadFrom(ctx, []string{"docker-mcp.yaml"})
1920
}
2021

21-
func ReadFrom(ctx context.Context, fileOrURL string) (Catalog, error) {
22-
servers, err := readMCPServers(ctx, fileOrURL)
23-
if err != nil {
24-
return Catalog{}, err
25-
}
26-
if servers == nil {
27-
servers = map[string]Server{}
22+
func ReadFrom(ctx context.Context, fileOrURLs []string) (Catalog, error) {
23+
mergedServers := map[string]Server{}
24+
25+
for _, fileOrURL := range fileOrURLs {
26+
servers, err := readMCPServers(ctx, fileOrURL)
27+
if err != nil {
28+
return Catalog{}, err
29+
}
30+
31+
// Merge servers into the combined map, checking for overlaps
32+
for key, server := range servers {
33+
if _, exists := mergedServers[key]; exists {
34+
log.Printf("Warning: overlapping key '%s' found in catalog '%s', overwriting previous value", key, fileOrURL)
35+
}
36+
mergedServers[key] = server
37+
}
2838
}
2939

3040
return Catalog{
31-
Servers: servers,
41+
Servers: mergedServers,
3242
}, nil
3343
}
3444

cmd/docker-mcp/internal/gateway/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package gateway
33
type Config struct {
44
Options
55
ServerNames []string
6-
CatalogPath string
7-
ConfigPath string
8-
RegistryPath string
6+
CatalogPath []string
7+
ConfigPath []string
8+
RegistryPath []string
99
SecretsPath string
1010
}
1111

cmd/docker-mcp/internal/gateway/configuration.go

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ func (c *Configuration) Find(serverName string) (*catalog.ServerConfig, *map[str
8686
}
8787

8888
type FileBasedConfiguration struct {
89-
CatalogPath string
89+
CatalogPath []string
9090
ServerNames []string // Takes precedence over the RegistryPath
91-
RegistryPath string
92-
ConfigPath string
91+
RegistryPath []string
92+
ConfigPath []string
9393
SecretsPath string // Optional, if not set, use Docker Desktop's secrets API
9494
Watch bool
9595

@@ -105,17 +105,28 @@ func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan
105105
return configuration, nil, func() error { return nil }, nil
106106
}
107107

108-
var registryPath string
108+
var registryPaths []string
109109
if len(c.ServerNames) == 0 {
110-
registryPath, err = config.FilePath(c.RegistryPath)
111-
if err != nil {
112-
return Configuration{}, nil, nil, err
110+
for _, path := range c.RegistryPath {
111+
if path != "" {
112+
registryPath, err := config.FilePath(path)
113+
if err != nil {
114+
return Configuration{}, nil, nil, err
115+
}
116+
registryPaths = append(registryPaths, registryPath)
117+
}
113118
}
114119
}
115120

116-
configPath, err := config.FilePath(c.ConfigPath)
117-
if err != nil {
118-
return Configuration{}, nil, nil, err
121+
var configPaths []string
122+
for _, path := range c.ConfigPath {
123+
if path != "" {
124+
configPath, err := config.FilePath(path)
125+
if err != nil {
126+
return Configuration{}, nil, nil, err
127+
}
128+
configPaths = append(configPaths, configPath)
129+
}
119130
}
120131

121132
watcher, err := fsnotify.NewWatcher()
@@ -142,29 +153,30 @@ func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan
142153
}
143154
}
144155

145-
if configuration, err := c.readOnce(ctx); err == nil {
146-
updates <- configuration
147-
}
148-
case err, ok := <-watcher.Errors:
149-
if !ok {
150-
return
156+
configuration, err := c.readOnce(ctx)
157+
if err != nil {
158+
log("Error reading configuration:", err)
159+
continue
151160
}
152-
logf("watch error: %s", err)
161+
162+
updates <- configuration
163+
164+
case <-ctx.Done():
165+
return
153166
}
154167
}
155168
}()
156169

157-
if registryPath != "" {
158-
log("- Watching registry at", registryPath)
159-
if err := watcher.Add(registryPath); err != nil {
160-
_ = watcher.Close()
170+
// Add all registry paths to watcher
171+
for _, path := range registryPaths {
172+
if err := watcher.Add(path); err != nil && !os.IsNotExist(err) {
161173
return Configuration{}, nil, nil, err
162174
}
163175
}
164-
if configPath != "" {
165-
log("- Watching config at", configPath)
166-
if err := watcher.Add(configPath); err != nil {
167-
_ = watcher.Close()
176+
177+
// Add all config paths to watcher
178+
for _, path := range configPaths {
179+
if err := watcher.Add(path); err != nil && !os.IsNotExist(err) {
168180
return Configuration{}, nil, nil, err
169181
}
170182
}
@@ -239,41 +251,75 @@ func (c *FileBasedConfiguration) readCatalog(ctx context.Context) (catalog.Catal
239251
}
240252

241253
func (c *FileBasedConfiguration) readRegistry(ctx context.Context) (config.Registry, error) {
242-
if c.RegistryPath == "" {
254+
if len(c.RegistryPath) == 0 {
243255
return config.Registry{}, nil
244256
}
245257

246-
log(" - Reading registry from", c.RegistryPath)
247-
yaml, err := config.ReadConfigFile(ctx, c.docker, c.RegistryPath)
248-
if err != nil {
249-
return config.Registry{}, fmt.Errorf("reading registry.yaml: %w", err)
258+
mergedRegistry := config.Registry{
259+
Servers: map[string]config.Tile{},
250260
}
251261

252-
cfg, err := config.ParseRegistryConfig(yaml)
253-
if err != nil {
254-
return config.Registry{}, fmt.Errorf("parsing registry.yaml: %w", err)
262+
for _, registryPath := range c.RegistryPath {
263+
if registryPath == "" {
264+
continue
265+
}
266+
267+
log(" - Reading registry from", registryPath)
268+
yaml, err := config.ReadConfigFile(ctx, c.docker, registryPath)
269+
if err != nil {
270+
return config.Registry{}, fmt.Errorf("reading registry file %s: %w", registryPath, err)
271+
}
272+
273+
cfg, err := config.ParseRegistryConfig(yaml)
274+
if err != nil {
275+
return config.Registry{}, fmt.Errorf("parsing registry file %s: %w", registryPath, err)
276+
}
277+
278+
// Merge servers into the combined registry, checking for overlaps
279+
for serverName, tile := range cfg.Servers {
280+
if _, exists := mergedRegistry.Servers[serverName]; exists {
281+
log(fmt.Sprintf("Warning: overlapping server '%s' found in registry '%s', overwriting previous value", serverName, registryPath))
282+
}
283+
mergedRegistry.Servers[serverName] = tile
284+
}
255285
}
256286

257-
return cfg, nil
287+
return mergedRegistry, nil
258288
}
259289

260290
func (c *FileBasedConfiguration) readConfig(ctx context.Context) (map[string]map[string]any, error) {
261-
if c.ConfigPath == "" {
291+
if len(c.ConfigPath) == 0 {
262292
return map[string]map[string]any{}, nil
263293
}
264294

265-
log(" - Reading config from", c.ConfigPath)
266-
yaml, err := config.ReadConfigFile(ctx, c.docker, c.ConfigPath)
267-
if err != nil {
268-
return nil, fmt.Errorf("reading config.yaml: %w", err)
269-
}
295+
mergedConfig := map[string]map[string]any{}
270296

271-
cfg, err := config.ParseConfig(yaml)
272-
if err != nil {
273-
return nil, fmt.Errorf("parsing config.yaml: %w", err)
297+
for _, configPath := range c.ConfigPath {
298+
if configPath == "" {
299+
continue
300+
}
301+
302+
log(" - Reading config from", configPath)
303+
yaml, err := config.ReadConfigFile(ctx, c.docker, configPath)
304+
if err != nil {
305+
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
306+
}
307+
308+
cfg, err := config.ParseConfig(yaml)
309+
if err != nil {
310+
return nil, fmt.Errorf("parsing config file %s: %w", configPath, err)
311+
}
312+
313+
// Merge configs into the combined config, checking for overlaps
314+
for serverName, serverConfig := range cfg {
315+
if _, exists := mergedConfig[serverName]; exists {
316+
log(fmt.Sprintf("Warning: overlapping server config '%s' found in config file '%s', overwriting previous value", serverName, configPath))
317+
}
318+
mergedConfig[serverName] = serverConfig
319+
}
274320
}
275321

276-
return cfg, nil
322+
return mergedConfig, nil
277323
}
278324

279325
func (c *FileBasedConfiguration) readDockerDesktopSecrets(ctx context.Context, servers map[string]catalog.Server, serverNames []string) (map[string]string, error) {

0 commit comments

Comments
 (0)