Skip to content

Commit 3287a73

Browse files
add flag to install pscale MCP to zed (#1147)
* add flag to install pscale MCP to zed as well * comment * remove bin * cleaner diffs * refactor
1 parent a724fbb commit 3287a73

File tree

3 files changed

+152
-90
lines changed

3 files changed

+152
-90
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ require (
3232
github.com/spf13/pflag v1.0.10
3333
github.com/spf13/viper v1.21.0
3434
github.com/stretchr/testify v1.11.1
35+
github.com/tidwall/jsonc v0.3.2
3536
github.com/xelabs/go-mysqlstack v1.0.0
3637
go.uber.org/zap v1.27.0
3738
go.uber.org/zap/exp v0.3.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
226226
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
227227
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
228228
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
229+
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
230+
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
229231
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
230232
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
231233
github.com/xelabs/go-mysqlstack v1.0.0 h1:go/UqwlxKRNh9df+AQ/pAAgcCCHCaeyv0PYZ/quRbbw=

internal/cmd/mcp/install.go

Lines changed: 149 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,9 @@ import (
99

1010
"github.com/planetscale/cli/internal/cmdutil"
1111
"github.com/spf13/cobra"
12+
"github.com/tidwall/jsonc"
1213
)
1314

14-
// ClaudeConfig represents the structure of the Claude Desktop config file
15-
type ClaudeConfig map[string]interface{}
16-
17-
// getClaudeConfigDir returns the path to the Claude Desktop config directory based on the OS
18-
func getClaudeConfigDir() (string, error) {
19-
switch runtime.GOOS {
20-
case "darwin":
21-
// macOS path: ~/Library/Application Support/Claude/
22-
homeDir, err := os.UserHomeDir()
23-
if err != nil {
24-
return "", fmt.Errorf("could not determine user home directory: %w", err)
25-
}
26-
return filepath.Join(homeDir, "Library", "Application Support", "Claude"), nil
27-
case "windows":
28-
// Windows path: %APPDATA%\Claude\
29-
return filepath.Join(os.Getenv("APPDATA"), "Claude"), nil
30-
default:
31-
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
32-
}
33-
}
34-
35-
// getCursorConfigPath returns the path to the Cursor MCP config file
36-
func getCursorConfigPath() (string, error) {
37-
homeDir, err := os.UserHomeDir()
38-
if err != nil {
39-
return "", fmt.Errorf("could not determine user home directory: %w", err)
40-
}
41-
42-
// Cursor uses ~/.cursor/mcp.json for its MCP configuration
43-
return filepath.Join(homeDir, ".cursor", "mcp.json"), nil
44-
}
45-
4615
// InstallCmd returns a new cobra.Command for the mcp install command.
4716
func InstallCmd(ch *cmdutil.Helper) *cobra.Command {
4817
var target string
@@ -57,86 +26,176 @@ func InstallCmd(ch *cmdutil.Helper) *cobra.Command {
5726

5827
switch target {
5928
case "claude":
60-
configDir, err := getClaudeConfigDir()
29+
configPath, err = getClaudeConfigPath()
6130
if err != nil {
62-
return fmt.Errorf("failed to determine Claude config directory: %w", err)
31+
return fmt.Errorf("failed to determine Claude config path: %w", err)
6332
}
64-
65-
// Check if the directory exists
66-
if _, err := os.Stat(configDir); os.IsNotExist(err) {
67-
return fmt.Errorf("no Claude Desktop installation: path %s not found", configDir)
33+
if err := installMCPServer(configPath, target, modifyClaudeConfig); err != nil {
34+
return err
6835
}
69-
70-
configPath = filepath.Join(configDir, "claude_desktop_config.json")
7136
case "cursor":
7237
configPath, err = getCursorConfigPath()
7338
if err != nil {
7439
return fmt.Errorf("failed to determine Cursor config path: %w", err)
7540
}
76-
77-
// Ensure the .cursor directory exists
78-
configDir := filepath.Dir(configPath)
79-
if _, err := os.Stat(configDir); os.IsNotExist(err) {
80-
if err := os.MkdirAll(configDir, 0755); err != nil {
81-
return fmt.Errorf("failed to create Cursor config directory: %w", err)
82-
}
41+
// Cursor uses the same config structure as Claude
42+
if err := installMCPServer(configPath, target, modifyClaudeConfig); err != nil {
43+
return err
8344
}
84-
default:
85-
return fmt.Errorf("invalid target vendor: %s (supported values: claude, cursor)", target)
86-
}
87-
88-
config := make(ClaudeConfig) // Same config structure for both tools
89-
90-
// Check if the file exists
91-
if _, err := os.Stat(configPath); err == nil {
92-
// File exists, read it
93-
configData, err := os.ReadFile(configPath)
45+
case "zed":
46+
configPath, err = getZedConfigPath()
9447
if err != nil {
95-
return fmt.Errorf("failed to read %s config file: %w", target, err)
48+
return fmt.Errorf("failed to determine Zed config path: %w", err)
9649
}
97-
98-
if err := json.Unmarshal(configData, &config); err != nil {
99-
return fmt.Errorf("failed to parse %s config file: %w", target, err)
50+
if err := installMCPServer(configPath, target, modifyZedConfig); err != nil {
51+
return err
10052
}
101-
}
102-
103-
// Get or initialize the mcpServers map
104-
var mcpServers map[string]interface{}
105-
if existingServers, ok := config["mcpServers"].(map[string]interface{}); ok {
106-
mcpServers = existingServers
107-
} else {
108-
mcpServers = make(map[string]interface{})
109-
}
110-
111-
// Add or update the planetscale server configuration
112-
mcpServers["planetscale"] = map[string]interface{}{
113-
"command": "pscale",
114-
"args": []string{"mcp", "server"},
115-
}
116-
117-
// Update the config with the new mcpServers
118-
config["mcpServers"] = mcpServers
119-
120-
// Write the updated config back to file
121-
configJSON, err := json.MarshalIndent(config, "", " ")
122-
if err != nil {
123-
return fmt.Errorf("failed to marshal %s config: %w", target, err)
124-
}
125-
126-
if err := os.WriteFile(configPath, configJSON, 0644); err != nil {
127-
return fmt.Errorf("failed to write %s config file: %w", target, err)
53+
default:
54+
return fmt.Errorf("invalid target vendor: %s (supported values: claude, cursor, zed)", target)
12855
}
12956

13057
fmt.Printf("MCP server successfully configured for %s at %s\n", target, configPath)
13158
return nil
13259
},
13360
}
13461

135-
cmd.Flags().StringVar(&target, "target", "", "Target vendor for MCP installation (required). Possible values: [claude, cursor]")
62+
cmd.Flags().StringVar(&target, "target", "", "Target vendor for MCP installation (required). Possible values: [claude, cursor, zed]")
13663
cmd.MarkFlagRequired("target")
13764
cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
138-
return []string{"claude", "cursor"}, cobra.ShellCompDirectiveDefault
65+
return []string{"claude", "cursor", "zed"}, cobra.ShellCompDirectiveDefault
13966
})
14067

14168
return cmd
14269
}
70+
71+
// installMCPServer handles common file I/O for all editors
72+
func installMCPServer(configPath string, target string, modifyConfig func(map[string]any) error) error {
73+
// Check if config directory exists
74+
configDir := filepath.Dir(configPath)
75+
if _, err := os.Stat(configDir); os.IsNotExist(err) {
76+
return fmt.Errorf("no %s installation: path %s not found", target, configDir)
77+
}
78+
79+
// Read existing config or create empty settings
80+
var fullSettings map[string]any
81+
if fileData, err := os.ReadFile(configPath); err == nil {
82+
cleanJSON := jsonc.ToJSON(fileData)
83+
if err := json.Unmarshal(cleanJSON, &fullSettings); err != nil {
84+
return fmt.Errorf("failed to parse %s config file: %w", target, err)
85+
}
86+
} else if !os.IsNotExist(err) {
87+
return fmt.Errorf("failed to read %s config file: %w", target, err)
88+
} else {
89+
fullSettings = make(map[string]any)
90+
}
91+
92+
// Let the editor-specific function modify the config
93+
if err := modifyConfig(fullSettings); err != nil {
94+
return err
95+
}
96+
97+
// Marshal updated config
98+
updatedData, err := json.MarshalIndent(fullSettings, "", " ")
99+
if err != nil {
100+
return fmt.Errorf("failed to marshal %s config: %w", target, err)
101+
}
102+
103+
// Backup existing file before writing
104+
if _, err := os.Stat(configPath); err == nil {
105+
backupPath := configPath + "~"
106+
if err := os.Rename(configPath, backupPath); err != nil {
107+
return fmt.Errorf("failed to create backup: %w", err)
108+
}
109+
fmt.Printf("Created backup at %s\n", backupPath)
110+
}
111+
112+
// Write updated config
113+
if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
114+
return fmt.Errorf("failed to write %s config file: %w", target, err)
115+
}
116+
117+
return nil
118+
}
119+
120+
// modifyClaudeConfig adds planetscale to mcpServers (Claude and Cursor both use this)
121+
func modifyClaudeConfig(settings map[string]any) error {
122+
var mcpServers map[string]any
123+
if existingServers, ok := settings["mcpServers"].(map[string]any); ok {
124+
mcpServers = existingServers
125+
} else {
126+
mcpServers = make(map[string]any)
127+
}
128+
129+
mcpServers["planetscale"] = map[string]any{
130+
"command": "pscale",
131+
"args": []string{"mcp", "server"},
132+
}
133+
134+
settings["mcpServers"] = mcpServers
135+
return nil
136+
}
137+
138+
// modifyZedConfig adds planetscale to context_servers (Zed-specific)
139+
func modifyZedConfig(settings map[string]any) error {
140+
var contextServers map[string]any
141+
if existingServers, ok := settings["context_servers"].(map[string]any); ok {
142+
contextServers = existingServers
143+
} else {
144+
contextServers = make(map[string]any)
145+
}
146+
147+
contextServers["planetscale"] = map[string]any{
148+
"source": "custom",
149+
"command": "pscale",
150+
"args": []string{"mcp", "server"},
151+
}
152+
153+
settings["context_servers"] = contextServers
154+
return nil
155+
}
156+
157+
// getClaudeConfigPath returns the path to the Claude Desktop config file based on the OS
158+
func getClaudeConfigPath() (string, error) {
159+
switch runtime.GOOS {
160+
case "darwin":
161+
// macOS path: ~/Library/Application Support/Claude/claude_desktop_config.json
162+
homeDir, err := os.UserHomeDir()
163+
if err != nil {
164+
return "", fmt.Errorf("could not determine user home directory: %w", err)
165+
}
166+
return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"), nil
167+
case "windows":
168+
// Windows path: %APPDATA%\Claude\claude_desktop_config.json
169+
return filepath.Join(os.Getenv("APPDATA"), "Claude", "claude_desktop_config.json"), nil
170+
default:
171+
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
172+
}
173+
}
174+
175+
// getCursorConfigPath returns the path to the Cursor MCP config file
176+
func getCursorConfigPath() (string, error) {
177+
homeDir, err := os.UserHomeDir()
178+
if err != nil {
179+
return "", fmt.Errorf("could not determine user home directory: %w", err)
180+
}
181+
182+
// Cursor uses ~/.cursor/mcp.json for its MCP configuration
183+
return filepath.Join(homeDir, ".cursor", "mcp.json"), nil
184+
}
185+
186+
// getZedConfigPath returns the path to the Zed config file based on the OS
187+
func getZedConfigPath() (string, error) {
188+
homeDir, err := os.UserHomeDir()
189+
if err != nil {
190+
return "", fmt.Errorf("could not determine user home directory: %w", err)
191+
}
192+
193+
switch runtime.GOOS {
194+
case "darwin", "linux":
195+
return filepath.Join(homeDir, ".config", "zed", "settings.json"), nil
196+
case "windows":
197+
return filepath.Join(os.Getenv("APPDATA"), "Zed", "settings.json"), nil
198+
default:
199+
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
200+
}
201+
}

0 commit comments

Comments
 (0)