@@ -29,11 +29,9 @@ type MCPConfig struct {
29
29
MCPServers map [string ]MCPServerConfig `json:"mcpServers"`
30
30
}
31
31
32
- // VSCodeConfig represents the VSCode settings .json structure
32
+ // VSCodeConfig represents the VSCode mcp .json structure
33
33
type VSCodeConfig struct {
34
- MCP struct {
35
- Servers map [string ]VSCodeMCPServerConfig `json:"servers"`
36
- } `json:"mcp"`
34
+ Servers map [string ]VSCodeMCPServerConfig `json:"servers"`
37
35
// Other VSCode settings can be preserved with this field
38
36
Other map [string ]interface {} `json:"-"`
39
37
}
@@ -49,95 +47,135 @@ type VSCodeMCPServerConfig struct {
49
47
Headers map [string ]string `json:"headers,omitempty"` // For sse
50
48
}
51
49
50
+ // MCPClient represents the supported MCP clients as an enum
51
+ type MCPClient string
52
+
53
+ const (
54
+ MCPClientVSCode MCPClient = "vscode"
55
+ MCPClientCode MCPClient = "code"
56
+ MCPClientVSCodeInsiders MCPClient = "vscode-insiders"
57
+ MCPClientInsiders MCPClient = "insiders"
58
+ MCPClientClaude MCPClient = "claude"
59
+ MCPClientWindsurf MCPClient = "windsurf"
60
+ MCPClientCascade MCPClient = "cascade"
61
+ MCPClientCodeium MCPClient = "codeium"
62
+ MCPClientCursor MCPClient = "cursor"
63
+ )
64
+
52
65
// ValidVSCodeClients is a list of supported VSCode MCP clients with shorthand names
53
- var ValidVSCodeClients = []string {
54
- "vscode" ,
55
- "code" ,
56
- "vscode-insiders" ,
57
- "insiders" ,
66
+ var ValidVSCodeClients = []MCPClient {
67
+ MCPClientVSCode ,
68
+ MCPClientCode ,
69
+ MCPClientVSCodeInsiders ,
70
+ MCPClientInsiders ,
58
71
}
59
72
60
73
// ValidClients is a list of supported MCP clients
61
74
var ValidClients = append (
62
- []string {
63
- "claude" ,
64
- "windsurf" ,
65
- "cursor" ,
75
+ []MCPClient {
76
+ MCPClientClaude ,
77
+ MCPClientWindsurf ,
78
+ MCPClientCascade ,
79
+ MCPClientCodeium ,
80
+ MCPClientCursor ,
66
81
},
67
82
ValidVSCodeClients ... ,
68
83
)
69
84
70
- // isValidClient checks if the provided client is in the list of valid clients
71
- func isValidClient (client string ) bool {
72
- return slices .Contains (ValidClients , client )
85
+ func ParseMCPClient (clientStr string ) (MCPClient , error ) {
86
+ clientStr = strings .ToLower (clientStr )
87
+ client := MCPClient (clientStr )
88
+ if ! slices .Contains (ValidClients , client ) {
89
+ return "" , fmt .Errorf ("invalid MCP client: %q. Valid MCP clients are: %v" , client , ValidClients )
90
+ }
91
+ return client , nil
73
92
}
74
93
75
- // getClientConfigPath returns the path to the config file for the given client
76
- func getClientConfigPath (client string ) (string , error ) {
77
- homeDir , err := os .UserHomeDir ()
78
- if err != nil {
79
- return "" , fmt .Errorf ("failed to get home directory: %w" , err )
80
- }
94
+ // ClientInfo defines where each client stores its MCP configuration
95
+ type ClientInfo struct {
96
+ configFile string // Configuration file name
97
+ useHomeDir bool // True if config goes directly in home dir, false if in system config dir
98
+ }
81
99
82
- var configPath string
83
- switch strings .ToLower (client ) {
84
- case "windsurf" , "cascade" , "codeium" :
85
- configPath = filepath .Join (homeDir , ".codeium" , "windsurf" , "mcp_config.json" )
86
- case "claude" :
87
- if runtime .GOOS == "darwin" {
88
- configPath = filepath .Join (homeDir , "Library" , "Application Support" , "Claude" , "claude_desktop_config.json" )
89
- } else if runtime .GOOS == "windows" {
90
- appData := os .Getenv ("APPDATA" )
91
- if appData == "" {
92
- appData = filepath .Join (homeDir , "AppData" , "Roaming" )
93
- }
94
- configPath = filepath .Join (appData , "Claude" , "claude_desktop_config.json" )
95
- } else {
96
- configHome := os .Getenv ("XDG_CONFIG_HOME" )
97
- if configHome == "" {
98
- configHome = filepath .Join (homeDir , ".config" )
99
- }
100
- configPath = filepath .Join (configHome , "Claude" , "claude_desktop_config.json" )
101
- }
102
- case "cursor" :
103
- configPath = filepath .Join (homeDir , ".cursor" , "mcp.json" )
104
- case "vscode" , "code" :
105
- if runtime .GOOS == "darwin" {
106
- configPath = filepath .Join (homeDir , "Library" , "Application Support" , "Code" , "User" , "settings.json" )
107
- } else if runtime .GOOS == "windows" {
108
- appData := os .Getenv ("APPDATA" )
109
- if appData == "" {
110
- appData = filepath .Join (homeDir , "AppData" , "Roaming" )
111
- }
112
- configPath = filepath .Join (appData , "Code" , "User" , "settings.json" )
113
- } else {
114
- configHome := os .Getenv ("XDG_CONFIG_HOME" )
115
- if configHome == "" {
116
- configHome = filepath .Join (homeDir , ".config" )
117
- }
118
- configPath = filepath .Join (configHome , "Code/User/settings.json" )
100
+ var windsurfConfig = ClientInfo {
101
+ configFile : ".codeium/windsurf/mcp_config.json" ,
102
+ useHomeDir : true ,
103
+ }
104
+
105
+ var vscodeConfig = ClientInfo {
106
+ configFile : "Code/User/mcp.json" ,
107
+ useHomeDir : false ,
108
+ }
109
+
110
+ var codeInsidersConfig = ClientInfo {
111
+ configFile : "Code - Insiders/User/mcp.json" ,
112
+ useHomeDir : false ,
113
+ }
114
+
115
+ var claudeConfig = ClientInfo {
116
+ configFile : "Claude/claude_desktop_config.json" ,
117
+ useHomeDir : false ,
118
+ }
119
+
120
+ var cursorConfig = ClientInfo {
121
+ configFile : ".cursor/settings.json" ,
122
+ useHomeDir : true ,
123
+ }
124
+
125
+ // clientRegistry maps client names to their configuration details
126
+ var clientRegistry = map [MCPClient ]ClientInfo {
127
+ MCPClientWindsurf : windsurfConfig ,
128
+ MCPClientCascade : windsurfConfig ,
129
+ MCPClientCodeium : windsurfConfig ,
130
+ MCPClientVSCode : vscodeConfig ,
131
+ MCPClientCode : vscodeConfig ,
132
+ MCPClientVSCodeInsiders : codeInsidersConfig ,
133
+ MCPClientInsiders : codeInsidersConfig ,
134
+ MCPClientClaude : claudeConfig ,
135
+ MCPClientCursor : cursorConfig ,
136
+ }
137
+
138
+ // getSystemConfigDir returns the system configuration directory for the given OS
139
+ func getSystemConfigDir (homeDir , goos string ) string {
140
+ switch goos {
141
+ case "darwin" :
142
+ return filepath .Join (homeDir , "Library" , "Application Support" )
143
+ case "windows" :
144
+ if appData := os .Getenv ("APPDATA" ); appData != "" {
145
+ return appData
119
146
}
120
- case "vscode-insiders" , "insiders" :
121
- if runtime .GOOS == "darwin" {
122
- configPath = filepath .Join (homeDir , "Library" , "Application Support" , "Code - Insiders" , "User" , "settings.json" )
123
- } else if runtime .GOOS == "windows" {
124
- appData := os .Getenv ("APPDATA" )
125
- if appData == "" {
126
- appData = filepath .Join (homeDir , "AppData" , "Roaming" )
127
- }
128
- configPath = filepath .Join (appData , "Code - Insiders" , "User" , "settings.json" )
129
- } else {
130
- configHome := os .Getenv ("XDG_CONFIG_HOME" )
131
- if configHome == "" {
132
- configHome = filepath .Join (homeDir , ".config" )
133
- }
134
- configPath = filepath .Join (configHome , "Code - Insiders/User/settings.json" )
147
+ return filepath .Join (homeDir , "AppData" , "Roaming" )
148
+ case "linux" :
149
+ if configHome := os .Getenv ("XDG_CONFIG_HOME" ); configHome != "" {
150
+ return configHome
135
151
}
152
+ return filepath .Join (homeDir , ".config" )
136
153
default :
154
+ // Default to Linux behavior
155
+ if configHome := os .Getenv ("XDG_CONFIG_HOME" ); configHome != "" {
156
+ return configHome
157
+ }
158
+ return filepath .Join (homeDir , ".config" )
159
+ }
160
+ }
161
+
162
+ // getClientConfigPath returns the path to the config file for the given client
163
+ func getClientConfigPath (homeDir , goos string , client MCPClient ) (string , error ) {
164
+ clientInfo , exists := clientRegistry [client ]
165
+ if ! exists {
137
166
return "" , fmt .Errorf ("unsupported client: %s" , client )
138
167
}
139
168
140
- return configPath , nil
169
+ var basePath string
170
+ if clientInfo .useHomeDir {
171
+ // Config goes directly in home directory
172
+ basePath = homeDir
173
+ } else {
174
+ // Config goes in system-specific config directory
175
+ basePath = getSystemConfigDir (homeDir , goos )
176
+ }
177
+
178
+ return filepath .Join (basePath , clientInfo .configFile ), nil
141
179
}
142
180
143
181
// getDefangMCPConfig returns the default MCP config for Defang
@@ -179,7 +217,7 @@ func getVSCodeServerConfig() (map[string]interface{}, error) {
179
217
}, nil
180
218
}
181
219
182
- // handleVSCodeConfig handles the special case for VSCode settings .json
220
+ // handleVSCodeConfig handles the special case for VSCode mcp .json
183
221
func handleVSCodeConfig (configPath string ) error {
184
222
// Create or update the config file
185
223
var existingData map [string ]interface {}
@@ -254,23 +292,80 @@ func handleVSCodeConfig(configPath string) error {
254
292
return fmt .Errorf ("failed to marshal config: %w" , err )
255
293
}
256
294
295
+ // #nosec G306 - config file does not contain sensitive data
296
+ if err := os .WriteFile (configPath , data , 0644 ); err != nil {
297
+ return fmt .Errorf ("failed to write config file: %w" , err )
298
+ }
299
+
300
+ return nil
301
+ }
302
+
303
+ func handleStandardConfig (configPath string ) error {
304
+ // For all other clients, use the standard format
305
+ var config MCPConfig
306
+
307
+ // Check if the file exists
308
+ if _ , err := os .Stat (configPath ); err == nil {
309
+ // File exists, read it
310
+ data , err := os .ReadFile (configPath )
311
+ if err != nil {
312
+ return fmt .Errorf ("failed to read config file: %w" , err )
313
+ }
314
+
315
+ // Parse the JSON
316
+ if err := json .Unmarshal (data , & config ); err != nil {
317
+ // If we can't parse it, start fresh
318
+ config = MCPConfig {
319
+ MCPServers : make (map [string ]MCPServerConfig ),
320
+ }
321
+ }
322
+ } else {
323
+ // File doesn't exist, create a new config
324
+ config = MCPConfig {
325
+ MCPServers : make (map [string ]MCPServerConfig ),
326
+ }
327
+ }
328
+
329
+ if config .MCPServers == nil {
330
+ config .MCPServers = make (map [string ]MCPServerConfig )
331
+ }
332
+
333
+ defangConfig , err := getDefangMCPConfig ()
334
+ if err != nil {
335
+ return fmt .Errorf ("failed to get Defang MCP config: %w" , err )
336
+ }
337
+ // Add or update the Defang MCP server config
338
+ config .MCPServers ["defang" ] = * defangConfig
339
+
340
+ // Write the config to the file
341
+ data , err := json .MarshalIndent (config , "" , " " )
342
+ if err != nil {
343
+ return fmt .Errorf ("failed to marshal config: %w" , err )
344
+ }
345
+
346
+ // #nosec G306 - config file does not contain sensitive data
257
347
if err := os .WriteFile (configPath , data , 0644 ); err != nil {
258
348
return fmt .Errorf ("failed to write config file: %w" , err )
259
349
}
260
350
261
351
return nil
262
352
}
263
353
264
- func SetupClient (client string ) error {
265
- // Validate client
266
- if ! isValidClient (client ) {
267
- return fmt .Errorf ("invalid MCP client: %q. Valid MCP clients are: %v" , client , strings .Join (ValidClients , ", " ))
354
+ func SetupClient (clientValue string ) error {
355
+ client , err := ParseMCPClient (clientValue )
356
+ if err != nil {
357
+ // cast the client string to MCPClient
358
+ return fmt .Errorf ("invalid MCP client: %q. Valid MCP clients are: %v" , client , ValidClients )
268
359
}
269
360
270
361
track .Evt ("MCP Setup Client: " , track .P ("client" , client ))
271
362
363
+ homeDir , err := os .UserHomeDir ()
364
+ if err != nil {
365
+ return fmt .Errorf ("failed to get home directory: %w" , err )
366
+ }
272
367
// Get the config path for the client
273
- configPath , err := getClientConfigPath (client )
368
+ configPath , err := getClientConfigPath (homeDir , runtime . GOOS , client )
274
369
if err != nil {
275
370
return err
276
371
}
@@ -283,56 +378,14 @@ func SetupClient(client string) error {
283
378
return fmt .Errorf ("failed to create config directory: %w" , err )
284
379
}
285
380
286
- // Handle VSCode settings .json specially
381
+ // Handle VSCode mcp .json specially
287
382
if slices .Contains (ValidVSCodeClients , client ) {
288
383
if err := handleVSCodeConfig (configPath ); err != nil {
289
384
return err
290
385
}
291
386
} else {
292
- // For all other clients, use the standard format
293
- var config MCPConfig
294
-
295
- // Check if the file exists
296
- if _ , err := os .Stat (configPath ); err == nil {
297
- // File exists, read it
298
- data , err := os .ReadFile (configPath )
299
- if err != nil {
300
- return fmt .Errorf ("failed to read config file: %w" , err )
301
- }
302
-
303
- // Parse the JSON
304
- if err := json .Unmarshal (data , & config ); err != nil {
305
- // If we can't parse it, start fresh
306
- config = MCPConfig {
307
- MCPServers : make (map [string ]MCPServerConfig ),
308
- }
309
- }
310
- } else {
311
- // File doesn't exist, create a new config
312
- config = MCPConfig {
313
- MCPServers : make (map [string ]MCPServerConfig ),
314
- }
315
- }
316
-
317
- if config .MCPServers == nil {
318
- config .MCPServers = make (map [string ]MCPServerConfig )
319
- }
320
-
321
- defangConfig , err := getDefangMCPConfig ()
322
- if err != nil {
323
- return fmt .Errorf ("failed to get Defang MCP config: %w" , err )
324
- }
325
- // Add or update the Defang MCP server config
326
- config .MCPServers ["defang" ] = * defangConfig
327
-
328
- // Write the config to the file
329
- data , err := json .MarshalIndent (config , "" , " " )
330
- if err != nil {
331
- return fmt .Errorf ("failed to marshal config: %w" , err )
332
- }
333
-
334
- if err := os .WriteFile (configPath , data , 0644 ); err != nil {
335
- return fmt .Errorf ("failed to write config file: %w" , err )
387
+ if err := handleStandardConfig (configPath ); err != nil {
388
+ return err
336
389
}
337
390
}
338
391
0 commit comments