Skip to content

Commit 88e0096

Browse files
committed
Add support for custom configuration directory via CLAUDE_CONFIG_DIR environment variable
- Introduced environment variable handling in the MCP CLI for custom config paths. - Updated config.yml to include CLAUDE_CONFIG_DIR in path checks. - Implemented isPathValid function to validate paths with environment variables. - Added tests for path validation and configuration updates with environment variables.
1 parent b884649 commit 88e0096

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
@@ -211,3 +211,159 @@ func TestGlobalCfgProcessor_SinglePath(t *testing.T) {
211211
assert.True(t, result.IsOsSupported)
212212
assert.Nil(t, result.Err)
213213
}
214+
215+
func TestIsPathValid(t *testing.T) {
216+
tests := []struct {
217+
name string
218+
path string
219+
envVars map[string]string
220+
expected bool
221+
}{
222+
{
223+
name: "no_env_vars",
224+
path: "/absolute/path/config.json",
225+
envVars: map[string]string{},
226+
expected: true,
227+
},
228+
{
229+
name: "defined_env_var",
230+
path: "$HOME/.config/app/config.json",
231+
envVars: map[string]string{"HOME": "/home/user"},
232+
expected: true,
233+
},
234+
{
235+
name: "undefined_env_var",
236+
path: "$UNDEFINED_VAR/.config/app/config.json",
237+
envVars: map[string]string{},
238+
expected: false,
239+
},
240+
{
241+
name: "empty_env_var",
242+
path: "$EMPTY_VAR/.config/app/config.json",
243+
envVars: map[string]string{"EMPTY_VAR": ""},
244+
expected: false,
245+
},
246+
{
247+
name: "multiple_defined_env_vars",
248+
path: "$HOME/$CONFIG_DIR/config.json",
249+
envVars: map[string]string{"HOME": "/home/user", "CONFIG_DIR": ".config"},
250+
expected: true,
251+
},
252+
{
253+
name: "multiple_mixed_env_vars",
254+
path: "$HOME/$UNDEFINED_VAR/config.json",
255+
envVars: map[string]string{"HOME": "/home/user"},
256+
expected: false,
257+
},
258+
{
259+
name: "windows_style_defined",
260+
path: "$USERPROFILE\\.config\\app\\config.json",
261+
envVars: map[string]string{"USERPROFILE": "C:\\Users\\user"},
262+
expected: true,
263+
},
264+
}
265+
266+
for _, tc := range tests {
267+
t.Run(tc.name, func(t *testing.T) {
268+
// Set up environment variables
269+
for k, v := range tc.envVars {
270+
t.Setenv(k, v)
271+
}
272+
273+
result := isPathValid(tc.path)
274+
assert.Equal(t, tc.expected, result)
275+
})
276+
}
277+
}
278+
279+
func TestGlobalCfgProcessor_Update_WithEnvVarPaths(t *testing.T) {
280+
tempDir := t.TempDir()
281+
282+
// Set up a custom config dir
283+
customConfigDir := filepath.Join(tempDir, "custom-config")
284+
require.NoError(t, os.MkdirAll(customConfigDir, 0o755))
285+
286+
// Create existing config in custom dir
287+
customConfigPath := filepath.Join(customConfigDir, ".claude.json")
288+
require.NoError(t, os.WriteFile(customConfigPath, []byte(`{"mcpServers": {"existing": {"command": "test"}}}`), 0o644))
289+
290+
// Set CLAUDE_CONFIG_DIR environment variable
291+
t.Setenv("CLAUDE_CONFIG_DIR", customConfigDir)
292+
293+
// Create home config path (should be ignored when CLAUDE_CONFIG_DIR is set)
294+
homeDir := filepath.Join(tempDir, "home")
295+
require.NoError(t, os.MkdirAll(homeDir, 0o755))
296+
homeConfigPath := filepath.Join(homeDir, ".claude.json")
297+
t.Setenv("HOME", homeDir)
298+
299+
cfg := newTestGlobalCfg()
300+
paths := []string{"$CLAUDE_CONFIG_DIR/.claude.json", "$HOME/.claude.json"}
301+
setPathsForCurrentOS(&cfg, paths)
302+
303+
processor, err := NewGlobalCfgProcessor(cfg)
304+
require.NoError(t, err)
305+
306+
err = processor.Update("new-server", &MCPServerSTDIO{
307+
Name: "new-server",
308+
Command: "docker",
309+
Args: []string{"mcp", "gateway", "run"},
310+
})
311+
require.NoError(t, err)
312+
313+
// Verify update went to the custom config dir
314+
content, err := os.ReadFile(customConfigPath)
315+
require.NoError(t, err)
316+
assert.Contains(t, string(content), "new-server")
317+
318+
// Verify home config was not created
319+
_, err = os.ReadFile(homeConfigPath)
320+
assert.True(t, os.IsNotExist(err), "home config should not be created when CLAUDE_CONFIG_DIR is set")
321+
}
322+
323+
func TestGlobalCfgProcessor_Update_FallbackWhenEnvVarUndefined(t *testing.T) {
324+
tempDir := t.TempDir()
325+
326+
// Set CLAUDE_CONFIG_DIR to empty - it should fall back to HOME
327+
t.Setenv("CLAUDE_CONFIG_DIR", "")
328+
329+
homeDir := filepath.Join(tempDir, "home")
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 home config dir
348+
content, err := os.ReadFile(homeConfigPath)
349+
require.NoError(t, err)
350+
assert.Contains(t, string(content), "new-server")
351+
}
352+
353+
func TestGlobalCfgProcessor_Update_AllPathsInvalid(t *testing.T) {
354+
cfg := newTestGlobalCfg()
355+
// Only provide paths with undefined environment variables
356+
paths := []string{"$UNDEFINED_VAR1/.config.json", "$UNDEFINED_VAR2/.config.json"}
357+
setPathsForCurrentOS(&cfg, paths)
358+
359+
processor, err := NewGlobalCfgProcessor(cfg)
360+
require.NoError(t, err)
361+
362+
err = processor.Update("new-server", &MCPServerSTDIO{
363+
Name: "new-server",
364+
Command: "docker",
365+
Args: []string{"mcp", "gateway", "run"},
366+
})
367+
require.Error(t, err)
368+
assert.ErrorContains(t, err, "no valid config paths found")
369+
}

0 commit comments

Comments
 (0)