Skip to content

Support multiple config directories for a client #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
68 changes: 47 additions & 21 deletions cmd/docker-mcp/client/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ system:
- /Applications/Claude.app
- $AppData\Claude\
paths:
linux: $HOME/.config/claude/claude_desktop_config.json
darwin: $HOME/Library/Application Support/Claude/claude_desktop_config.json
windows: $APPDATA\Claude\claude_desktop_config.json
linux:
- $HOME/.config/claude/claude_desktop_config.json
darwin:
- $HOME/Library/Application Support/Claude/claude_desktop_config.json
windows:
- $APPDATA\Claude\claude_desktop_config.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
set: .mcpServers[$NAME] = $JSON
Expand All @@ -22,9 +25,12 @@ system:
- $HOME/.continue
- $USERPROFILE\.continue
paths:
linux: $HOME/.continue/config.yaml
darwin: $HOME/.continue/config.yaml
windows: $USERPROFILE\.continue\config.yaml
linux:
- $HOME/.continue/config.yaml
darwin:
- $HOME/.continue/config.yaml
windows:
- $USERPROFILE\.continue\config.yaml
yq:
list: .mcpServers
set: .mcpServers = (.mcpServers // []) | .mcpServers += [{"name":$NAME}+$JSON]
Expand All @@ -37,9 +43,12 @@ system:
- /Applications/Cursor.app
- $AppData/Cursor/
paths:
linux: $HOME/.cursor/mcp.json
darwin: $HOME/.cursor/mcp.json
windows: $USERPROFILE\.cursor\mcp.json
linux:
- $HOME/.cursor/mcp.json
darwin:
- $HOME/.cursor/mcp.json
windows:
- $USERPROFILE\.cursor\mcp.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
set: .mcpServers[$NAME] = $JSON
Expand All @@ -52,9 +61,12 @@ system:
- $HOME/.gemini
- $USERPROFILE\.gemini
paths:
linux: $HOME/.gemini/settings.json
darwin: $HOME/.gemini/settings.json
windows: $USERPROFILE\.gemini\settings.json
linux:
- $HOME/.gemini/settings.json
darwin:
- $HOME/.gemini/settings.json
windows:
- $USERPROFILE\.gemini\settings.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
set: .mcpServers[$NAME] = $JSON
Expand All @@ -67,9 +79,12 @@ system:
- $HOME/.config/goose
- $USERPROFILE\.config\goose
paths:
linux: $HOME/.config/goose/config.yaml
darwin: $HOME/.config/goose/config.yaml
windows: $USERPROFILE\.config\goose\config.yaml
linux:
- $HOME/.config/goose/config.yaml
darwin:
- $HOME/.config/goose/config.yaml
windows:
- $USERPROFILE\.config\goose\config.yaml
yq:
list: '.extensions | to_entries | map(select(.value.bundled != true)) | map(.value + {"name": .key})'
set: '.extensions[$SIMPLE_NAME] = {
Expand All @@ -91,11 +106,19 @@ system:
icon: https://raw.githubusercontent.com/docker/mcp-gateway/main/img/client/lmstudio.png
installCheckPaths:
- $HOME/.lmstudio
- $HOME/.cache/lm-studio
- $USERPROFILE\.lmstudio
- $USERPROFILE\.cache\lm-studio
paths:
linux: $HOME/.lmstudio/mcp.json
darwin: $HOME/.lmstudio/mcp.json
windows: $USERPROFILE\.lmstudio\mcp.json
linux:
- $HOME/.lmstudio/mcp.json
- $HOME/.cache/lm-studio/mcp.json
darwin:
- $HOME/.lmstudio/mcp.json
- $HOME/.cache/lm-studio/mcp.json
windows:
- $USERPROFILE\.lmstudio\mcp.json
- $USERPROFILE\.cache\lm-studio\mcp.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
set: .mcpServers[$NAME] = $JSON
Expand All @@ -108,9 +131,12 @@ system:
- $HOME/.sema4ai
- $USERPROFILE\AppData\Local\sema4ai
paths:
linux: $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
darwin: $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
windows: $USERPROFILE\AppData\Local\sema4ai\sema4ai-studio\mcp_servers.json
linux:
- $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
darwin:
- $HOME/.sema4ai/sema4ai-studio/mcp_servers.json
windows:
- $USERPROFILE\AppData\Local\sema4ai\sema4ai-studio\mcp_servers.json
yq:
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
set: .mcpServers[$NAME] = $JSON+{"transport":"stdio"}
Expand Down
94 changes: 58 additions & 36 deletions cmd/docker-mcp/client/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ type globalCfg struct {
}

type Paths struct {
Linux string `yaml:"linux"`
Darwin string `yaml:"darwin"`
Windows string `yaml:"windows"`
Linux []string `yaml:"linux"`
Darwin []string `yaml:"darwin"`
Windows []string `yaml:"windows"`
}

func (c *globalCfg) GetPathsForCurrentOS() string {
func (c *globalCfg) GetPathsForCurrentOS() []string {
switch runtime.GOOS {
case "darwin":
return c.Darwin
Expand All @@ -35,7 +35,7 @@ func (c *globalCfg) GetPathsForCurrentOS() string {
case "windows":
return c.Windows
}
return ""
return []string{}
}

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

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

path := c.GetPathsForCurrentOS()
if path == "" {
paths := c.GetPathsForCurrentOS()
if len(paths) == 0 {
return result
}
fullPath := os.ExpandEnv(path)
result.IsOsSupported = true

data, err := os.ReadFile(fullPath)
if os.IsNotExist(err) {
// it's not an error for us, it just means nothing is configured/connected
installed, installCheckErr := c.isInstalled()
result.IsInstalled = installed
result.Err = classifyError(installCheckErr)
return result
}

// The file was found but can't be read. Because of an old bug, it could be a directory.
// In which case, we want to delete it.
stat, err := os.Stat(fullPath)
if err == nil && stat.IsDir() {
if err := os.RemoveAll(fullPath); err != nil {
result.Err = classifyError(err)
for _, path := range paths {
fullPath := os.ExpandEnv(path)
data, err := os.ReadFile(fullPath)
if err == nil {
result.IsInstalled = true
result.setParseResult(c.p.Parse(data))
return result
}
installed, installCheckErr := c.isInstalled()
result.IsInstalled = installed
result.Err = classifyError(installCheckErr)
return result
}

// config exists for us means it's installed (we then don't care if it's actually installed or not)
result.IsInstalled = true
if err != nil {
if os.IsNotExist(err) {
continue
}

// File exists but can't be read. Because of an old bug, it could be a directory.
// In which case, we want to delete it.
stat, statErr := os.Stat(fullPath)
if statErr == nil && stat.IsDir() {
if rmErr := os.RemoveAll(fullPath); rmErr != nil {
result.Err = classifyError(rmErr)
return result
}
continue
}

result.IsInstalled = true
result.Err = classifyError(err)
return result
}
result.setParseResult(c.p.Parse(data))

// No files found - check if the application is installed
installed, installCheckErr := c.isInstalled()
result.IsInstalled = installed
result.Err = classifyError(installCheckErr)
return result
}

func (c *GlobalCfgProcessor) Update(key string, server *MCPServerSTDIO) error {
file := c.GetPathsForCurrentOS()
if file == "" {
paths := c.GetPathsForCurrentOS()
if len(paths) == 0 {
return fmt.Errorf("unknown config path for OS %s", runtime.GOOS)
}
return updateConfig(os.ExpandEnv(file), c.p.Add, c.p.Del, key, server)

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

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

func containsMCPDocker(in []MCPServerSTDIO) bool {
Expand Down
Loading