Skip to content

Commit 45835f8

Browse files
Merge branch 'feature/handle-claude-config-dir' of github.com:jandroav/mcp-gateway into jandroav-feature/handle-claude-config-dir
2 parents d910b73 + 88e0096 commit 45835f8

File tree

4 files changed

+213
-3
lines changed

4 files changed

+213
-3
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,26 @@ The MCP CLI uses several configuration files:
192192
Configuration files are typically stored in `~/.docker/mcp/`. This is in this directory that Docker Desktop's
193193
MCP Toolkit with store its configuration.
194194

195+
### Environment Variables
196+
197+
The MCP CLI respects the following environment variables for client configuration:
198+
199+
- **`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:
200+
- Maintaining separate Claude Code installations for work and personal use
201+
- Testing configuration changes in isolation
202+
- Managing multiple Claude Code profiles
203+
204+
Example usage:
205+
```bash
206+
# Set custom Claude Code configuration directory
207+
export CLAUDE_CONFIG_DIR=/path/to/custom/config
208+
209+
# Connect MCP Gateway to Claude Code
210+
docker mcp client connect claude-code --global
211+
212+
# Claude Code will now use /path/to/custom/config/.claude.json
213+
```
214+
195215
## Architecture
196216

197217
The Docker MCP CLI implements a gateway pattern:

cmd/docker-mcp/client/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ system:
2424
installCheckPaths:
2525
- $HOME/.claude
2626
- $USERPROFILE\.claude
27+
- $CLAUDE_CONFIG_DIR
2728
paths:
2829
linux:
30+
- $CLAUDE_CONFIG_DIR/.claude.json
2931
- $HOME/.claude.json
3032
darwin:
33+
- $CLAUDE_CONFIG_DIR/.claude.json
3134
- $HOME/.claude.json
3235
windows:
36+
- $CLAUDE_CONFIG_DIR\.claude.json
3337
- $USERPROFILE\.claude.json
3438
yq:
3539
list: '.mcpServers | to_entries | map(.value + {"name": .key})'

cmd/docker-mcp/client/global.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,31 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"regexp"
78
"runtime"
89
)
910

1011
const (
1112
DockerMCPCatalog = "MCP_DOCKER"
1213
)
1314

15+
var envVarRegex = regexp.MustCompile(`\$([A-Za-z_][A-Za-z0-9_]*)`)
16+
17+
// isPathValid checks if all environment variables in a path are defined and non-empty
18+
func isPathValid(path string) bool {
19+
matches := envVarRegex.FindAllStringSubmatch(path, -1)
20+
for _, match := range matches {
21+
if len(match) > 1 {
22+
varName := match[1]
23+
value, ok := os.LookupEnv(varName)
24+
if !ok || value == "" {
25+
return false
26+
}
27+
}
28+
}
29+
return true
30+
}
31+
1432
type globalCfg struct {
1533
DisplayName string `yaml:"displayName"`
1634
Source string `yaml:"source"`
@@ -125,17 +143,29 @@ func (c *GlobalCfgProcessor) Update(key string, server *MCPServerSTDIO) error {
125143
return fmt.Errorf("unknown config path for OS %s", runtime.GOOS)
126144
}
127145

128-
// Use first existing path, or first path if none exist
129-
var targetPath string
146+
// Filter out paths with undefined environment variables
147+
var validPaths []string
130148
for _, path := range paths {
149+
if isPathValid(path) {
150+
validPaths = append(validPaths, path)
151+
}
152+
}
153+
154+
if len(validPaths) == 0 {
155+
return fmt.Errorf("no valid config paths found (all paths contain undefined environment variables)")
156+
}
157+
158+
// Use first existing path, or first valid path if none exist
159+
var targetPath string
160+
for _, path := range validPaths {
131161
fullPath := os.ExpandEnv(path)
132162
if _, err := os.Stat(fullPath); err == nil {
133163
targetPath = fullPath
134164
break
135165
}
136166
}
137167
if targetPath == "" {
138-
targetPath = os.ExpandEnv(paths[0])
168+
targetPath = os.ExpandEnv(validPaths[0])
139169
}
140170

141171
return updateConfig(targetPath, c.p.Add, c.p.Del, key, server)

cmd/docker-mcp/client/global_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,159 @@ func TestGlobalCfgProcessor_NoWorkingSet(t *testing.T) {
245245
assert.True(t, result.IsMCPCatalogConnected)
246246
assert.Empty(t, result.WorkingSet)
247247
}
248+
249+
func TestIsPathValid(t *testing.T) {
250+
tests := []struct {
251+
name string
252+
path string
253+
envVars map[string]string
254+
expected bool
255+
}{
256+
{
257+
name: "no_env_vars",
258+
path: "/absolute/path/config.json",
259+
envVars: map[string]string{},
260+
expected: true,
261+
},
262+
{
263+
name: "defined_env_var",
264+
path: "$HOME/.config/app/config.json",
265+
envVars: map[string]string{"HOME": "/home/user"},
266+
expected: true,
267+
},
268+
{
269+
name: "undefined_env_var",
270+
path: "$UNDEFINED_VAR/.config/app/config.json",
271+
envVars: map[string]string{},
272+
expected: false,
273+
},
274+
{
275+
name: "empty_env_var",
276+
path: "$EMPTY_VAR/.config/app/config.json",
277+
envVars: map[string]string{"EMPTY_VAR": ""},
278+
expected: false,
279+
},
280+
{
281+
name: "multiple_defined_env_vars",
282+
path: "$HOME/$CONFIG_DIR/config.json",
283+
envVars: map[string]string{"HOME": "/home/user", "CONFIG_DIR": ".config"},
284+
expected: true,
285+
},
286+
{
287+
name: "multiple_mixed_env_vars",
288+
path: "$HOME/$UNDEFINED_VAR/config.json",
289+
envVars: map[string]string{"HOME": "/home/user"},
290+
expected: false,
291+
},
292+
{
293+
name: "windows_style_defined",
294+
path: "$USERPROFILE\\.config\\app\\config.json",
295+
envVars: map[string]string{"USERPROFILE": "C:\\Users\\user"},
296+
expected: true,
297+
},
298+
}
299+
300+
for _, tc := range tests {
301+
t.Run(tc.name, func(t *testing.T) {
302+
// Set up environment variables
303+
for k, v := range tc.envVars {
304+
t.Setenv(k, v)
305+
}
306+
307+
result := isPathValid(tc.path)
308+
assert.Equal(t, tc.expected, result)
309+
})
310+
}
311+
}
312+
313+
func TestGlobalCfgProcessor_Update_WithEnvVarPaths(t *testing.T) {
314+
tempDir := t.TempDir()
315+
316+
// Set up a custom config dir
317+
customConfigDir := filepath.Join(tempDir, "custom-config")
318+
require.NoError(t, os.MkdirAll(customConfigDir, 0o755))
319+
320+
// Create existing config in custom dir
321+
customConfigPath := filepath.Join(customConfigDir, ".claude.json")
322+
require.NoError(t, os.WriteFile(customConfigPath, []byte(`{"mcpServers": {"existing": {"command": "test"}}}`), 0o644))
323+
324+
// Set CLAUDE_CONFIG_DIR environment variable
325+
t.Setenv("CLAUDE_CONFIG_DIR", customConfigDir)
326+
327+
// Create home config path (should be ignored when CLAUDE_CONFIG_DIR is set)
328+
homeDir := filepath.Join(tempDir, "home")
329+
require.NoError(t, os.MkdirAll(homeDir, 0o755))
330+
homeConfigPath := filepath.Join(homeDir, ".claude.json")
331+
t.Setenv("HOME", homeDir)
332+
333+
cfg := newTestGlobalCfg()
334+
paths := []string{"$CLAUDE_CONFIG_DIR/.claude.json", "$HOME/.claude.json"}
335+
setPathsForCurrentOS(&cfg, paths)
336+
337+
processor, err := NewGlobalCfgProcessor(cfg)
338+
require.NoError(t, err)
339+
340+
err = processor.Update("new-server", &MCPServerSTDIO{
341+
Name: "new-server",
342+
Command: "docker",
343+
Args: []string{"mcp", "gateway", "run"},
344+
})
345+
require.NoError(t, err)
346+
347+
// Verify update went to the custom config dir
348+
content, err := os.ReadFile(customConfigPath)
349+
require.NoError(t, err)
350+
assert.Contains(t, string(content), "new-server")
351+
352+
// Verify home config was not created
353+
_, err = os.ReadFile(homeConfigPath)
354+
assert.True(t, os.IsNotExist(err), "home config should not be created when CLAUDE_CONFIG_DIR is set")
355+
}
356+
357+
func TestGlobalCfgProcessor_Update_FallbackWhenEnvVarUndefined(t *testing.T) {
358+
tempDir := t.TempDir()
359+
360+
// Set CLAUDE_CONFIG_DIR to empty - it should fall back to HOME
361+
t.Setenv("CLAUDE_CONFIG_DIR", "")
362+
363+
homeDir := filepath.Join(tempDir, "home")
364+
homeConfigPath := filepath.Join(homeDir, ".claude.json")
365+
t.Setenv("HOME", homeDir)
366+
367+
cfg := newTestGlobalCfg()
368+
paths := []string{"$CLAUDE_CONFIG_DIR/.claude.json", "$HOME/.claude.json"}
369+
setPathsForCurrentOS(&cfg, paths)
370+
371+
processor, err := NewGlobalCfgProcessor(cfg)
372+
require.NoError(t, err)
373+
374+
err = processor.Update("new-server", &MCPServerSTDIO{
375+
Name: "new-server",
376+
Command: "docker",
377+
Args: []string{"mcp", "gateway", "run"},
378+
})
379+
require.NoError(t, err)
380+
381+
// Verify update went to the home config dir
382+
content, err := os.ReadFile(homeConfigPath)
383+
require.NoError(t, err)
384+
assert.Contains(t, string(content), "new-server")
385+
}
386+
387+
func TestGlobalCfgProcessor_Update_AllPathsInvalid(t *testing.T) {
388+
cfg := newTestGlobalCfg()
389+
// Only provide paths with undefined environment variables
390+
paths := []string{"$UNDEFINED_VAR1/.config.json", "$UNDEFINED_VAR2/.config.json"}
391+
setPathsForCurrentOS(&cfg, paths)
392+
393+
processor, err := NewGlobalCfgProcessor(cfg)
394+
require.NoError(t, err)
395+
396+
err = processor.Update("new-server", &MCPServerSTDIO{
397+
Name: "new-server",
398+
Command: "docker",
399+
Args: []string{"mcp", "gateway", "run"},
400+
})
401+
require.Error(t, err)
402+
assert.ErrorContains(t, err, "no valid config paths found")
403+
}

0 commit comments

Comments
 (0)