Skip to content

Commit c2bf69a

Browse files
committed
Support multiple config paths
1 parent 5722077 commit c2bf69a

File tree

3 files changed

+315
-57
lines changed

3 files changed

+315
-57
lines changed

cmd/docker-mcp/client/config.yml

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ system:
77
- /Applications/Claude.app
88
- $AppData\Claude\
99
paths:
10-
linux: $HOME/.config/claude/claude_desktop_config.json
11-
darwin: $HOME/Library/Application Support/Claude/claude_desktop_config.json
12-
windows: $APPDATA\Claude\claude_desktop_config.json
10+
linux:
11+
- $HOME/.config/claude/claude_desktop_config.json
12+
darwin:
13+
- $HOME/Library/Application Support/Claude/claude_desktop_config.json
14+
windows:
15+
- $APPDATA\Claude\claude_desktop_config.json
1316
yq:
1417
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
1518
set: .mcpServers[$NAME] = $JSON
@@ -22,9 +25,12 @@ system:
2225
- $HOME/.continue
2326
- $USERPROFILE\.continue
2427
paths:
25-
linux: $HOME/.continue/config.yaml
26-
darwin: $HOME/.continue/config.yaml
27-
windows: $USERPROFILE\.continue\config.yaml
28+
linux:
29+
- $HOME/.continue/config.yaml
30+
darwin:
31+
- $HOME/.continue/config.yaml
32+
windows:
33+
- $USERPROFILE\.continue\config.yaml
2834
yq:
2935
list: .mcpServers
3036
set: .mcpServers = (.mcpServers // []) | .mcpServers += [{"name":$NAME}+$JSON]
@@ -37,9 +43,12 @@ system:
3743
- /Applications/Cursor.app
3844
- $AppData/Cursor/
3945
paths:
40-
linux: $HOME/.cursor/mcp.json
41-
darwin: $HOME/.cursor/mcp.json
42-
windows: $USERPROFILE\.cursor\mcp.json
46+
linux:
47+
- $HOME/.cursor/mcp.json
48+
darwin:
49+
- $HOME/.cursor/mcp.json
50+
windows:
51+
- $USERPROFILE\.cursor\mcp.json
4352
yq:
4453
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
4554
set: .mcpServers[$NAME] = $JSON
@@ -52,9 +61,12 @@ system:
5261
- $HOME/.gemini
5362
- $USERPROFILE\.gemini
5463
paths:
55-
linux: $HOME/.gemini/settings.json
56-
darwin: $HOME/.gemini/settings.json
57-
windows: $USERPROFILE\.gemini\settings.json
64+
linux:
65+
- $HOME/.gemini/settings.json
66+
darwin:
67+
- $HOME/.gemini/settings.json
68+
windows:
69+
- $USERPROFILE\.gemini\settings.json
5870
yq:
5971
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
6072
set: .mcpServers[$NAME] = $JSON
@@ -67,9 +79,12 @@ system:
6779
- $HOME/.config/goose
6880
- $USERPROFILE\.config\goose
6981
paths:
70-
linux: $HOME/.config/goose/config.yaml
71-
darwin: $HOME/.config/goose/config.yaml
72-
windows: $USERPROFILE\.config\goose\config.yaml
82+
linux:
83+
- $HOME/.config/goose/config.yaml
84+
darwin:
85+
- $HOME/.config/goose/config.yaml
86+
windows:
87+
- $USERPROFILE\.config\goose\config.yaml
7388
yq:
7489
list: '.extensions | to_entries | map(select(.value.bundled != true)) | map(.value + {"name": .key})'
7590
set: '.extensions[$SIMPLE_NAME] = {
@@ -91,11 +106,16 @@ system:
91106
icon: https://raw.githubusercontent.com/docker/mcp-gateway/main/img/client/lmstudio.png
92107
installCheckPaths:
93108
- $HOME/.lmstudio
109+
- $HOME/.cache/lm-studio
94110
- $USERPROFILE\.lmstudio
111+
- $USERPROFILE\.cache\lm-studio
95112
paths:
96-
linux: $HOME/.lmstudio/mcp.json
97-
darwin: $HOME/.lmstudio/mcp.json
98-
windows: $USERPROFILE\.lmstudio\mcp.json
113+
linux:
114+
- $HOME/.lmstudio/mcp.json
115+
darwin:
116+
- $HOME/.lmstudio/mcp.json
117+
windows:
118+
- $USERPROFILE\.lmstudio\mcp.json
99119
yq:
100120
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
101121
set: .mcpServers[$NAME] = $JSON
@@ -108,9 +128,12 @@ system:
108128
- $HOME/.sema4ai
109129
- $USERPROFILE\AppData\Local\sema4ai
110130
paths:
111-
linux: $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
112-
darwin: $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
113-
windows: $USERPROFILE\AppData\Local\sema4ai\sema4ai-studio\mcp_servers.json
131+
linux:
132+
- $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
133+
darwin:
134+
- $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
135+
windows:
136+
- $USERPROFILE\AppData\Local\sema4ai\sema4ai-studio\mcp_servers.json
114137
yq:
115138
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
116139
set: .mcpServers[$NAME] = $JSON+{"transport":"stdio"}

cmd/docker-mcp/client/global.go

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ type globalCfg struct {
2121
}
2222

2323
type Paths struct {
24-
Linux string `yaml:"linux"`
25-
Darwin string `yaml:"darwin"`
26-
Windows string `yaml:"windows"`
24+
Linux []string `yaml:"linux"`
25+
Darwin []string `yaml:"darwin"`
26+
Windows []string `yaml:"windows"`
2727
}
2828

29-
func (c *globalCfg) GetPathsForCurrentOS() string {
29+
func (c *globalCfg) GetPathsForCurrentOS() []string {
3030
switch runtime.GOOS {
3131
case "darwin":
3232
return c.Darwin
@@ -35,7 +35,7 @@ func (c *globalCfg) GetPathsForCurrentOS() string {
3535
case "windows":
3636
return c.Windows
3737
}
38-
return ""
38+
return []string{}
3939
}
4040

4141
func (c *globalCfg) isInstalled() (bool, error) {
@@ -58,7 +58,13 @@ type GlobalCfgProcessor struct {
5858
}
5959

6060
func NewGlobalCfgProcessor(g globalCfg) (*GlobalCfgProcessor, error) {
61-
p, err := newYQProcessor(g.YQ, g.GetPathsForCurrentOS())
61+
paths := g.GetPathsForCurrentOS()
62+
if len(paths) == 0 {
63+
return nil, fmt.Errorf("no paths configured for OS %s", runtime.GOOS)
64+
}
65+
// All paths for a client must use same file format (json/yaml) since YQ processor
66+
// determines encoding from first path but may operate on any path
67+
p, err := newYQProcessor(g.YQ, paths[0])
6268
if err != nil {
6369
return nil, err
6470
}
@@ -71,52 +77,68 @@ func NewGlobalCfgProcessor(g globalCfg) (*GlobalCfgProcessor, error) {
7177
func (c *GlobalCfgProcessor) ParseConfig() MCPClientCfg {
7278
result := MCPClientCfg{MCPClientCfgBase: MCPClientCfgBase{DisplayName: c.DisplayName, Source: c.Source, Icon: c.Icon}}
7379

74-
path := c.GetPathsForCurrentOS()
75-
if path == "" {
80+
paths := c.GetPathsForCurrentOS()
81+
if len(paths) == 0 {
7682
return result
7783
}
78-
fullPath := os.ExpandEnv(path)
7984
result.IsOsSupported = true
8085

81-
data, err := os.ReadFile(fullPath)
82-
if os.IsNotExist(err) {
83-
// it's not an error for us, it just means nothing is configured/connected
84-
installed, installCheckErr := c.isInstalled()
85-
result.IsInstalled = installed
86-
result.Err = classifyError(installCheckErr)
87-
return result
88-
}
89-
90-
// The file was found but can't be read. Because of an old bug, it could be a directory.
91-
// In which case, we want to delete it.
92-
stat, err := os.Stat(fullPath)
93-
if err == nil && stat.IsDir() {
94-
if err := os.RemoveAll(fullPath); err != nil {
95-
result.Err = classifyError(err)
86+
for _, path := range paths {
87+
fullPath := os.ExpandEnv(path)
88+
data, err := os.ReadFile(fullPath)
89+
if err == nil {
90+
result.IsInstalled = true
91+
result.setParseResult(c.p.Parse(data))
9692
return result
9793
}
98-
installed, installCheckErr := c.isInstalled()
99-
result.IsInstalled = installed
100-
result.Err = classifyError(installCheckErr)
101-
return result
102-
}
10394

104-
// config exists for us means it's installed (we then don't care if it's actually installed or not)
105-
result.IsInstalled = true
106-
if err != nil {
95+
if os.IsNotExist(err) {
96+
continue
97+
}
98+
99+
// File exists but can't be read. Because of an old bug, it could be a directory.
100+
// In which case, we want to delete it.
101+
stat, statErr := os.Stat(fullPath)
102+
if statErr == nil && stat.IsDir() {
103+
if rmErr := os.RemoveAll(fullPath); rmErr != nil {
104+
result.Err = classifyError(rmErr)
105+
return result
106+
}
107+
continue
108+
}
109+
110+
result.IsInstalled = true
107111
result.Err = classifyError(err)
108112
return result
109113
}
110-
result.setParseResult(c.p.Parse(data))
114+
115+
// No files found - check if the application is installed
116+
installed, installCheckErr := c.isInstalled()
117+
result.IsInstalled = installed
118+
result.Err = classifyError(installCheckErr)
111119
return result
112120
}
113121

114122
func (c *GlobalCfgProcessor) Update(key string, server *MCPServerSTDIO) error {
115-
file := c.GetPathsForCurrentOS()
116-
if file == "" {
123+
paths := c.GetPathsForCurrentOS()
124+
if len(paths) == 0 {
117125
return fmt.Errorf("unknown config path for OS %s", runtime.GOOS)
118126
}
119-
return updateConfig(os.ExpandEnv(file), c.p.Add, c.p.Del, key, server)
127+
128+
// Use first existing path, or first path if none exist
129+
var targetPath string
130+
for _, path := range paths {
131+
fullPath := os.ExpandEnv(path)
132+
if _, err := os.Stat(fullPath); err == nil {
133+
targetPath = fullPath
134+
break
135+
}
136+
}
137+
if targetPath == "" {
138+
targetPath = os.ExpandEnv(paths[0])
139+
}
140+
141+
return updateConfig(targetPath, c.p.Add, c.p.Del, key, server)
120142
}
121143

122144
func containsMCPDocker(in []MCPServerSTDIO) bool {

0 commit comments

Comments
 (0)