Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ The MCP CLI uses several configuration files:

- **`docker-mcp.yaml`**: Server catalog defining available MCP servers
- **`registry.yaml`**: Registry of enabled servers
- **`config.yaml`**: Gateway configuration and options
- **`config.yaml`**: Configuration per server
- **`tools.yaml`**: Enabled tools per server

Configuration files are typically stored in `~/.docker/mcp/`. This is in this directory that Docker Desktop's
MCP Toolkit with store its configuration.
Expand Down
6 changes: 6 additions & 0 deletions cmd/docker-mcp/backup/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func Dump(ctx context.Context, docker docker.Client) ([]byte, error) {
return nil, err
}

toolsConfig, err := config.ReadTools(ctx, docker)
if err != nil {
return nil, err
}

catalogConfig, err := catalog.ReadConfig()
if err != nil {
return nil, err
Expand Down Expand Up @@ -74,6 +79,7 @@ func Dump(ctx context.Context, docker docker.Client) ([]byte, error) {
Registry: string(registryContent),
Catalog: string(catalogContent),
CatalogFiles: catalogFiles,
Tools: string(toolsConfig),
Secrets: secrets,
Policy: policy,
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/docker-mcp/backup/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func Restore(ctx context.Context, backupData []byte) error {
if err := config.WriteRegistry([]byte(backup.Registry)); err != nil {
return err
}
if err := config.WriteTools([]byte(backup.Tools)); err != nil {
return err
}

catalogBefore, err := catalog.ReadConfig()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/docker-mcp/backup/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Backup struct {
Registry string `json:"registry"`
Catalog string `json:"catalog"`
CatalogFiles map[string]string `json:"catalogFiles"`
Tools string `json:"tools"`
Secrets []desktop.Secret `json:"secrets"`
Policy string `json:"policy"`
}
5 changes: 5 additions & 0 deletions cmd/docker-mcp/commands/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
var additionalCatalogs []string
var additionalRegistries []string
var additionalConfigs []string
var additionalToolsConfig []string
if os.Getenv("DOCKER_MCP_IN_CONTAINER") == "1" {
// In-container.
options = gateway.Config{
Expand All @@ -43,6 +44,7 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
CatalogPath: []string{"docker-mcp.yaml"},
RegistryPath: []string{"registry.yaml"},
ConfigPath: []string{"config.yaml"},
ToolsPath: []string{"tools.yaml"},
SecretsPath: "docker-desktop",
Options: gateway.Options{
Cpus: 1,
Expand Down Expand Up @@ -81,6 +83,7 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
options.CatalogPath = append(options.CatalogPath, additionalCatalogs...)
options.RegistryPath = append(options.RegistryPath, additionalRegistries...)
options.ConfigPath = append(options.ConfigPath, additionalConfigs...)
options.ToolsPath = append(options.ToolsPath, additionalToolsConfig...)

return gateway.NewGateway(options, docker).Run(cmd.Context())
},
Expand All @@ -93,6 +96,8 @@ func gatewayCommand(docker docker.Client) *cobra.Command {
runCmd.Flags().StringSliceVar(&additionalRegistries, "additional-registry", nil, "Additional registry paths to merge with the default registry.yaml")
runCmd.Flags().StringSliceVar(&options.ConfigPath, "config", options.ConfigPath, "Paths to the config files (absolute or relative to ~/.docker/mcp/)")
runCmd.Flags().StringSliceVar(&additionalConfigs, "additional-config", nil, "Additional config paths to merge with the default config.yaml")
runCmd.Flags().StringSliceVar(&options.ToolsPath, "tools-config", options.ToolsPath, "Paths to the tools files (absolute or relative to ~/.docker/mcp/)")
runCmd.Flags().StringSliceVar(&additionalToolsConfig, "additional-tools-config", nil, "Additional tools paths to merge with the default tools.yaml")
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 Desktop's secrets API)")
runCmd.Flags().StringSliceVar(&options.ToolNames, "tools", options.ToolNames, "List of tools to enable")
runCmd.Flags().StringArrayVar(&options.Interceptors, "interceptor", options.Interceptors, "List of interceptors to use (format: when:type:path, e.g. 'before:exec:/bin/path')")
Expand Down
2 changes: 1 addition & 1 deletion cmd/docker-mcp/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
cmd.AddCommand(policyCommand())
cmd.AddCommand(secretCommand(dockerClient))
cmd.AddCommand(serverCommand(dockerClient))
cmd.AddCommand(toolsCommand())
cmd.AddCommand(toolsCommand(dockerClient))
cmd.AddCommand(versionCommand())

if os.Getenv("DOCKER_MCP_SHOW_HIDDEN") == "1" {
Expand Down
29 changes: 27 additions & 2 deletions cmd/docker-mcp/commands/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package commands
import (
"github.com/spf13/cobra"

"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/docker"
"github.com/docker/mcp-gateway/cmd/docker-mcp/tools"
)

func toolsCommand() *cobra.Command {
func toolsCommand(docker docker.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "tools",
Short: "List/count/call MCP tools",
Short: "Manage tools",
}

var (
Expand Down Expand Up @@ -58,5 +59,29 @@ func toolsCommand() *cobra.Command {
},
})

var enableServerName string
enableCmd := &cobra.Command{
Use: "enable [tool1] [tool2] ...",
Short: "enable one or more tools",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return tools.Enable(cmd.Context(), docker, args, enableServerName)
},
}
enableCmd.Flags().StringVar(&enableServerName, "server", "", "Specify which server provides the tools (optional, will auto-discover if not provided)")
cmd.AddCommand(enableCmd)

var disableServerName string
disableCmd := &cobra.Command{
Use: "disable [tool1] [tool2] ...",
Short: "disable one or more tools",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return tools.Disable(cmd.Context(), docker, args, disableServerName)
},
}
disableCmd.Flags().StringVar(&disableServerName, "server", "", "Specify which server provides the tools (optional, will auto-discover if not provided)")
cmd.AddCommand(disableCmd)

return cmd
}
8 changes: 8 additions & 0 deletions cmd/docker-mcp/internal/config/readwrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/user"
)

func ReadTools(ctx context.Context, docker docker.Client) ([]byte, error) {
return ReadConfigFile(ctx, docker, "tools.yaml")
}

func ReadConfig(ctx context.Context, docker docker.Client) ([]byte, error) {
return ReadConfigFile(ctx, docker, "config.yaml")
}
Expand All @@ -37,6 +41,10 @@ func ReadCatalogFile(name string) ([]byte, error) {
return readFileOrEmpty(path)
}

func WriteTools(content []byte) error {
return writeConfigFile("tools.yaml", content)
}

func WriteConfig(content []byte) error {
return writeConfigFile("config.yaml", content)
}
Expand Down
22 changes: 22 additions & 0 deletions cmd/docker-mcp/internal/config/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package config

import (
"gopkg.in/yaml.v3"
)

type ToolsConfig struct {
ServerTools map[string][]string `yaml:",inline"`
}

func ParseToolsConfig(toolsYaml []byte) (ToolsConfig, error) {
var toolsConfig ToolsConfig
if err := yaml.Unmarshal(toolsYaml, &toolsConfig); err != nil {
return ToolsConfig{}, err
}

if toolsConfig.ServerTools == nil {
toolsConfig.ServerTools = make(map[string][]string)
}

return toolsConfig, nil
}
14 changes: 10 additions & 4 deletions cmd/docker-mcp/internal/gateway/capabilitites.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"runtime"
"slices"
"strings"
"sync"

Expand Down Expand Up @@ -51,7 +52,7 @@ func (g *Gateway) listCapabilities(ctx context.Context, configuration Configurat
logf(" > Can't list tools %s: %s", serverConfig.Name, err)
} else {
for _, tool := range tools.Tools {
if !isToolEnabled(serverConfig.Name, serverConfig.Spec.Image, tool.Name, g.ToolNames) {
if !isToolEnabled(configuration, serverConfig.Name, serverConfig.Spec.Image, tool.Name, g.ToolNames) {
continue
}
capabilities.Tools = append(capabilities.Tools, server.ServerTool{
Expand Down Expand Up @@ -120,7 +121,7 @@ func (g *Gateway) listCapabilities(ctx context.Context, configuration Configurat
var capabilities Capabilities

for _, tool := range *toolGroup {
if !isToolEnabled(serverName, "", tool.Name, g.ToolNames) {
if !isToolEnabled(configuration, serverName, "", tool.Name, g.ToolNames) {
continue
}

Expand Down Expand Up @@ -188,9 +189,14 @@ func (c *Capabilities) PromptNames() []string {
return names
}

func isToolEnabled(serverName, serverImage, toolName string, enabledTools []string) bool {
func isToolEnabled(configuration Configuration, serverName, serverImage, toolName string, enabledTools []string) bool {
if len(enabledTools) == 0 {
return true
tools, exists := configuration.tools.ServerTools[serverName]
if !exists {
return true
}

return slices.Contains(tools, toolName)
}

for _, enabled := range enabledTools {
Expand Down
1 change: 1 addition & 0 deletions cmd/docker-mcp/internal/gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Config struct {
CatalogPath []string
ConfigPath []string
RegistryPath []string
ToolsPath []string
SecretsPath string
}

Expand Down
63 changes: 63 additions & 0 deletions cmd/docker-mcp/internal/gateway/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Configuration struct {
serverNames []string
servers map[string]catalog.Server
config map[string]map[string]any
tools config.ToolsConfig
secrets map[string]string
}

Expand Down Expand Up @@ -92,6 +93,7 @@ type FileBasedConfiguration struct {
ServerNames []string // Takes precedence over the RegistryPath
RegistryPath []string
ConfigPath []string
ToolsPath []string
SecretsPath string // Optional, if not set, use Docker Desktop's secrets API
Watch bool
Central bool
Expand Down Expand Up @@ -132,6 +134,17 @@ func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan
}
}

var toolsPaths []string
for _, path := range c.ToolsPath {
if path != "" {
toolsPath, err := config.FilePath(path)
if err != nil {
return Configuration{}, nil, nil, err
}
toolsPaths = append(toolsPaths, toolsPath)
}
}

watcher, err := fsnotify.NewWatcher()
if err != nil {
return Configuration{}, nil, nil, err
Expand Down Expand Up @@ -184,6 +197,13 @@ func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan
}
}

// Add all tools paths to watcher
for _, path := range toolsPaths {
if err := watcher.Add(path); err != nil && !os.IsNotExist(err) {
return Configuration{}, nil, nil, err
}
}

return configuration, updates, watcher.Close, nil
}

Expand Down Expand Up @@ -217,6 +237,11 @@ func (c *FileBasedConfiguration) readOnce(ctx context.Context) (Configuration, e
return Configuration{}, fmt.Errorf("reading config: %w", err)
}

serverToolsConfig, err := c.readToolsConfig(ctx)
if err != nil {
return Configuration{}, fmt.Errorf("reading tools: %w", err)
}

// TODO(dga): How do we know which secrets to read, in Central mode?
var secrets map[string]string
if c.SecretsPath == "docker-desktop" {
Expand Down Expand Up @@ -247,6 +272,7 @@ func (c *FileBasedConfiguration) readOnce(ctx context.Context) (Configuration, e
serverNames: serverNames,
servers: servers,
config: serversConfig,
tools: serverToolsConfig,
secrets: secrets,
}, nil
}
Expand Down Expand Up @@ -328,6 +354,43 @@ func (c *FileBasedConfiguration) readConfig(ctx context.Context) (map[string]map
return mergedConfig, nil
}

func (c *FileBasedConfiguration) readToolsConfig(ctx context.Context) (config.ToolsConfig, error) {
if len(c.ToolsPath) == 0 {
return config.ToolsConfig{}, nil
}

mergedToolsConfig := config.ToolsConfig{
ServerTools: make(map[string][]string),
}

for _, toolsPath := range c.ToolsPath {
if toolsPath == "" {
continue
}

log(" - Reading tools from", toolsPath)
yaml, err := config.ReadConfigFile(ctx, c.docker, toolsPath)
if err != nil {
return config.ToolsConfig{}, fmt.Errorf("reading tools file %s: %w", toolsPath, err)
}

toolsConfig, err := config.ParseToolsConfig(yaml)
if err != nil {
return config.ToolsConfig{}, fmt.Errorf("parsing tools file %s: %w", toolsPath, err)
}

// Merge tools into the combined tools, checking for overlaps
for serverName, serverTools := range toolsConfig.ServerTools {
if _, exists := mergedToolsConfig.ServerTools[serverName]; exists {
log(fmt.Sprintf("Warning: overlapping server tools '%s' found in tools file '%s', overwriting previous value", serverName, toolsPath))
}
mergedToolsConfig.ServerTools[serverName] = serverTools
}
}

return mergedToolsConfig, nil
}

func (c *FileBasedConfiguration) readDockerDesktopSecrets(ctx context.Context, servers map[string]catalog.Server, serverNames []string) (map[string]string, error) {
var secretNames []string
for _, serverName := range serverNames {
Expand Down
1 change: 1 addition & 0 deletions cmd/docker-mcp/internal/gateway/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func NewGateway(config Config, docker docker.Client) *Gateway {
RegistryPath: config.RegistryPath,
ConfigPath: config.ConfigPath,
SecretsPath: config.SecretsPath,
ToolsPath: config.ToolsPath,
Watch: config.Watch,
Central: config.Central,
docker: docker,
Expand Down
Loading