Skip to content

Commit 05d0fbf

Browse files
authored
Merge pull request #246 from dgageot/rewrite-mcp-clients
Rewrite the MCP clients connect/disconnect code
2 parents 135ca0a + 503f8e6 commit 05d0fbf

30 files changed

+1203
-661
lines changed

src/extension/host-binary/cmd/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import (
99
"slices"
1010
"syscall"
1111

12-
"github.com/docker/labs-ai-tools-for-devs/cmd/commands"
1312
"github.com/docker/labs-ai-tools-for-devs/pkg/client"
13+
"github.com/docker/labs-ai-tools-for-devs/pkg/clients"
14+
"github.com/docker/labs-ai-tools-for-devs/pkg/commands"
1415
secretsapi "github.com/docker/labs-ai-tools-for-devs/pkg/generated/go/client/secrets"
1516
"github.com/docker/labs-ai-tools-for-devs/pkg/paths"
1617
"github.com/spf13/cobra"
1718
)
1819

1920
func main() {
21+
// We need to preserve CWD as paths.Init will change it.
22+
originalCwd, err := os.Getwd()
23+
if err != nil {
24+
fmt.Println(err)
25+
return
26+
}
27+
2028
ctx, closeFunc := newSigContext()
2129
defer closeFunc()
2230
paths.Init(paths.OnHost)
@@ -30,6 +38,7 @@ func main() {
3038
cmd.AddCommand(commands.CurrentUser(ctx))
3139
cmd.AddCommand(commands.ReadFromVolume(ctx))
3240
cmd.AddCommand(commands.WriteToVolume(ctx))
41+
cmd.AddCommand(clients.NewClientCmd(ctx, originalCwd))
3342
if err := cmd.Execute(); err != nil {
3443
fmt.Println(err)
3544
os.Exit(1)

src/extension/host-binary/go.mod

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,27 @@ require (
99
)
1010

1111
require (
12+
github.com/a8m/envsubst v1.4.2 // indirect
13+
github.com/alecthomas/participle/v2 v2.1.1 // indirect
14+
github.com/dimchansky/utfbom v1.1.1 // indirect
15+
github.com/elliotchance/orderedmap v1.7.1 // indirect
16+
github.com/fatih/color v1.18.0 // indirect
17+
github.com/goccy/go-json v0.10.4 // indirect
18+
github.com/goccy/go-yaml v1.13.3 // indirect
1219
github.com/inconshreveable/mousetrap v1.1.0 // indirect
20+
github.com/jinzhu/copier v0.4.0 // indirect
21+
github.com/magiconair/properties v1.8.9 // indirect
22+
github.com/mattn/go-colorable v0.1.13 // indirect
23+
github.com/mattn/go-isatty v0.0.20 // indirect
24+
github.com/mikefarah/yq/v4 v4.45.1 // indirect
25+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
1326
github.com/spf13/pflag v1.0.6 // indirect
14-
golang.org/x/sys v0.10.0 // indirect
27+
github.com/yuin/gopher-lua v1.1.1 // indirect
28+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
29+
golang.org/x/mod v0.24.0 // indirect
30+
golang.org/x/net v0.33.0 // indirect
31+
golang.org/x/sys v0.28.0 // indirect
32+
golang.org/x/text v0.21.0 // indirect
33+
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect
34+
gopkg.in/yaml.v3 v3.0.1 // indirect
1535
)

src/extension/host-binary/go.sum

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,67 @@
11
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
22
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
3+
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
4+
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
5+
github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
6+
github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
37
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
8+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
10+
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
11+
github.com/elliotchance/orderedmap v1.7.1 h1:8SR2DB391dw0HVI9572ElrY+KU0Q89OCXYwWZx7aAZc=
12+
github.com/elliotchance/orderedmap v1.7.1/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
13+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
14+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
15+
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
16+
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
17+
github.com/goccy/go-yaml v1.13.3 h1:IXRULR8mAa0MXQobzzp0VOfMUJ8EnaQ4x3jhf7S0/nI=
18+
github.com/goccy/go-yaml v1.13.3/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
419
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
520
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
621
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
722
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
23+
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
24+
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
25+
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
26+
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
27+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
28+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
29+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
30+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
31+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
32+
github.com/mikefarah/yq/v4 v4.45.1 h1:EW+HjKEVa55pUYFJseEHEHdQ0+ulunY+q42zF3M7ZaQ=
33+
github.com/mikefarah/yq/v4 v4.45.1/go.mod h1:djgN2vD749hpjVNGYTShr5Kmv5LYljhCG3lUTuEe3LM=
34+
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
35+
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
36+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
837
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
938
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
1039
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
1140
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
1241
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
42+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
43+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
44+
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
45+
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
46+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
47+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
48+
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
49+
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
50+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
51+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
1352
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
1453
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
54+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
55+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1556
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
1657
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
58+
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
59+
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
60+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
61+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
1762
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
63+
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
64+
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
65+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
66+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1867
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package clients
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"sort"
11+
12+
"github.com/spf13/cobra"
13+
"github.com/spf13/pflag"
14+
"golang.org/x/exp/maps"
15+
"gopkg.in/yaml.v3"
16+
)
17+
18+
//go:embed config.yml
19+
var configYaml string
20+
21+
var (
22+
getProjectRoot = findGitProjectRoot
23+
errNotInGitRepo = errors.New("not in a git repo")
24+
)
25+
26+
func NewClientCmd(ctx context.Context, cwd string) *cobra.Command {
27+
cfg := readConfig()
28+
cmd := &cobra.Command{
29+
Use: "client",
30+
Short: "Connect/disconnect MCP clients.",
31+
}
32+
cmd.AddCommand(ListCommand(ctx, cwd, *cfg))
33+
cmd.AddCommand(ConnectCommand(ctx, cwd, *cfg))
34+
cmd.AddCommand(DisconnectCommand(ctx, cwd, *cfg))
35+
return cmd
36+
}
37+
38+
type Config struct {
39+
System map[string]globalCfg `yaml:"system"`
40+
Project map[string]localCfg `yaml:"project"`
41+
}
42+
43+
func readConfig() *Config {
44+
var result Config
45+
_ = yaml.Unmarshal([]byte(configYaml), &result)
46+
return &result
47+
}
48+
49+
func addGlobalFlag(flags *pflag.FlagSet, p *bool) {
50+
flags.BoolVarP(p, "global", "g", false, "Change the system wide configuration or the clients setup in your current git repo.")
51+
}
52+
53+
func addQuietFlag(flags *pflag.FlagSet, p *bool) {
54+
flags.BoolVarP(p, "quiet", "q", false, "Only display errors.")
55+
}
56+
57+
func findGitProjectRoot(dir string) string {
58+
for {
59+
gitPath := filepath.Join(dir, ".git")
60+
if _, err := os.Stat(gitPath); err == nil {
61+
return dir
62+
}
63+
parent := filepath.Dir(dir)
64+
if parent == dir {
65+
break
66+
}
67+
dir = parent
68+
}
69+
return ""
70+
}
71+
72+
func getSupportedMCPClients(cfg Config) []string {
73+
tmp := map[string]struct{}{
74+
vendorGordon: {},
75+
}
76+
for k := range cfg.System {
77+
tmp[k] = struct{}{}
78+
}
79+
for k := range cfg.Project {
80+
tmp[k] = struct{}{}
81+
}
82+
result := maps.Keys(tmp)
83+
sort.Strings(result)
84+
return result
85+
}
86+
87+
type ErrVendorNotFound struct {
88+
global bool
89+
vendor string
90+
config Config
91+
}
92+
93+
func (e *ErrVendorNotFound) Error() string {
94+
var alternative string
95+
if e.global {
96+
if _, ok := e.config.Project[e.vendor]; ok {
97+
alternative = " Did you mean to not use the --global flag?"
98+
}
99+
} else {
100+
if _, ok := e.config.System[e.vendor]; ok {
101+
alternative = " Did you mean to use the --global flag?"
102+
}
103+
}
104+
return "Vendor not found: " + e.vendor + "." + alternative
105+
}
106+
107+
type Updater func(key string, server *MCPServerSTDIO) error
108+
109+
func newMCPGatewayServer(client string) *MCPServerSTDIO {
110+
return &MCPServerSTDIO{
111+
Command: "docker",
112+
Args: []string{"run", "-l", fmt.Sprintf("mcp.client=%s", client), "--rm", "-i", "alpine/socat", "STDIO", "TCP:host.docker.internal:8811"},
113+
}
114+
}
115+
116+
func GetUpdater(vendor string, global bool, cwd string, config Config) (Updater, error) {
117+
if global {
118+
cfg, ok := config.System[vendor]
119+
if !ok {
120+
return nil, &ErrVendorNotFound{vendor: vendor, global: global, config: config}
121+
}
122+
processor, err := NewGlobalCfgProcessor(cfg)
123+
if err != nil {
124+
return nil, err
125+
}
126+
return processor.Update, nil
127+
}
128+
projectRoot := getProjectRoot(cwd)
129+
if projectRoot == "" {
130+
return nil, errNotInGitRepo
131+
}
132+
cfg, ok := config.Project[vendor]
133+
if !ok {
134+
return nil, &ErrVendorNotFound{vendor: vendor, global: global, config: config}
135+
}
136+
processor, err := NewLocalCfgProcessor(cfg, projectRoot)
137+
if err != nil {
138+
return nil, err
139+
}
140+
return processor.Update, nil
141+
}
142+
143+
type MCPClientCfgBase struct {
144+
DisplayName string `json:"displayName"`
145+
ConfigName string `json:"configName"`
146+
IsMCPCatalogConnected bool `json:"dockerMCPCatalogConnected"`
147+
Err *CfgError `json:"error"`
148+
149+
cfg *MCPJSONLists
150+
}
151+
152+
func (c *MCPClientCfgBase) setParseResult(lists *MCPJSONLists, err error) {
153+
c.Err = classifyError(err)
154+
if lists != nil {
155+
if containsMCPDocker(lists.STDIOServers) {
156+
c.IsMCPCatalogConnected = true
157+
}
158+
}
159+
c.cfg = lists
160+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
system:
2+
claude-desktop:
3+
displayName: Claude Desktop
4+
installCheckPaths:
5+
- /Applications/Claude.app
6+
- $AppData/Claude/
7+
paths:
8+
linux: $HOME/.config/claude/claude_desktop_config.json
9+
darwin: $HOME/Library/Application Support/Claude/claude_desktop_config.json
10+
windows: $APPDATA/Claude/claude_desktop_config.json
11+
yq:
12+
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
13+
set: .mcpServers[$NAME] = $JSON
14+
del: del(.mcpServers[$NAME])
15+
continue:
16+
displayName: Continue.dev
17+
installCheckPaths:
18+
- $HOME/.continue
19+
- $USERPROFILE/.continue
20+
paths:
21+
linux: $HOME/.continue/config.yaml
22+
darwin: $HOME/.continue/config.yaml
23+
windows: $USERPROFILE/.continue/config.yaml
24+
yq:
25+
list: .mcpServers
26+
set: .mcpServers = (.mcpServers // []) | .mcpServers += [{"name":$NAME}+$JSON]
27+
del: del(.mcpServers[] | select(.name == $NAME))
28+
cursor:
29+
displayName: Cursor
30+
installCheckPaths:
31+
- /Applications/Cursor.app
32+
- $AppData/Cursor/
33+
paths:
34+
linux: $HOME/.cursor/mcp.json
35+
darwin: $HOME/.cursor/mcp.json
36+
windows: $USERPROFILE/.cursor/mcp.json
37+
yq:
38+
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
39+
set: .mcpServers[$NAME] = $JSON
40+
del: del(.mcpServers[$NAME])
41+
project:
42+
cursor:
43+
displayname: Cursor
44+
projectfile: .cursor/mcp.json
45+
yq:
46+
list: '.mcpServers | to_entries | map(.value + {"name": .key})'
47+
set: .mcpServers[$NAME] = $JSON
48+
del: del(.mcpServers[$NAME])
49+
vscode:
50+
displayname: VSCode
51+
projectfile: .vscode/mcp.json
52+
yq:
53+
list: '.servers | to_entries | map(.value + {"name": .key})'
54+
set: .servers[$NAME] = $JSON+{"type":"stdio"}
55+
del: del(.servers[$NAME])

0 commit comments

Comments
 (0)