@@ -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.
4716func 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