Skip to content
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,26 @@ The MCP CLI uses several configuration files:
Configuration files are typically stored in `~/.docker/mcp/`. This is in this directory that Docker Desktop's
MCP Toolkit with store its configuration.

### Environment Variables

The MCP CLI respects the following environment variables for client configuration:

- **`CLAUDE_CONFIG_DIR`**: Override the default Claude Code configuration directory (`~/.claude`). When set, Claude Code will use `$CLAUDE_CONFIG_DIR/.claude.json` instead of `~/.claude.json` for its MCP server configuration. This is useful for:
- Maintaining separate Claude Code installations for work and personal use
- Testing configuration changes in isolation
- Managing multiple Claude Code profiles

Example usage:
```bash
# Set custom Claude Code configuration directory
export CLAUDE_CONFIG_DIR=/path/to/custom/config

# Connect MCP Gateway to Claude Code
docker mcp client connect claude-code --global

# Claude Code will now use /path/to/custom/config/.claude.json
```

## Architecture

The Docker MCP CLI implements a gateway pattern:
Expand Down
4 changes: 4 additions & 0 deletions cmd/docker-mcp/client/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ system:
installCheckPaths:
- $HOME/.claude
- $USERPROFILE\.claude
- $CLAUDE_CONFIG_DIR
paths:
linux:
- $CLAUDE_CONFIG_DIR/.claude.json
- $HOME/.claude.json
darwin:
- $CLAUDE_CONFIG_DIR/.claude.json
- $HOME/.claude.json
windows:
- $CLAUDE_CONFIG_DIR\.claude.json
- $USERPROFILE\.claude.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
Expand Down
36 changes: 33 additions & 3 deletions cmd/docker-mcp/client/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,31 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
)

const (
DockerMCPCatalog = "MCP_DOCKER"
)

var envVarRegex = regexp.MustCompile(`\$([A-Za-z_][A-Za-z0-9_]*)`)

// isPathValid checks if all environment variables in a path are defined and non-empty
func isPathValid(path string) bool {
matches := envVarRegex.FindAllStringSubmatch(path, -1)
for _, match := range matches {
if len(match) > 1 {
varName := match[1]
value, ok := os.LookupEnv(varName)
if !ok || value == "" {
return false
}
}
}
return true
}

type globalCfg struct {
DisplayName string `yaml:"displayName"`
Source string `yaml:"source"`
Expand Down Expand Up @@ -125,17 +143,29 @@ func (c *GlobalCfgProcessor) Update(key string, server *MCPServerSTDIO) error {
return fmt.Errorf("unknown config path for OS %s", runtime.GOOS)
}

// Use first existing path, or first path if none exist
var targetPath string
// Filter out paths with undefined environment variables
var validPaths []string
for _, path := range paths {
if isPathValid(path) {
validPaths = append(validPaths, path)
}
}

if len(validPaths) == 0 {
return fmt.Errorf("no valid config paths found (all paths contain undefined environment variables)")
}

// Use first existing path, or first valid path if none exist
var targetPath string
for _, path := range validPaths {
fullPath := os.ExpandEnv(path)
if _, err := os.Stat(fullPath); err == nil {
targetPath = fullPath
break
}
}
if targetPath == "" {
targetPath = os.ExpandEnv(paths[0])
targetPath = os.ExpandEnv(validPaths[0])
}

return updateConfig(targetPath, c.p.Add, c.p.Del, key, server)
Expand Down
156 changes: 156 additions & 0 deletions cmd/docker-mcp/client/global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,159 @@ func TestGlobalCfgProcessor_SinglePath(t *testing.T) {
assert.True(t, result.IsOsSupported)
assert.Nil(t, result.Err)
}

func TestIsPathValid(t *testing.T) {
tests := []struct {
name string
path string
envVars map[string]string
expected bool
}{
{
name: "no_env_vars",
path: "/absolute/path/config.json",
envVars: map[string]string{},
expected: true,
},
{
name: "defined_env_var",
path: "$HOME/.config/app/config.json",
envVars: map[string]string{"HOME": "/home/user"},
expected: true,
},
{
name: "undefined_env_var",
path: "$UNDEFINED_VAR/.config/app/config.json",
envVars: map[string]string{},
expected: false,
},
{
name: "empty_env_var",
path: "$EMPTY_VAR/.config/app/config.json",
envVars: map[string]string{"EMPTY_VAR": ""},
expected: false,
},
{
name: "multiple_defined_env_vars",
path: "$HOME/$CONFIG_DIR/config.json",
envVars: map[string]string{"HOME": "/home/user", "CONFIG_DIR": ".config"},
expected: true,
},
{
name: "multiple_mixed_env_vars",
path: "$HOME/$UNDEFINED_VAR/config.json",
envVars: map[string]string{"HOME": "/home/user"},
expected: false,
},
{
name: "windows_style_defined",
path: "$USERPROFILE\\.config\\app\\config.json",
envVars: map[string]string{"USERPROFILE": "C:\\Users\\user"},
expected: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Set up environment variables
for k, v := range tc.envVars {
t.Setenv(k, v)
}

result := isPathValid(tc.path)
assert.Equal(t, tc.expected, result)
})
}
}

func TestGlobalCfgProcessor_Update_WithEnvVarPaths(t *testing.T) {
tempDir := t.TempDir()

// Set up a custom config dir
customConfigDir := filepath.Join(tempDir, "custom-config")
require.NoError(t, os.MkdirAll(customConfigDir, 0o755))

// Create existing config in custom dir
customConfigPath := filepath.Join(customConfigDir, ".claude.json")
require.NoError(t, os.WriteFile(customConfigPath, []byte(`{"mcpServers": {"existing": {"command": "test"}}}`), 0o644))

// Set CLAUDE_CONFIG_DIR environment variable
t.Setenv("CLAUDE_CONFIG_DIR", customConfigDir)

// Create home config path (should be ignored when CLAUDE_CONFIG_DIR is set)
homeDir := filepath.Join(tempDir, "home")
require.NoError(t, os.MkdirAll(homeDir, 0o755))
homeConfigPath := filepath.Join(homeDir, ".claude.json")
t.Setenv("HOME", homeDir)

cfg := newTestGlobalCfg()
paths := []string{"$CLAUDE_CONFIG_DIR/.claude.json", "$HOME/.claude.json"}
setPathsForCurrentOS(&cfg, paths)

processor, err := NewGlobalCfgProcessor(cfg)
require.NoError(t, err)

err = processor.Update("new-server", &MCPServerSTDIO{
Name: "new-server",
Command: "docker",
Args: []string{"mcp", "gateway", "run"},
})
require.NoError(t, err)

// Verify update went to the custom config dir
content, err := os.ReadFile(customConfigPath)
require.NoError(t, err)
assert.Contains(t, string(content), "new-server")

// Verify home config was not created
_, err = os.ReadFile(homeConfigPath)
assert.True(t, os.IsNotExist(err), "home config should not be created when CLAUDE_CONFIG_DIR is set")
}

func TestGlobalCfgProcessor_Update_FallbackWhenEnvVarUndefined(t *testing.T) {
tempDir := t.TempDir()

// Set CLAUDE_CONFIG_DIR to empty - it should fall back to HOME
t.Setenv("CLAUDE_CONFIG_DIR", "")

homeDir := filepath.Join(tempDir, "home")
homeConfigPath := filepath.Join(homeDir, ".claude.json")
t.Setenv("HOME", homeDir)

cfg := newTestGlobalCfg()
paths := []string{"$CLAUDE_CONFIG_DIR/.claude.json", "$HOME/.claude.json"}
setPathsForCurrentOS(&cfg, paths)

processor, err := NewGlobalCfgProcessor(cfg)
require.NoError(t, err)

err = processor.Update("new-server", &MCPServerSTDIO{
Name: "new-server",
Command: "docker",
Args: []string{"mcp", "gateway", "run"},
})
require.NoError(t, err)

// Verify update went to the home config dir
content, err := os.ReadFile(homeConfigPath)
require.NoError(t, err)
assert.Contains(t, string(content), "new-server")
}

func TestGlobalCfgProcessor_Update_AllPathsInvalid(t *testing.T) {
cfg := newTestGlobalCfg()
// Only provide paths with undefined environment variables
paths := []string{"$UNDEFINED_VAR1/.config.json", "$UNDEFINED_VAR2/.config.json"}
setPathsForCurrentOS(&cfg, paths)

processor, err := NewGlobalCfgProcessor(cfg)
require.NoError(t, err)

err = processor.Update("new-server", &MCPServerSTDIO{
Name: "new-server",
Command: "docker",
Args: []string{"mcp", "gateway", "run"},
})
require.Error(t, err)
assert.ErrorContains(t, err, "no valid config paths found")
}