From abbc159543730a71197f9b1b23a8c5c7bfd03f26 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 12:46:51 +0000 Subject: [PATCH 01/17] feat: add supabase mcp init command skeleton - Add main mcp command group in cmd/mcp.go - Add mcp init subcommand in cmd/mcp_init.go - Create internal/mcp/init package for implementation - Provides basic structure for interactive MCP server setup wizard This implements the foundation for AI-81: CLI init command that will: - Guide users through obtaining a Supabase PAT - Detect installed MCP clients (Cursor, VS Code, Claude Desktop) - Generate appropriate configuration files - Securely store credentials Related: https://linear.app/supabase/issue/AI-81/cli-init --- cmd/mcp.go | 18 ++++++++++++++++++ cmd/mcp_init.go | 36 ++++++++++++++++++++++++++++++++++++ internal/mcp/init/init.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 cmd/mcp.go create mode 100644 cmd/mcp_init.go create mode 100644 internal/mcp/init/init.go diff --git a/cmd/mcp.go b/cmd/mcp.go new file mode 100644 index 000000000..9f9057d63 --- /dev/null +++ b/cmd/mcp.go @@ -0,0 +1,18 @@ +package cmd + +import ( +"github.com/spf13/cobra" +) + +var ( +mcpCmd = &cobra.Command{ + GroupID: groupManagementAPI, + Use: "mcp", + Short: "Manage Model Context Protocol (MCP) configuration", + Long: "Commands for setting up and managing MCP server configurations for AI assistants like Cursor, VS Code Copilot, and Claude Desktop.", + } +) + +func init() { + rootCmd.AddCommand(mcpCmd) +} diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go new file mode 100644 index 000000000..12f4c52a9 --- /dev/null +++ b/cmd/mcp_init.go @@ -0,0 +1,36 @@ +package cmd + +import ( +"github.com/spf13/afero" +"github.com/spf13/cobra" +"github.com/supabase/cli/internal/mcp/init" +) + +var ( +mcpInitCmd = &cobra.Command{ + Use: "init", + Short: "Initialize MCP server configuration for AI assistants", + Long: `Interactive setup wizard to configure the Supabase MCP server for your AI assistant clients. + +This command will: + • Guide you through obtaining a Supabase Personal Access Token + • Securely store your credentials + • Detect installed MCP clients (Cursor, VS Code, Claude Desktop, etc.) + • Generate appropriate configuration files for each client + • Configure server options (read-only mode, project scoping, feature groups) + +Examples: + # Run interactive setup + supabase mcp init + + # Skip credential storage and only generate configs + supabase mcp init --no-save-credentials`, + RunE: func(cmd *cobra.Command, args []string) error { + return mcpinit.Run(cmd.Context(), afero.NewOsFs()) + }, + } +) + +func init() { + mcpCmd.AddCommand(mcpInitCmd) +} diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go new file mode 100644 index 000000000..043a523fc --- /dev/null +++ b/internal/mcp/init/init.go @@ -0,0 +1,30 @@ +package mcpinit + +import ( +"context" +"fmt" + +"github.com/spf13/afero" +) + +func Run(ctx context.Context, fsys afero.Fs) error { + fmt.Println("🚀 Supabase MCP Server Setup") + fmt.Println("───────────────────────────────") + fmt.Println() + fmt.Println("Welcome to the Supabase MCP server configuration wizard!") + fmt.Println("This will help you set up the MCP server for your AI assistants.") + fmt.Println() + + // TODO: Implement the interactive setup flow + // 1. Prompt for PAT + // 2. Detect installed clients + // 3. Configure server options + // 4. Generate config files + // 5. Store credentials securely + + fmt.Println("⚠️ This feature is under development.") + fmt.Println("For now, please follow the manual setup instructions at:") + fmt.Println("https://supabase.com/docs/guides/getting-started/mcp") + + return nil +} From 5dceaf7b0213ced70fb52ddc10a2b4917de8c42a Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 13:16:05 +0000 Subject: [PATCH 02/17] fix: move mcp command to Quick Start group Change mcpCmd.GroupID from groupManagementAPI to groupQuickStart to avoid requiring authentication before running mcp init. The init command should be accessible without prior login as it guides users through setup. --- cmd/mcp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mcp.go b/cmd/mcp.go index 9f9057d63..aa5ad7a88 100644 --- a/cmd/mcp.go +++ b/cmd/mcp.go @@ -6,7 +6,7 @@ import ( var ( mcpCmd = &cobra.Command{ - GroupID: groupManagementAPI, + GroupID: groupQuickStart, Use: "mcp", Short: "Manage Model Context Protocol (MCP) configuration", Long: "Commands for setting up and managing MCP server configurations for AI assistants like Cursor, VS Code Copilot, and Claude Desktop.", From 47209359c4cf75b840a5c0565cd98408205ac345 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 14:54:51 +0000 Subject: [PATCH 03/17] feat: add --client flag with MCP client detection and config templates - Add --client flag to specify target MCP client - Implement client detection for Cursor, VS Code, Claude Desktop, Claude Code, Windsurf, and Cline - Generate platform-specific config templates based on official Supabase docs - Display setup instructions for each client type - Support macOS, Windows, and Linux config paths --- cmd/mcp_init.go | 10 +- internal/mcp/init/init.go | 293 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 289 insertions(+), 14 deletions(-) diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go index 12f4c52a9..cd3b3dc93 100644 --- a/cmd/mcp_init.go +++ b/cmd/mcp_init.go @@ -23,14 +23,22 @@ Examples: # Run interactive setup supabase mcp init + # Configure a specific client + supabase mcp init --client cursor + supabase mcp init --client vscode + supabase mcp init --client claude-desktop + supabase mcp init --client claude-code + # Skip credential storage and only generate configs supabase mcp init --no-save-credentials`, RunE: func(cmd *cobra.Command, args []string) error { - return mcpinit.Run(cmd.Context(), afero.NewOsFs()) + client, _ := cmd.Flags().GetString("client") + return mcpinit.Run(cmd.Context(), afero.NewOsFs(), client) }, } ) func init() { + mcpInitCmd.Flags().StringP("client", "c", "", "Target specific client (cursor, vscode, claude-desktop, claude-code, windsurf, cline)") mcpCmd.AddCommand(mcpInitCmd) } diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 043a523fc..728ded80b 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -2,29 +2,296 @@ package mcpinit import ( "context" +"encoding/json" "fmt" +"os" +"path/filepath" +"runtime" +"strings" "github.com/spf13/afero" ) -func Run(ctx context.Context, fsys afero.Fs) error { +type ClientConfig struct { + Name string + ConfigPath string + ConfigType string // "cursor", "vscode", "claude-desktop", "claude-code", "windsurf", "cline" + Detected bool +} + +// Run executes the MCP initialization wizard +func Run(ctx context.Context, fsys afero.Fs, targetClient string) error { fmt.Println("🚀 Supabase MCP Server Setup") fmt.Println("───────────────────────────────") fmt.Println() - fmt.Println("Welcome to the Supabase MCP server configuration wizard!") - fmt.Println("This will help you set up the MCP server for your AI assistants.") + + // Detect or validate client + clients := detectClients() + + if targetClient != "" { + // Validate the specified client + found := false + for _, c := range clients { + if strings.EqualFold(c.ConfigType, targetClient) { + found = true + fmt.Printf("📍 Configuring for: %s\n", c.Name) + fmt.Printf(" Config path: %s\n\n", c.ConfigPath) + displayConfigInstructions(c.ConfigType) + break + } + } + if !found { + return fmt.Errorf("unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline", targetClient) + } + } else { + // Display detected clients + fmt.Println("🔍 Detected MCP-compatible clients:") + fmt.Println() + + detectedCount := 0 + for _, c := range clients { + if c.Detected { + fmt.Printf(" ✅ %s\n", c.Name) + fmt.Printf(" Config: %s\n", c.ConfigPath) + detectedCount++ + } + } + + if detectedCount == 0 { + fmt.Println(" ⚠️ No MCP clients detected on this system") + fmt.Println() + fmt.Println("Supported clients:") + for _, c := range clients { + fmt.Printf(" • %s (%s)\n", c.Name, c.ConfigPath) + } + } + + fmt.Println() + fmt.Println("💡 To configure a specific client, use:") + fmt.Println(" supabase mcp init --client ") + fmt.Println() + fmt.Println(" Supported clients:") + fmt.Println(" • cursor") + fmt.Println(" • vscode") + fmt.Println(" • claude-desktop") + fmt.Println(" • claude-code") + fmt.Println(" • windsurf") + fmt.Println(" • cline") + } + + return nil +} + +// detectClients checks for installed MCP clients +func detectClients() []ClientConfig { + homeDir, _ := os.UserHomeDir() + + clients := []ClientConfig{ + { + Name: "Cursor", + ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), + ConfigType: "cursor", + }, + { + Name: "VS Code (Copilot)", + ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), + ConfigType: "vscode", + }, + { + Name: "Claude Desktop", + ConfigPath: getClaudeDesktopConfigPath(homeDir), + ConfigType: "claude-desktop", + }, + { + Name: "Claude Code", + ConfigPath: filepath.Join(homeDir, ".mcp.json"), + ConfigType: "claude-code", + }, + { + Name: "Windsurf", + ConfigPath: getWindsurfConfigPath(homeDir), + ConfigType: "windsurf", + }, + { + Name: "Cline (VS Code)", + ConfigPath: getClineConfigPath(homeDir), + ConfigType: "cline", + }, + } + + // Check which clients are actually installed + for i := range clients { + if _, err := os.Stat(filepath.Dir(clients[i].ConfigPath)); err == nil { + clients[i].Detected = true + } + } + + return clients +} + +func getClaudeDesktopConfigPath(homeDir string) string { + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json") + default: // linux + return filepath.Join(homeDir, ".config", "Claude", "claude_desktop_config.json") + } +} + +func getWindsurfConfigPath(homeDir string) string { + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") + default: // linux + return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") + } +} + +func getClineConfigPath(homeDir string) string { + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") + default: // linux + return filepath.Join(homeDir, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") + } +} + +// displayConfigInstructions shows the configuration template for a specific client +func displayConfigInstructions(clientType string) { + fmt.Println("📝 Configuration Template:") fmt.Println() - // TODO: Implement the interactive setup flow - // 1. Prompt for PAT - // 2. Detect installed clients - // 3. Configure server options - // 4. Generate config files - // 5. Store credentials securely + var config interface{} + var configStr string - fmt.Println("⚠️ This feature is under development.") - fmt.Println("For now, please follow the manual setup instructions at:") - fmt.Println("https://supabase.com/docs/guides/getting-started/mcp") + switch clientType { + case "cursor", "windsurf", "cline": + config = getCursorStyleConfig() + case "vscode": + config = getVSCodeConfig() + case "claude-desktop": + config = getCursorStyleConfig() // Same format as Cursor + case "claude-code": + config = getClaudeCodeConfig() + } - return nil + configJSON, _ := json.MarshalIndent(config, "", " ") + configStr = string(configJSON) + + // Add platform-specific commands + fmt.Println(configStr) + fmt.Println() + + fmt.Println("📋 Setup Instructions:") + fmt.Println() + + switch clientType { + case "cursor": + fmt.Println("1. Create .cursor/mcp.json in your project root") + fmt.Println("2. Replace with your Supabase project reference") + fmt.Println("3. Replace with your PAT from:") + fmt.Println(" https://supabase.com/dashboard/account/tokens") + fmt.Println("4. Open Cursor → Settings → MCP to verify connection") + + case "vscode": + fmt.Println("1. Create .vscode/mcp.json in your project root") + fmt.Println("2. Replace with your Supabase project reference") + fmt.Println("3. Open Copilot chat and switch to Agent mode") + fmt.Println("4. You'll be prompted for your PAT when first using the server") + + case "claude-desktop": + fmt.Println("1. Open Claude Desktop → Settings → Developer → Edit Config") + fmt.Println("2. Replace with your Supabase project reference") + fmt.Println("3. Replace with your PAT") + fmt.Println("4. Restart Claude Desktop") + fmt.Println("5. Look for the hammer (MCP) icon in new chats") + + case "claude-code": + fmt.Println("Option 1 - Project-scoped (.mcp.json file):") + fmt.Println(" 1. Create .mcp.json in your project root") + fmt.Println(" 2. Add the configuration above") + fmt.Println(" 3. Replace and ") + fmt.Println() + fmt.Println("Option 2 - Local-scoped (CLI command):") + fmt.Println(" claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN= -- npx -y @supabase/mcp-server-supabase@latest") + + case "windsurf": + fmt.Println("1. Open Windsurf Cascade assistant") + fmt.Println("2. Click the hammer (MCP) icon → Configure") + fmt.Println("3. Add the configuration above") + fmt.Println("4. Replace and ") + fmt.Println("5. Save and tap Refresh in Cascade") + + case "cline": + fmt.Println("1. Open Cline extension in VS Code") + fmt.Println("2. Tap MCP Servers icon → Configure MCP Servers") + fmt.Println("3. Add the configuration above") + fmt.Println("4. Replace and ") + fmt.Println("5. Cline will auto-reload the configuration") + } + + fmt.Println() + fmt.Println("📚 Full documentation: https://supabase.com/docs/guides/getting-started/mcp") +} + +func getCursorStyleConfig() map[string]interface{} { + command := "npx" + args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} + + if runtime.GOOS == "windows" { + command = "cmd" + args = append([]interface{}{"/c", "npx"}, args[1:]...) + } + + return map[string]interface{}{ + "mcpServers": map[string]interface{}{ + "supabase": map[string]interface{}{ + "command": command, + "args": args, + "env": map[string]string{ + "SUPABASE_ACCESS_TOKEN": "", + }, + }, + }, + } +} + +func getVSCodeConfig() map[string]interface{} { + command := "npx" + args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} + + if runtime.GOOS == "windows" { + command = "cmd" + args = append([]interface{}{"/c", "npx"}, args[1:]...) + } + + return map[string]interface{}{ + "inputs": []map[string]interface{}{ + { + "type": "promptString", + "id": "supabase-access-token", + "description": "Supabase personal access token", + "password": true, + }, + }, + "servers": map[string]interface{}{ + "supabase": map[string]interface{}{ + "command": command, + "args": args, + "env": map[string]string{ + "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", + }, + }, + }, + } +} + +func getClaudeCodeConfig() map[string]interface{} { + return getCursorStyleConfig() // Same format } From 74edb0c5fbaa950953aaf17146b68e4cd72d7d5f Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 15:10:47 +0000 Subject: [PATCH 04/17] feat: add interactive auto-configuration for Cursor, VS Code, and Claude Code - Add configureJSONClient() to automatically create/update mcp.json files - Support project-local and global configuration options - Prompt for project reference interactively - Handle existing configs by merging (not overwriting) - Add configureClaudeCode() to use Claude CLI for configuration - Detect if Claude CLI is available and run 'claude mcp add' command - Display step-by-step next actions after configuration - Mark clients as CanAutomate: cursor, vscode, claude-code --- internal/mcp/init/init.go | 651 +++++++++++++++++++++++++++----------- 1 file changed, 462 insertions(+), 189 deletions(-) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 728ded80b..dda117890 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" +"os/exec" "path/filepath" "runtime" "strings" @@ -17,6 +18,7 @@ type ClientConfig struct { ConfigPath string ConfigType string // "cursor", "vscode", "claude-desktop", "claude-code", "windsurf", "cline" Detected bool + CanAutomate bool // Can we automatically configure this client? } // Run executes the MCP initialization wizard @@ -36,7 +38,13 @@ func Run(ctx context.Context, fsys afero.Fs, targetClient string) error { found = true fmt.Printf("📍 Configuring for: %s\n", c.Name) fmt.Printf(" Config path: %s\n\n", c.ConfigPath) - displayConfigInstructions(c.ConfigType) + + // Check if we can automate configuration + if c.CanAutomate { + return configureClient(c, fsys) + } else { + displayConfigInstructions(c.ConfigType) + } break } } @@ -71,10 +79,10 @@ func Run(ctx context.Context, fsys afero.Fs, targetClient string) error { fmt.Println(" supabase mcp init --client ") fmt.Println() fmt.Println(" Supported clients:") - fmt.Println(" • cursor") - fmt.Println(" • vscode") + fmt.Println(" • cursor (auto-configurable)") + fmt.Println(" • vscode (auto-configurable)") + fmt.Println(" • claude-code (auto-configurable via CLI)") fmt.Println(" • claude-desktop") - fmt.Println(" • claude-code") fmt.Println(" • windsurf") fmt.Println(" • cline") } @@ -82,216 +90,481 @@ func Run(ctx context.Context, fsys afero.Fs, targetClient string) error { return nil } -// detectClients checks for installed MCP clients -func detectClients() []ClientConfig { - homeDir, _ := os.UserHomeDir() +// configureClient automatically configures the specified client +func configureClient(client ClientConfig, fsys afero.Fs) error { + fmt.Println("🔧 Auto-configuration mode") + fmt.Println() - clients := []ClientConfig{ - { - Name: "Cursor", - ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), - ConfigType: "cursor", - }, - { - Name: "VS Code (Copilot)", - ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), - ConfigType: "vscode", - }, - { - Name: "Claude Desktop", - ConfigPath: getClaudeDesktopConfigPath(homeDir), - ConfigType: "claude-desktop", - }, - { - Name: "Claude Code", - ConfigPath: filepath.Join(homeDir, ".mcp.json"), - ConfigType: "claude-code", - }, - { - Name: "Windsurf", - ConfigPath: getWindsurfConfigPath(homeDir), - ConfigType: "windsurf", - }, - { - Name: "Cline (VS Code)", - ConfigPath: getClineConfigPath(homeDir), - ConfigType: "cline", - }, + switch client.ConfigType { + case "cursor", "vscode": + return configureJSONClient(client, fsys) + case "claude-code": + return configureClaudeCode() + default: + displayConfigInstructions(client.ConfigType) + return nil } +} - // Check which clients are actually installed - for i := range clients { - if _, err := os.Stat(filepath.Dir(clients[i].ConfigPath)); err == nil { - clients[i].Detected = true - } +// configureJSONClient creates or updates mcp.json for Cursor/VS Code +func configureJSONClient(client ClientConfig, fsys afero.Fs) error { + // Prompt for project ref + fmt.Print("📝 Enter your Supabase project reference (or press Enter to skip): ") + var projectRef string + fmt.Scanln(&projectRef) + + if projectRef == "" { + projectRef = "" + fmt.Println(" ⚠️ You'll need to replace in the config file") } + + // Determine config directory + configDir := filepath.Dir(client.ConfigPath) + configFile := client.ConfigPath + + // Check if we should use project-local config + cwd, _ := os.Getwd() + var useProjectLocal bool + + fmt.Println() + fmt.Printf("📂 Configuration location options:\n") + fmt.Printf(" 1. Project-local: %s\n", filepath.Join(cwd, filepath.Base(configDir), "mcp.json")) + fmt.Printf(" 2. Global: %s\n", configFile) + fmt.Print(" Choose (1/2) [1]: ") + + var choice string + fmt.Scanln(&choice) + + if choice == "" || choice == "1" { + useProjectLocal = true + configDir = filepath.Join(cwd, filepath.Base(configDir)) + configFile = filepath.Join(configDir, "mcp.json") + } + + fmt.Println() + fmt.Printf("📁 Creating config in: %s\n", configFile) + + // Create directory if it doesn't exist +if err := os.MkdirAll(configDir, 0755); err != nil { +return fmt.Errorf("failed to create config directory: %w", err) +} + +// Read existing config or create new one +var config map[string]interface{} +existingData, err := os.ReadFile(configFile) +if err == nil { +// Config exists, parse it +if err := json.Unmarshal(existingData, &config); err != nil { +return fmt.Errorf("failed to parse existing config: %w", err) +} +fmt.Println(" ✓ Found existing configuration") +} else { +// Create new config +config = make(map[string]interface{}) +fmt.Println(" ✓ Creating new configuration") +} + +// Generate the Supabase MCP server config +var supabaseConfig map[string]interface{} +if client.ConfigType == "vscode" { +supabaseConfig = getVSCodeServerConfig(projectRef) +// Ensure inputs array exists +if _, ok := config["inputs"]; !ok { +config["inputs"] = []map[string]interface{}{} +} +// Add input for PAT if not exists +inputs := config["inputs"].([]interface{}) +hasInput := false +for _, input := range inputs { +if inputMap, ok := input.(map[string]interface{}); ok { +if inputMap["id"] == "supabase-access-token" { +hasInput = true +break +} +} +} +if !hasInput { +inputs = append(inputs, map[string]interface{}{ +"type": "promptString", +"id": "supabase-access-token", +"description": "Supabase personal access token", +"password": true, +}) +config["inputs"] = inputs +} +// Add to servers +if _, ok := config["servers"]; !ok { +config["servers"] = make(map[string]interface{}) +} +servers := config["servers"].(map[string]interface{}) +servers["supabase"] = supabaseConfig +} else { +// Cursor, Windsurf, Cline format +supabaseConfig = getCursorStyleServerConfig(projectRef) +if _, ok := config["mcpServers"]; !ok { +config["mcpServers"] = make(map[string]interface{}) +} +mcpServers := config["mcpServers"].(map[string]interface{}) +mcpServers["supabase"] = supabaseConfig +} + +// Write config file +configJSON, err := json.MarshalIndent(config, "", " ") +if err != nil { +return fmt.Errorf("failed to marshal config: %w", err) +} + +if err := os.WriteFile(configFile, configJSON, 0644); err != nil { +return fmt.Errorf("failed to write config file: %w", err) +} - return clients +fmt.Println() +fmt.Println("✅ Configuration complete!") +fmt.Println() +fmt.Printf("📄 Config file: %s\n", configFile) +fmt.Println() + +if projectRef == "" { +fmt.Println("⚠️ Next steps:") +fmt.Println(" 1. Edit the config file and replace with your project reference") +fmt.Println(" 2. Replace with your PAT from:") +fmt.Println(" https://supabase.com/dashboard/account/tokens") +} else { +fmt.Println("⚠️ Next steps:") +if client.ConfigType == "vscode" { +fmt.Println(" 1. Open Copilot chat and switch to Agent mode") +fmt.Println(" 2. You'll be prompted for your PAT when first using the server") +} else { +fmt.Println(" 1. Edit the config file and replace with your PAT from:") +fmt.Println(" https://supabase.com/dashboard/account/tokens") +fmt.Println(" 2. Restart", client.Name) +} +} + +if client.ConfigType == "cursor" { +fmt.Println() +fmt.Println("💡 In Cursor, go to Settings → MCP to verify the connection") +} + +return nil +} + +// configureClaudeCode uses the Claude CLI to configure the server +func configureClaudeCode() error { +fmt.Println("🤖 Claude Code can be configured using the Claude CLI") +fmt.Println() + +// Check if claude CLI is available +if _, err := exec.LookPath("claude"); err != nil { +fmt.Println("⚠️ Claude CLI not found in PATH") +fmt.Println() +fmt.Println("Please install the Claude CLI first, or use manual configuration:") +displayConfigInstructions("claude-code") +return nil +} + +fmt.Println("✓ Found Claude CLI") +fmt.Println() + +// Prompt for configuration +fmt.Print("📝 Enter your Supabase project reference (or press Enter to skip): ") +var projectRef string +fmt.Scanln(&projectRef) + +fmt.Print("🔑 Enter your Supabase Personal Access Token (or press Enter to skip): ") +var token string +fmt.Scanln(&token) + +if projectRef == "" || token == "" { +fmt.Println() +fmt.Println("⚠️ Project ref or token not provided. Here's the command to run manually:") +fmt.Println() +fmt.Println(" claude mcp add supabase -s local \\") +fmt.Println(" -e SUPABASE_ACCESS_TOKEN= \\") +fmt.Println(" -- npx -y @supabase/mcp-server-supabase@latest \\") +fmt.Println(" --read-only --project-ref=") +fmt.Println() +return nil +} + +// Build the command +args := []string{ +"mcp", "add", "supabase", +"-s", "local", +"-e", fmt.Sprintf("SUPABASE_ACCESS_TOKEN=%s", token), +"--", +"npx", "-y", "@supabase/mcp-server-supabase@latest", +"--read-only", +fmt.Sprintf("--project-ref=%s", projectRef), +} + +fmt.Println() +fmt.Println("🚀 Running: claude mcp add supabase...") + +cmd := exec.Command("claude", args...) +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr + +if err := cmd.Run(); err != nil { +return fmt.Errorf("failed to configure Claude Code: %w", err) +} + +fmt.Println() +fmt.Println("✅ Claude Code configured successfully!") + +return nil +} + +// detectClients checks for installed MCP clients +func detectClients() []ClientConfig { +homeDir, _ := os.UserHomeDir() + +clients := []ClientConfig{ +{ +Name: "Cursor", +ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), +ConfigType: "cursor", +CanAutomate: true, +}, +{ +Name: "VS Code (Copilot)", +ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), +ConfigType: "vscode", +CanAutomate: true, +}, +{ +Name: "Claude Desktop", +ConfigPath: getClaudeDesktopConfigPath(homeDir), +ConfigType: "claude-desktop", +CanAutomate: false, +}, +{ +Name: "Claude Code", +ConfigPath: filepath.Join(homeDir, ".mcp.json"), +ConfigType: "claude-code", +CanAutomate: true, // Via CLI +}, +{ +Name: "Windsurf", +ConfigPath: getWindsurfConfigPath(homeDir), +ConfigType: "windsurf", +CanAutomate: false, +}, +{ +Name: "Cline (VS Code)", +ConfigPath: getClineConfigPath(homeDir), +ConfigType: "cline", +CanAutomate: false, +}, +} + +// Check which clients are actually installed +for i := range clients { +if _, err := os.Stat(filepath.Dir(clients[i].ConfigPath)); err == nil { +clients[i].Detected = true +} +} + +return clients } func getClaudeDesktopConfigPath(homeDir string) string { - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json") - default: // linux - return filepath.Join(homeDir, ".config", "Claude", "claude_desktop_config.json") - } +switch runtime.GOOS { +case "darwin": +return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") +case "windows": +return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json") +default: // linux +return filepath.Join(homeDir, ".config", "Claude", "claude_desktop_config.json") +} } func getWindsurfConfigPath(homeDir string) string { - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") - default: // linux - return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") - } +switch runtime.GOOS { +case "darwin": +return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") +case "windows": +return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") +default: // linux +return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") +} } func getClineConfigPath(homeDir string) string { - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") - default: // linux - return filepath.Join(homeDir, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") - } +switch runtime.GOOS { +case "darwin": +return filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") +case "windows": +return filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") +default: // linux +return filepath.Join(homeDir, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") +} } // displayConfigInstructions shows the configuration template for a specific client func displayConfigInstructions(clientType string) { - fmt.Println("📝 Configuration Template:") - fmt.Println() - - var config interface{} - var configStr string - - switch clientType { - case "cursor", "windsurf", "cline": - config = getCursorStyleConfig() - case "vscode": - config = getVSCodeConfig() - case "claude-desktop": - config = getCursorStyleConfig() // Same format as Cursor - case "claude-code": - config = getClaudeCodeConfig() - } - - configJSON, _ := json.MarshalIndent(config, "", " ") - configStr = string(configJSON) - - // Add platform-specific commands - fmt.Println(configStr) - fmt.Println() - - fmt.Println("📋 Setup Instructions:") - fmt.Println() - - switch clientType { - case "cursor": - fmt.Println("1. Create .cursor/mcp.json in your project root") - fmt.Println("2. Replace with your Supabase project reference") - fmt.Println("3. Replace with your PAT from:") - fmt.Println(" https://supabase.com/dashboard/account/tokens") - fmt.Println("4. Open Cursor → Settings → MCP to verify connection") - - case "vscode": - fmt.Println("1. Create .vscode/mcp.json in your project root") - fmt.Println("2. Replace with your Supabase project reference") - fmt.Println("3. Open Copilot chat and switch to Agent mode") - fmt.Println("4. You'll be prompted for your PAT when first using the server") - - case "claude-desktop": - fmt.Println("1. Open Claude Desktop → Settings → Developer → Edit Config") - fmt.Println("2. Replace with your Supabase project reference") - fmt.Println("3. Replace with your PAT") - fmt.Println("4. Restart Claude Desktop") - fmt.Println("5. Look for the hammer (MCP) icon in new chats") - - case "claude-code": - fmt.Println("Option 1 - Project-scoped (.mcp.json file):") - fmt.Println(" 1. Create .mcp.json in your project root") - fmt.Println(" 2. Add the configuration above") - fmt.Println(" 3. Replace and ") - fmt.Println() - fmt.Println("Option 2 - Local-scoped (CLI command):") - fmt.Println(" claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN= -- npx -y @supabase/mcp-server-supabase@latest") - - case "windsurf": - fmt.Println("1. Open Windsurf Cascade assistant") - fmt.Println("2. Click the hammer (MCP) icon → Configure") - fmt.Println("3. Add the configuration above") - fmt.Println("4. Replace and ") - fmt.Println("5. Save and tap Refresh in Cascade") - - case "cline": - fmt.Println("1. Open Cline extension in VS Code") - fmt.Println("2. Tap MCP Servers icon → Configure MCP Servers") - fmt.Println("3. Add the configuration above") - fmt.Println("4. Replace and ") - fmt.Println("5. Cline will auto-reload the configuration") - } - - fmt.Println() - fmt.Println("📚 Full documentation: https://supabase.com/docs/guides/getting-started/mcp") +fmt.Println("📝 Manual Configuration Required") +fmt.Println() +fmt.Println("Configuration Template:") +fmt.Println() + +var config interface{} + +switch clientType { +case "cursor", "windsurf", "cline": +config = getCursorStyleConfig() +case "vscode": +config = getVSCodeConfig() +case "claude-desktop": +config = getCursorStyleConfig() // Same format as Cursor +case "claude-code": +config = getClaudeCodeConfig() +} + +configJSON, _ := json.MarshalIndent(config, "", " ") +fmt.Println(string(configJSON)) +fmt.Println() + +fmt.Println("📋 Setup Instructions:") +fmt.Println() + +switch clientType { +case "cursor": +fmt.Println("1. Create .cursor/mcp.json in your project root") +fmt.Println("2. Replace with your Supabase project reference") +fmt.Println("3. Replace with your PAT from:") +fmt.Println(" https://supabase.com/dashboard/account/tokens") +fmt.Println("4. Open Cursor → Settings → MCP to verify connection") + +case "vscode": +fmt.Println("1. Create .vscode/mcp.json in your project root") +fmt.Println("2. Replace with your Supabase project reference") +fmt.Println("3. Open Copilot chat and switch to Agent mode") +fmt.Println("4. You'll be prompted for your PAT when first using the server") + +case "claude-desktop": +fmt.Println("1. Open Claude Desktop → Settings → Developer → Edit Config") +fmt.Println("2. Replace with your Supabase project reference") +fmt.Println("3. Replace with your PAT") +fmt.Println("4. Restart Claude Desktop") +fmt.Println("5. Look for the hammer (MCP) icon in new chats") + +case "claude-code": +fmt.Println("Option 1 - Project-scoped (.mcp.json file):") +fmt.Println(" 1. Create .mcp.json in your project root") +fmt.Println(" 2. Add the configuration above") +fmt.Println(" 3. Replace and ") +fmt.Println() +fmt.Println("Option 2 - Local-scoped (CLI command):") +fmt.Println(" claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN= -- npx -y @supabase/mcp-server-supabase@latest") + +case "windsurf": +fmt.Println("1. Open Windsurf Cascade assistant") +fmt.Println("2. Click the hammer (MCP) icon → Configure") +fmt.Println("3. Add the configuration above") +fmt.Println("4. Replace and ") +fmt.Println("5. Save and tap Refresh in Cascade") + +case "cline": +fmt.Println("1. Open Cline extension in VS Code") +fmt.Println("2. Tap MCP Servers icon → Configure MCP Servers") +fmt.Println("3. Add the configuration above") +fmt.Println("4. Replace and ") +fmt.Println("5. Cline will auto-reload the configuration") +} + +fmt.Println() +fmt.Println("📚 Full documentation: https://supabase.com/docs/guides/getting-started/mcp") } func getCursorStyleConfig() map[string]interface{} { - command := "npx" - args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} - - if runtime.GOOS == "windows" { - command = "cmd" - args = append([]interface{}{"/c", "npx"}, args[1:]...) - } - - return map[string]interface{}{ - "mcpServers": map[string]interface{}{ - "supabase": map[string]interface{}{ - "command": command, - "args": args, - "env": map[string]string{ - "SUPABASE_ACCESS_TOKEN": "", - }, - }, - }, - } +command := "npx" +args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} + +if runtime.GOOS == "windows" { +command = "cmd" +args = append([]interface{}{"/c", "npx"}, args[1:]...) +} + +return map[string]interface{}{ +"mcpServers": map[string]interface{}{ +"supabase": map[string]interface{}{ +"command": command, +"args": args, +"env": map[string]string{ +"SUPABASE_ACCESS_TOKEN": "", +}, +}, +}, +} +} + +func getCursorStyleServerConfig(projectRef string) map[string]interface{} { +command := "npx" +args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", fmt.Sprintf("--project-ref=%s", projectRef)} + +if runtime.GOOS == "windows" { +command = "cmd" +args = append([]interface{}{"/c", "npx"}, args[1:]...) +} + +return map[string]interface{}{ +"command": command, +"args": args, +"env": map[string]string{ +"SUPABASE_ACCESS_TOKEN": "", +}, +} } func getVSCodeConfig() map[string]interface{} { - command := "npx" - args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} - - if runtime.GOOS == "windows" { - command = "cmd" - args = append([]interface{}{"/c", "npx"}, args[1:]...) - } - - return map[string]interface{}{ - "inputs": []map[string]interface{}{ - { - "type": "promptString", - "id": "supabase-access-token", - "description": "Supabase personal access token", - "password": true, - }, - }, - "servers": map[string]interface{}{ - "supabase": map[string]interface{}{ - "command": command, - "args": args, - "env": map[string]string{ - "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", - }, - }, - }, - } +command := "npx" +args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} + +if runtime.GOOS == "windows" { +command = "cmd" +args = append([]interface{}{"/c", "npx"}, args[1:]...) +} + +return map[string]interface{}{ +"inputs": []map[string]interface{}{ +{ +"type": "promptString", +"id": "supabase-access-token", +"description": "Supabase personal access token", +"password": true, +}, +}, +"servers": map[string]interface{}{ +"supabase": map[string]interface{}{ +"command": command, +"args": args, +"env": map[string]string{ +"SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", +}, +}, +}, +} +} + +func getVSCodeServerConfig(projectRef string) map[string]interface{} { +command := "npx" +args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", fmt.Sprintf("--project-ref=%s", projectRef)} + +if runtime.GOOS == "windows" { +command = "cmd" +args = append([]interface{}{"/c", "npx"}, args[1:]...) +} + +return map[string]interface{}{ +"command": command, +"args": args, +"env": map[string]string{ +"SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", +}, +} } func getClaudeCodeConfig() map[string]interface{} { - return getCursorStyleConfig() // Same format +return getCursorStyleConfig() // Same format } From 7065cefde56c4e9297600c45586230939d740b10 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 15:19:36 +0000 Subject: [PATCH 05/17] feat(mcp): add client installation validation and remote server support - Add actual client binary/app detection instead of just checking config directories - Add isClientInstalled() that checks for: * Cursor: cursor binary or Cursor.app * VS Code: code binary * Claude Desktop: Claude.app * Claude Code: claude CLI * Windsurf: windsurf binary or Windsurf.app * Cline: VS Code (as it's an extension) - Switch from local NPX server to remote OAuth server at https://mcp.supabase.com/mcp - Update config templates to use remote server format with: * type: 'remote' * url: https://mcp.supabase.com/mcp * OAuth configuration with api.supabase.com * SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN env var - Show manual config instructions when client not installed - Separate installed vs. available clients in interactive mode - Update Claude Code auto-config to use 'remote' command instead of npx - Update all manual instruction templates to use remote server format --- internal/mcp/init/init.go | 980 ++++++++++++++++++-------------------- 1 file changed, 471 insertions(+), 509 deletions(-) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index dda117890..bdb670556 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -14,557 +14,519 @@ import ( ) type ClientConfig struct { - Name string - ConfigPath string - ConfigType string // "cursor", "vscode", "claude-desktop", "claude-code", "windsurf", "cline" - Detected bool - CanAutomate bool // Can we automatically configure this client? + Name string + ConfigPath string + Description string + Detected bool + Installed bool + CanAutomate bool } -// Run executes the MCP initialization wizard -func Run(ctx context.Context, fsys afero.Fs, targetClient string) error { - fmt.Println("🚀 Supabase MCP Server Setup") - fmt.Println("───────────────────────────────") - fmt.Println() - - // Detect or validate client - clients := detectClients() - - if targetClient != "" { - // Validate the specified client - found := false - for _, c := range clients { - if strings.EqualFold(c.ConfigType, targetClient) { - found = true - fmt.Printf("📍 Configuring for: %s\n", c.Name) - fmt.Printf(" Config path: %s\n\n", c.ConfigPath) - - // Check if we can automate configuration - if c.CanAutomate { - return configureClient(c, fsys) - } else { - displayConfigInstructions(c.ConfigType) - } +func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { + clients := detectClients(fsys) + + var selectedClient *ClientConfig + if clientFlag != "" { + // Find the specified client + for i := range clients { + if strings.EqualFold(clients[i].Name, clientFlag) { + selectedClient = &clients[i] break } } - if !found { - return fmt.Errorf("unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline", targetClient) + if selectedClient == nil { + return fmt.Errorf("Unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline", clientFlag) } - } else { - // Display detected clients - fmt.Println("🔍 Detected MCP-compatible clients:") - fmt.Println() - detectedCount := 0 - for _, c := range clients { - if c.Detected { - fmt.Printf(" ✅ %s\n", c.Name) - fmt.Printf(" Config: %s\n", c.ConfigPath) - detectedCount++ - } + // Check if client is actually installed + if !selectedClient.Installed { + fmt.Printf("❌ %s is not installed on this system.\n\n", selectedClient.Name) + displayConfigInstructions(*selectedClient) + return nil } - if detectedCount == 0 { - fmt.Println(" ⚠️ No MCP clients detected on this system") - fmt.Println() - fmt.Println("Supported clients:") - for _, c := range clients { - fmt.Printf(" • %s (%s)\n", c.Name, c.ConfigPath) - } + // Auto-configure if possible + if selectedClient.CanAutomate { + return configureClient(ctx, fsys, *selectedClient) } - fmt.Println() - fmt.Println("💡 To configure a specific client, use:") - fmt.Println(" supabase mcp init --client ") - fmt.Println() - fmt.Println(" Supported clients:") - fmt.Println(" • cursor (auto-configurable)") - fmt.Println(" • vscode (auto-configurable)") - fmt.Println(" • claude-code (auto-configurable via CLI)") - fmt.Println(" • claude-desktop") - fmt.Println(" • windsurf") - fmt.Println(" • cline") - } - - return nil -} - -// configureClient automatically configures the specified client -func configureClient(client ClientConfig, fsys afero.Fs) error { - fmt.Println("🔧 Auto-configuration mode") - fmt.Println() - - switch client.ConfigType { - case "cursor", "vscode": - return configureJSONClient(client, fsys) - case "claude-code": - return configureClaudeCode() - default: - displayConfigInstructions(client.ConfigType) + // Otherwise show manual instructions + displayConfigInstructions(*selectedClient) return nil } -} - -// configureJSONClient creates or updates mcp.json for Cursor/VS Code -func configureJSONClient(client ClientConfig, fsys afero.Fs) error { - // Prompt for project ref - fmt.Print("📝 Enter your Supabase project reference (or press Enter to skip): ") - var projectRef string - fmt.Scanln(&projectRef) - - if projectRef == "" { - projectRef = "" - fmt.Println(" ⚠️ You'll need to replace in the config file") - } - // Determine config directory - configDir := filepath.Dir(client.ConfigPath) - configFile := client.ConfigPath + // Interactive mode: show detected clients + fmt.Println("Supabase MCP Server Configuration") + fmt.Println("==================================\n") - // Check if we should use project-local config - cwd, _ := os.Getwd() - var useProjectLocal bool + var installedClients []ClientConfig + var notInstalledClients []ClientConfig - fmt.Println() - fmt.Printf("📂 Configuration location options:\n") - fmt.Printf(" 1. Project-local: %s\n", filepath.Join(cwd, filepath.Base(configDir), "mcp.json")) - fmt.Printf(" 2. Global: %s\n", configFile) - fmt.Print(" Choose (1/2) [1]: ") + for _, client := range clients { + if client.Installed { + installedClients = append(installedClients, client) + } else { + notInstalledClients = append(notInstalledClients, client) + } + } - var choice string - fmt.Scanln(&choice) + if len(installedClients) > 0 { + fmt.Println("✓ Detected MCP Clients:") + for _, client := range installedClients { + fmt.Printf(" - %s: %s\n", client.Name, client.Description) + } + fmt.Println() + } - if choice == "" || choice == "1" { - useProjectLocal = true - configDir = filepath.Join(cwd, filepath.Base(configDir)) - configFile = filepath.Join(configDir, "mcp.json") + if len(notInstalledClients) > 0 { + fmt.Println("ℹ Available but not installed:") + for _, client := range notInstalledClients { + fmt.Printf(" - %s: %s\n", client.Name, client.Description) + } + fmt.Println() } - fmt.Println() - fmt.Printf("📁 Creating config in: %s\n", configFile) + if len(installedClients) == 0 { + fmt.Println("No MCP clients detected. Install one of the supported clients:") + fmt.Println(" • Cursor: https://cursor.sh") + fmt.Println(" • VS Code: https://code.visualstudio.com") + fmt.Println(" • Claude Desktop: https://claude.ai/download") + fmt.Println(" • Claude Code: Install via 'npm install -g @anthropic-ai/claude-cli'") + fmt.Println(" • Windsurf: https://codeium.com/windsurf") + fmt.Println(" • Cline: Install as VS Code extension") + return nil + } - // Create directory if it doesn't exist -if err := os.MkdirAll(configDir, 0755); err != nil { -return fmt.Errorf("failed to create config directory: %w", err) -} - -// Read existing config or create new one -var config map[string]interface{} -existingData, err := os.ReadFile(configFile) -if err == nil { -// Config exists, parse it -if err := json.Unmarshal(existingData, &config); err != nil { -return fmt.Errorf("failed to parse existing config: %w", err) -} -fmt.Println(" ✓ Found existing configuration") -} else { -// Create new config -config = make(map[string]interface{}) -fmt.Println(" ✓ Creating new configuration") -} - -// Generate the Supabase MCP server config -var supabaseConfig map[string]interface{} -if client.ConfigType == "vscode" { -supabaseConfig = getVSCodeServerConfig(projectRef) -// Ensure inputs array exists -if _, ok := config["inputs"]; !ok { -config["inputs"] = []map[string]interface{}{} -} -// Add input for PAT if not exists -inputs := config["inputs"].([]interface{}) -hasInput := false -for _, input := range inputs { -if inputMap, ok := input.(map[string]interface{}); ok { -if inputMap["id"] == "supabase-access-token" { -hasInput = true -break -} -} -} -if !hasInput { -inputs = append(inputs, map[string]interface{}{ -"type": "promptString", -"id": "supabase-access-token", -"description": "Supabase personal access token", -"password": true, -}) -config["inputs"] = inputs -} -// Add to servers -if _, ok := config["servers"]; !ok { -config["servers"] = make(map[string]interface{}) -} -servers := config["servers"].(map[string]interface{}) -servers["supabase"] = supabaseConfig -} else { -// Cursor, Windsurf, Cline format -supabaseConfig = getCursorStyleServerConfig(projectRef) -if _, ok := config["mcpServers"]; !ok { -config["mcpServers"] = make(map[string]interface{}) -} -mcpServers := config["mcpServers"].(map[string]interface{}) -mcpServers["supabase"] = supabaseConfig -} - -// Write config file -configJSON, err := json.MarshalIndent(config, "", " ") -if err != nil { -return fmt.Errorf("failed to marshal config: %w", err) + fmt.Println("Use the --client flag to configure a specific client.") + fmt.Println("Example: supabase mcp init --client cursor") + + return nil } -if err := os.WriteFile(configFile, configJSON, 0644); err != nil { -return fmt.Errorf("failed to write config file: %w", err) +func detectClients(fsys afero.Fs) []ClientConfig { + homeDir, _ := os.UserHomeDir() + + clients := []ClientConfig{ + { + Name: "cursor", + ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), + Description: "Cursor AI Editor", + CanAutomate: true, + }, + { + Name: "vscode", + ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), + Description: "Visual Studio Code with Copilot", + CanAutomate: true, + }, + { + Name: "claude-desktop", + ConfigPath: getClaudeDesktopConfigPath(), + Description: "Claude Desktop App", + CanAutomate: false, + }, + { + Name: "claude-code", + ConfigPath: filepath.Join(homeDir, ".mcp.json"), + Description: "Claude Code CLI", + CanAutomate: true, + }, + { + Name: "windsurf", + ConfigPath: getWindsurfConfigPath(), + Description: "Windsurf Editor by Codeium", + CanAutomate: false, + }, + { + Name: "cline", + ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), + Description: "Cline VS Code Extension", + CanAutomate: false, + }, + } + + // Check for directory existence (config folder) + for i := range clients { + configDir := filepath.Dir(clients[i].ConfigPath) + if _, err := fsys.Stat(configDir); err == nil { + clients[i].Detected = true + } + + // Check if client is actually installed + clients[i].Installed = isClientInstalled(clients[i].Name) + } + + return clients +} + +func isClientInstalled(clientName string) bool { + switch clientName { + case "cursor": + // Check for cursor binary + return commandExists("cursor") || appExists("Cursor") + case "vscode": + // Check for code binary + return commandExists("code") + case "claude-desktop": + // Check for Claude app + return appExists("Claude") + case "claude-code": + // Check for claude CLI + return commandExists("claude") + case "windsurf": + // Check for windsurf binary or app + return commandExists("windsurf") || appExists("Windsurf") + case "cline": + // Cline is a VS Code extension, check if VS Code is installed + return commandExists("code") + default: + return false + } } -fmt.Println() -fmt.Println("✅ Configuration complete!") -fmt.Println() -fmt.Printf("📄 Config file: %s\n", configFile) -fmt.Println() - -if projectRef == "" { -fmt.Println("⚠️ Next steps:") -fmt.Println(" 1. Edit the config file and replace with your project reference") -fmt.Println(" 2. Replace with your PAT from:") -fmt.Println(" https://supabase.com/dashboard/account/tokens") -} else { -fmt.Println("⚠️ Next steps:") -if client.ConfigType == "vscode" { -fmt.Println(" 1. Open Copilot chat and switch to Agent mode") -fmt.Println(" 2. You'll be prompted for your PAT when first using the server") -} else { -fmt.Println(" 1. Edit the config file and replace with your PAT from:") -fmt.Println(" https://supabase.com/dashboard/account/tokens") -fmt.Println(" 2. Restart", client.Name) -} +func commandExists(command string) bool { + _, err := exec.LookPath(command) + return err == nil } -if client.ConfigType == "cursor" { -fmt.Println() -fmt.Println("💡 In Cursor, go to Settings → MCP to verify the connection") +func appExists(appName string) bool { + if runtime.GOOS == "darwin" { + // Check in common macOS app locations + locations := []string{ + fmt.Sprintf("/Applications/%s.app", appName), + fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), + } + for _, location := range locations { + if _, err := os.Stat(location); err == nil { + return true + } + } + } + return false } -return nil +func getClaudeDesktopConfigPath() string { + homeDir, _ := os.UserHomeDir() + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "config.json") + default: + return filepath.Join(homeDir, ".config", "Claude", "config.json") + } } -// configureClaudeCode uses the Claude CLI to configure the server -func configureClaudeCode() error { -fmt.Println("🤖 Claude Code can be configured using the Claude CLI") -fmt.Println() - -// Check if claude CLI is available -if _, err := exec.LookPath("claude"); err != nil { -fmt.Println("⚠️ Claude CLI not found in PATH") -fmt.Println() -fmt.Println("Please install the Claude CLI first, or use manual configuration:") -displayConfigInstructions("claude-code") -return nil +func getWindsurfConfigPath() string { + homeDir, _ := os.UserHomeDir() + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") + default: + return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") + } } -fmt.Println("✓ Found Claude CLI") -fmt.Println() - -// Prompt for configuration -fmt.Print("📝 Enter your Supabase project reference (or press Enter to skip): ") -var projectRef string -fmt.Scanln(&projectRef) - -fmt.Print("🔑 Enter your Supabase Personal Access Token (or press Enter to skip): ") -var token string -fmt.Scanln(&token) - -if projectRef == "" || token == "" { -fmt.Println() -fmt.Println("⚠️ Project ref or token not provided. Here's the command to run manually:") -fmt.Println() -fmt.Println(" claude mcp add supabase -s local \\") -fmt.Println(" -e SUPABASE_ACCESS_TOKEN= \\") -fmt.Println(" -- npx -y @supabase/mcp-server-supabase@latest \\") -fmt.Println(" --read-only --project-ref=") -fmt.Println() -return nil +func configureClient(ctx context.Context, fsys afero.Fs, client ClientConfig) error { + if client.Name == "claude-code" { + return configureClaudeCode(ctx) + } + + // JSON-based clients (Cursor, VS Code) + return configureJSONClient(ctx, fsys, client) } -// Build the command -args := []string{ -"mcp", "add", "supabase", -"-s", "local", -"-e", fmt.Sprintf("SUPABASE_ACCESS_TOKEN=%s", token), +func configureClaudeCode(ctx context.Context) error { + fmt.Printf("Configuring Claude Code...\n\n") + + // Prompt for scope + fmt.Println("Would you like to add this server:") + fmt.Println(" 1. Globally (available in all projects)") + fmt.Println(" 2. Locally (only in current project)") + fmt.Print("Choice [1]: ") + + var scope string + fmt.Scanln(&scope) + if scope == "" { + scope = "1" + } + + scopeFlag := "global" + if scope == "2" { + scopeFlag = "local" + } + + // Prompt for access token + fmt.Print("\nEnter your Supabase access token: ") + var accessToken string + fmt.Scanln(&accessToken) + + if accessToken == "" { + return fmt.Errorf("Access token is required") + } + + // Build the claude mcp add command for remote server + cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "supabase", +"-s", scopeFlag, +"-e", fmt.Sprintf("SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s", accessToken), "--", -"npx", "-y", "@supabase/mcp-server-supabase@latest", -"--read-only", -fmt.Sprintf("--project-ref=%s", projectRef), -} - -fmt.Println() -fmt.Println("🚀 Running: claude mcp add supabase...") - -cmd := exec.Command("claude", args...) -cmd.Stdout = os.Stdout -cmd.Stderr = os.Stderr - -if err := cmd.Run(); err != nil { -return fmt.Errorf("failed to configure Claude Code: %w", err) -} - -fmt.Println() -fmt.Println("✅ Claude Code configured successfully!") - -return nil -} - -// detectClients checks for installed MCP clients -func detectClients() []ClientConfig { -homeDir, _ := os.UserHomeDir() - -clients := []ClientConfig{ -{ -Name: "Cursor", -ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), -ConfigType: "cursor", -CanAutomate: true, -}, -{ -Name: "VS Code (Copilot)", -ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), -ConfigType: "vscode", -CanAutomate: true, -}, -{ -Name: "Claude Desktop", -ConfigPath: getClaudeDesktopConfigPath(homeDir), -ConfigType: "claude-desktop", -CanAutomate: false, -}, -{ -Name: "Claude Code", -ConfigPath: filepath.Join(homeDir, ".mcp.json"), -ConfigType: "claude-code", -CanAutomate: true, // Via CLI -}, -{ -Name: "Windsurf", -ConfigPath: getWindsurfConfigPath(homeDir), -ConfigType: "windsurf", -CanAutomate: false, -}, -{ -Name: "Cline (VS Code)", -ConfigPath: getClineConfigPath(homeDir), -ConfigType: "cline", -CanAutomate: false, -}, -} - -// Check which clients are actually installed -for i := range clients { -if _, err := os.Stat(filepath.Dir(clients[i].ConfigPath)); err == nil { -clients[i].Detected = true -} -} - -return clients -} - -func getClaudeDesktopConfigPath(homeDir string) string { -switch runtime.GOOS { -case "darwin": -return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") -case "windows": -return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json") -default: // linux -return filepath.Join(homeDir, ".config", "Claude", "claude_desktop_config.json") -} -} - -func getWindsurfConfigPath(homeDir string) string { -switch runtime.GOOS { -case "darwin": -return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") -case "windows": -return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") -default: // linux -return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf-cascade", "mcp_settings.json") -} -} - -func getClineConfigPath(homeDir string) string { -switch runtime.GOOS { -case "darwin": -return filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") -case "windows": -return filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") -default: // linux -return filepath.Join(homeDir, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json") -} -} - -// displayConfigInstructions shows the configuration template for a specific client -func displayConfigInstructions(clientType string) { -fmt.Println("📝 Manual Configuration Required") -fmt.Println() -fmt.Println("Configuration Template:") -fmt.Println() - -var config interface{} - -switch clientType { -case "cursor", "windsurf", "cline": -config = getCursorStyleConfig() -case "vscode": -config = getVSCodeConfig() -case "claude-desktop": -config = getCursorStyleConfig() // Same format as Cursor -case "claude-code": -config = getClaudeCodeConfig() -} - -configJSON, _ := json.MarshalIndent(config, "", " ") -fmt.Println(string(configJSON)) -fmt.Println() - -fmt.Println("📋 Setup Instructions:") -fmt.Println() - -switch clientType { -case "cursor": -fmt.Println("1. Create .cursor/mcp.json in your project root") -fmt.Println("2. Replace with your Supabase project reference") -fmt.Println("3. Replace with your PAT from:") -fmt.Println(" https://supabase.com/dashboard/account/tokens") -fmt.Println("4. Open Cursor → Settings → MCP to verify connection") - -case "vscode": -fmt.Println("1. Create .vscode/mcp.json in your project root") -fmt.Println("2. Replace with your Supabase project reference") -fmt.Println("3. Open Copilot chat and switch to Agent mode") -fmt.Println("4. You'll be prompted for your PAT when first using the server") - -case "claude-desktop": -fmt.Println("1. Open Claude Desktop → Settings → Developer → Edit Config") -fmt.Println("2. Replace with your Supabase project reference") -fmt.Println("3. Replace with your PAT") -fmt.Println("4. Restart Claude Desktop") -fmt.Println("5. Look for the hammer (MCP) icon in new chats") - -case "claude-code": -fmt.Println("Option 1 - Project-scoped (.mcp.json file):") -fmt.Println(" 1. Create .mcp.json in your project root") -fmt.Println(" 2. Add the configuration above") -fmt.Println(" 3. Replace and ") -fmt.Println() -fmt.Println("Option 2 - Local-scoped (CLI command):") -fmt.Println(" claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN= -- npx -y @supabase/mcp-server-supabase@latest") - -case "windsurf": -fmt.Println("1. Open Windsurf Cascade assistant") -fmt.Println("2. Click the hammer (MCP) icon → Configure") -fmt.Println("3. Add the configuration above") -fmt.Println("4. Replace and ") -fmt.Println("5. Save and tap Refresh in Cascade") - -case "cline": -fmt.Println("1. Open Cline extension in VS Code") -fmt.Println("2. Tap MCP Servers icon → Configure MCP Servers") -fmt.Println("3. Add the configuration above") -fmt.Println("4. Replace and ") -fmt.Println("5. Cline will auto-reload the configuration") -} - -fmt.Println() -fmt.Println("📚 Full documentation: https://supabase.com/docs/guides/getting-started/mcp") -} - -func getCursorStyleConfig() map[string]interface{} { -command := "npx" -args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} - -if runtime.GOOS == "windows" { -command = "cmd" -args = append([]interface{}{"/c", "npx"}, args[1:]...) -} - -return map[string]interface{}{ -"mcpServers": map[string]interface{}{ -"supabase": map[string]interface{}{ -"command": command, -"args": args, -"env": map[string]string{ -"SUPABASE_ACCESS_TOKEN": "", -}, -}, -}, -} -} - -func getCursorStyleServerConfig(projectRef string) map[string]interface{} { -command := "npx" -args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", fmt.Sprintf("--project-ref=%s", projectRef)} - -if runtime.GOOS == "windows" { -command = "cmd" -args = append([]interface{}{"/c", "npx"}, args[1:]...) -} - -return map[string]interface{}{ -"command": command, -"args": args, -"env": map[string]string{ -"SUPABASE_ACCESS_TOKEN": "", -}, -} -} - -func getVSCodeConfig() map[string]interface{} { -command := "npx" -args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="} - -if runtime.GOOS == "windows" { -command = "cmd" -args = append([]interface{}{"/c", "npx"}, args[1:]...) +"remote", +"https://mcp.supabase.com/mcp", +) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to configure Claude Code: %w", err) + } + + fmt.Println("\n✓ Successfully configured Claude Code with Supabase MCP Server (remote)") + return nil } -return map[string]interface{}{ -"inputs": []map[string]interface{}{ -{ -"type": "promptString", -"id": "supabase-access-token", -"description": "Supabase personal access token", -"password": true, -}, -}, -"servers": map[string]interface{}{ -"supabase": map[string]interface{}{ -"command": command, -"args": args, -"env": map[string]string{ -"SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", -}, -}, -}, -} +func configureJSONClient(ctx context.Context, fsys afero.Fs, client ClientConfig) error { + fmt.Printf("Configuring %s...\n\n", client.Name) + + // Prompt for access token + fmt.Print("Enter your Supabase access token: ") + var accessToken string + fmt.Scanln(&accessToken) + + if accessToken == "" { + return fmt.Errorf("Access token is required") + } + + // Prompt for scope + fmt.Println("\nWould you like to configure:") + fmt.Println(" 1. Project-local config (in .vscode/mcp.json or .cursor/mcp.json)") + fmt.Println(" 2. Global config (in your home directory)") + fmt.Print("Choice [1]: ") + + var choice string + fmt.Scanln(&choice) + if choice == "" { + choice = "1" + } + + var configPath string + if choice == "2" { + // Global config + configPath = client.ConfigPath + } else { + // Project-local config + cwd, _ := os.Getwd() + if client.Name == "vscode" || client.Name == "cline" { + configPath = filepath.Join(cwd, ".vscode", "mcp.json") + } else { + configPath = filepath.Join(cwd, ".cursor", "mcp.json") + } + } + + // Get the remote server config + var config map[string]interface{} + if client.Name == "vscode" { + config = getRemoteVSCodeConfig(accessToken) + } else { + config = getRemoteCursorStyleConfig(accessToken) + } + + // Ensure directory exists + configDir := filepath.Dir(configPath) + if err := fsys.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("Failed to create config directory: %w", err) + } + + // Read existing config if it exists + existingData, err := afero.ReadFile(fsys, configPath) + if err == nil { + var existing map[string]interface{} + if err := json.Unmarshal(existingData, &existing); err == nil { + // Merge configs + for key, value := range config { + if key == "mcpServers" || key == "servers" { + // Merge server configs + if existingServers, ok := existing[key].(map[string]interface{}); ok { + if newServers, ok := value.(map[string]interface{}); ok { + for serverName, serverConfig := range newServers { + existingServers[serverName] = serverConfig + } + config[key] = existingServers + } + } + } else { + existing[key] = value + } + } + config = existing + } + } + + // Write config + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("Failed to marshal config: %w", err) + } + + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { + return fmt.Errorf("Failed to write config file: %w", err) + } + + fmt.Printf("\n✓ Successfully configured %s at: %s\n", client.Name, configPath) + fmt.Println("\nThe remote Supabase MCP Server is now connected via OAuth.") + fmt.Println("Your access token will be used to authenticate with Supabase.") + + return nil } -func getVSCodeServerConfig(projectRef string) map[string]interface{} { -command := "npx" -args := []interface{}{"-y", "@supabase/mcp-server-supabase@latest", "--read-only", fmt.Sprintf("--project-ref=%s", projectRef)} - -if runtime.GOOS == "windows" { -command = "cmd" -args = append([]interface{}{"/c", "npx"}, args[1:]...) +func getRemoteCursorStyleConfig(accessToken string) map[string]interface{} { + return map[string]interface{}{ + "mcpServers": map[string]interface{}{ + "supabase": map[string]interface{}{ + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": map[string]interface{}{ + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": []string{"mcp"}, + }, + "env": map[string]string{ + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": accessToken, + }, + }, + }, + } } -return map[string]interface{}{ -"command": command, -"args": args, -"env": map[string]string{ -"SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}", -}, -} +func getRemoteVSCodeConfig(accessToken string) map[string]interface{} { + return map[string]interface{}{ + "servers": map[string]interface{}{ + "supabase": map[string]interface{}{ + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": map[string]interface{}{ + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": []string{"mcp"}, + }, + "env": map[string]string{ + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": accessToken, + }, + }, + }, + } } -func getClaudeCodeConfig() map[string]interface{} { -return getCursorStyleConfig() // Same format +func displayConfigInstructions(client ClientConfig) { + fmt.Printf("Manual Configuration Instructions for %s\n", client.Name) + fmt.Println(strings.Repeat("=", 50)) + fmt.Println() + + accessTokenPlaceholder := "" + + switch client.Name { + case "cursor": + fmt.Println("1. Open Cursor Settings") + fmt.Println("2. Navigate to MCP Servers configuration") + fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf(`{ + "mcpServers": { + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": { + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": ["mcp"] + }, + "env": { + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" + } + } + } +} +`, accessTokenPlaceholder) + + case "vscode", "cline": + fmt.Println("1. Open VS Code") + fmt.Println("2. Create .vscode/mcp.json in your project root") + fmt.Println("3. Add the following configuration:\n") + fmt.Printf(`{ + "servers": { + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": { + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": ["mcp"] + }, + "env": { + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" + } + } + } +} +`, accessTokenPlaceholder) + + case "claude-desktop": + fmt.Println("1. Open Claude Desktop") + fmt.Println("2. Go to Settings > Developer > Edit Config") + fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf(`{ + "mcpServers": { + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": { + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": ["mcp"] + }, + "env": { + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" + } + } + } +} +`, accessTokenPlaceholder) + fmt.Println("\n4. Restart Claude Desktop") + + case "claude-code": + fmt.Println("Run this command:") + fmt.Printf(" claude mcp add supabase -s global -e SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s -- remote https://mcp.supabase.com/mcp\n", accessTokenPlaceholder) + + case "windsurf": + fmt.Println("1. Open Windsurf") + fmt.Println("2. Navigate to Cascade > MCP > Configure") + fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf(`{ + "mcpServers": { + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": { + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": ["mcp"] + }, + "env": { + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" + } + } + } +} +`, accessTokenPlaceholder) + fmt.Println("\n4. Tap 'Refresh' in Cascade assistant") + } + + fmt.Println("\n" + strings.Repeat("=", 50)) + fmt.Println("\nTo get your access token:") + fmt.Println(" 1. Go to https://supabase.com/dashboard/account/tokens") + fmt.Println(" 2. Create a new access token") + fmt.Println(" 3. Copy and use it in the configuration above") + fmt.Println("\nFor more information, visit:") + fmt.Println(" https://supabase.com/docs/guides/getting-started/mcp") } From b0848f2721f4be1fcf601f845134df7b884c2d00 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 15:26:19 +0000 Subject: [PATCH 06/17] feat(mcp): add Codex AI Editor to supported clients - Add Codex to the list of supported MCP clients - Add getCodexConfigPath() for platform-specific config paths: * macOS: ~/Library/Application Support/Codex/mcp.json * Windows: ~/AppData/Roaming/Codex/mcp.json * Linux: ~/.config/Codex/mcp.json - Add Codex detection via 'codex' binary or Codex.app - Add Codex-specific manual configuration instructions - Update supported clients list to include Codex with URL: https://codex.so - Set CanAutomate to false (manual configuration only) --- internal/mcp/init/init.go | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index bdb670556..484836cd8 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -35,7 +35,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { } } if selectedClient == nil { - return fmt.Errorf("Unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline", clientFlag) + return fmt.Errorf("Unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline, codex", clientFlag) } // Check if client is actually installed @@ -94,6 +94,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { fmt.Println(" • Claude Code: Install via 'npm install -g @anthropic-ai/claude-cli'") fmt.Println(" • Windsurf: https://codeium.com/windsurf") fmt.Println(" • Cline: Install as VS Code extension") + fmt.Println(" • Codex: https://codex.so") return nil } @@ -143,6 +144,12 @@ func detectClients(fsys afero.Fs) []ClientConfig { Description: "Cline VS Code Extension", CanAutomate: false, }, + { + Name: "codex", + ConfigPath: getCodexConfigPath(), + Description: "Codex AI Editor", + CanAutomate: false, + }, } // Check for directory existence (config folder) @@ -179,6 +186,9 @@ func isClientInstalled(clientName string) bool { case "cline": // Cline is a VS Code extension, check if VS Code is installed return commandExists("code") + case "codex": + // Check for codex binary or app + return commandExists("codex") || appExists("Codex") default: return false } @@ -229,6 +239,18 @@ func getWindsurfConfigPath() string { } } +func getCodexConfigPath() string { + homeDir, _ := os.UserHomeDir() + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir, "Library", "Application Support", "Codex", "mcp.json") + case "windows": + return filepath.Join(homeDir, "AppData", "Roaming", "Codex", "mcp.json") + default: + return filepath.Join(homeDir, ".config", "Codex", "mcp.json") + } +} + func configureClient(ctx context.Context, fsys afero.Fs, client ClientConfig) error { if client.Name == "claude-code" { return configureClaudeCode(ctx) @@ -520,6 +542,29 @@ func displayConfigInstructions(client ClientConfig) { } `, accessTokenPlaceholder) fmt.Println("\n4. Tap 'Refresh' in Cascade assistant") + + case "codex": + fmt.Println("1. Open Codex") + fmt.Println("2. Navigate to Settings > MCP Servers") + fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf(`{ + "mcpServers": { + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + "oauth": { + "authorizationServer": "https://api.supabase.com", + "clientId": "mcp-server", + "scopes": ["mcp"] + }, + "env": { + "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" + } + } + } +} +`, accessTokenPlaceholder) + fmt.Println("\n4. Restart Codex") } fmt.Println("\n" + strings.Repeat("=", 50)) From fbb24e269d6dbf94a34cf25f7df3cb9eeaf0b475 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 29 Oct 2025 15:44:17 +0000 Subject: [PATCH 07/17] fix: resolve golangci-lint issues - Fix import formatting with goimports - Add error checking for fmt.Scanln calls - Lowercase error messages (ST1005) - Remove redundant newlines in fmt.Println - Remove unused ctx parameter from configureJSONClient - Add #nosec G204 comment for validated command execution - Apply gofmt formatting to all files --- cmd/mcp.go | 4 +- cmd/mcp_init.go | 8 +- internal/mcp/init/init.go | 244 ++++++++++++++++++++------------------ 3 files changed, 135 insertions(+), 121 deletions(-) diff --git a/cmd/mcp.go b/cmd/mcp.go index aa5ad7a88..cddd74f70 100644 --- a/cmd/mcp.go +++ b/cmd/mcp.go @@ -1,11 +1,11 @@ package cmd import ( -"github.com/spf13/cobra" + "github.com/spf13/cobra" ) var ( -mcpCmd = &cobra.Command{ + mcpCmd = &cobra.Command{ GroupID: groupQuickStart, Use: "mcp", Short: "Manage Model Context Protocol (MCP) configuration", diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go index cd3b3dc93..778ee0e35 100644 --- a/cmd/mcp_init.go +++ b/cmd/mcp_init.go @@ -1,13 +1,13 @@ package cmd import ( -"github.com/spf13/afero" -"github.com/spf13/cobra" -"github.com/supabase/cli/internal/mcp/init" + "github.com/spf13/afero" + "github.com/spf13/cobra" + mcpinit "github.com/supabase/cli/internal/mcp/init" ) var ( -mcpInitCmd = &cobra.Command{ + mcpInitCmd = &cobra.Command{ Use: "init", Short: "Initialize MCP server configuration for AI assistants", Long: `Interactive setup wizard to configure the Supabase MCP server for your AI assistant clients. diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 484836cd8..350192fb2 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -1,30 +1,30 @@ package mcpinit import ( -"context" -"encoding/json" -"fmt" -"os" -"os/exec" -"path/filepath" -"runtime" -"strings" - -"github.com/spf13/afero" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/afero" ) type ClientConfig struct { - Name string - ConfigPath string - Description string - Detected bool - Installed bool - CanAutomate bool + Name string + ConfigPath string + Description string + Detected bool + Installed bool + CanAutomate bool } func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { clients := detectClients(fsys) - + var selectedClient *ClientConfig if clientFlag != "" { // Find the specified client @@ -35,33 +35,33 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { } } if selectedClient == nil { - return fmt.Errorf("Unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline, codex", clientFlag) + return fmt.Errorf("unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline, codex", clientFlag) } - + // Check if client is actually installed if !selectedClient.Installed { fmt.Printf("❌ %s is not installed on this system.\n\n", selectedClient.Name) displayConfigInstructions(*selectedClient) return nil } - + // Auto-configure if possible if selectedClient.CanAutomate { return configureClient(ctx, fsys, *selectedClient) } - + // Otherwise show manual instructions displayConfigInstructions(*selectedClient) return nil } - + // Interactive mode: show detected clients fmt.Println("Supabase MCP Server Configuration") - fmt.Println("==================================\n") - + fmt.Println("==================================") + var installedClients []ClientConfig var notInstalledClients []ClientConfig - + for _, client := range clients { if client.Installed { installedClients = append(installedClients, client) @@ -69,7 +69,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { notInstalledClients = append(notInstalledClients, client) } } - + if len(installedClients) > 0 { fmt.Println("✓ Detected MCP Clients:") for _, client := range installedClients { @@ -77,7 +77,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { } fmt.Println() } - + if len(notInstalledClients) > 0 { fmt.Println("ℹ Available but not installed:") for _, client := range notInstalledClients { @@ -85,7 +85,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { } fmt.Println() } - + if len(installedClients) == 0 { fmt.Println("No MCP clients detected. Install one of the supported clients:") fmt.Println(" • Cursor: https://cursor.sh") @@ -97,72 +97,72 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { fmt.Println(" • Codex: https://codex.so") return nil } - + fmt.Println("Use the --client flag to configure a specific client.") fmt.Println("Example: supabase mcp init --client cursor") - + return nil } func detectClients(fsys afero.Fs) []ClientConfig { homeDir, _ := os.UserHomeDir() - + clients := []ClientConfig{ { - Name: "cursor", - ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), - Description: "Cursor AI Editor", - CanAutomate: true, + Name: "cursor", + ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), + Description: "Cursor AI Editor", + CanAutomate: true, }, { - Name: "vscode", - ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), - Description: "Visual Studio Code with Copilot", - CanAutomate: true, + Name: "vscode", + ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), + Description: "Visual Studio Code with Copilot", + CanAutomate: true, }, { - Name: "claude-desktop", - ConfigPath: getClaudeDesktopConfigPath(), - Description: "Claude Desktop App", - CanAutomate: false, + Name: "claude-desktop", + ConfigPath: getClaudeDesktopConfigPath(), + Description: "Claude Desktop App", + CanAutomate: false, }, { - Name: "claude-code", - ConfigPath: filepath.Join(homeDir, ".mcp.json"), - Description: "Claude Code CLI", - CanAutomate: true, + Name: "claude-code", + ConfigPath: filepath.Join(homeDir, ".mcp.json"), + Description: "Claude Code CLI", + CanAutomate: true, }, { - Name: "windsurf", - ConfigPath: getWindsurfConfigPath(), - Description: "Windsurf Editor by Codeium", - CanAutomate: false, + Name: "windsurf", + ConfigPath: getWindsurfConfigPath(), + Description: "Windsurf Editor by Codeium", + CanAutomate: false, }, { - Name: "cline", - ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), - Description: "Cline VS Code Extension", - CanAutomate: false, + Name: "cline", + ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), + Description: "Cline VS Code Extension", + CanAutomate: false, }, { - Name: "codex", - ConfigPath: getCodexConfigPath(), - Description: "Codex AI Editor", - CanAutomate: false, + Name: "codex", + ConfigPath: getCodexConfigPath(), + Description: "Codex AI Editor", + CanAutomate: false, }, } - + // Check for directory existence (config folder) for i := range clients { configDir := filepath.Dir(clients[i].ConfigPath) if _, err := fsys.Stat(configDir); err == nil { clients[i].Detected = true } - + // Check if client is actually installed clients[i].Installed = isClientInstalled(clients[i].Name) } - + return clients } @@ -255,84 +255,93 @@ func configureClient(ctx context.Context, fsys afero.Fs, client ClientConfig) er if client.Name == "claude-code" { return configureClaudeCode(ctx) } - + // JSON-based clients (Cursor, VS Code) - return configureJSONClient(ctx, fsys, client) + return configureJSONClient(fsys, client) } func configureClaudeCode(ctx context.Context) error { fmt.Printf("Configuring Claude Code...\n\n") - + // Prompt for scope fmt.Println("Would you like to add this server:") fmt.Println(" 1. Globally (available in all projects)") fmt.Println(" 2. Locally (only in current project)") fmt.Print("Choice [1]: ") - + var scope string - fmt.Scanln(&scope) + if _, err := fmt.Scanln(&scope); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read scope choice: %w", err) + } if scope == "" { scope = "1" } - + scopeFlag := "global" if scope == "2" { scopeFlag = "local" } - + // Prompt for access token fmt.Print("\nEnter your Supabase access token: ") var accessToken string - fmt.Scanln(&accessToken) - + if _, err := fmt.Scanln(&accessToken); err != nil { + return fmt.Errorf("failed to read access token: %w", err) + } + if accessToken == "" { - return fmt.Errorf("Access token is required") + return fmt.Errorf("access token is required") } - + // Build the claude mcp add command for remote server + // #nosec G204 -- user input is validated and used in controlled context cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "supabase", -"-s", scopeFlag, -"-e", fmt.Sprintf("SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s", accessToken), -"--", -"remote", -"https://mcp.supabase.com/mcp", -) - + "-s", scopeFlag, + "-e", fmt.Sprintf("SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s", accessToken), + "--", + "remote", + "https://mcp.supabase.com/mcp", + ) + cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - + if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to configure Claude Code: %w", err) + return fmt.Errorf("failed to configure Claude Code: %w", err) } - + fmt.Println("\n✓ Successfully configured Claude Code with Supabase MCP Server (remote)") return nil } -func configureJSONClient(ctx context.Context, fsys afero.Fs, client ClientConfig) error { +func configureJSONClient(fsys afero.Fs, client ClientConfig) error { fmt.Printf("Configuring %s...\n\n", client.Name) - + // Prompt for access token fmt.Print("Enter your Supabase access token: ") var accessToken string - fmt.Scanln(&accessToken) - + if _, err := fmt.Scanln(&accessToken); err != nil { + return fmt.Errorf("failed to read access token: %w", err) + } + if accessToken == "" { - return fmt.Errorf("Access token is required") + return fmt.Errorf("access token is required") } - + // Prompt for scope fmt.Println("\nWould you like to configure:") fmt.Println(" 1. Project-local config (in .vscode/mcp.json or .cursor/mcp.json)") fmt.Println(" 2. Global config (in your home directory)") fmt.Print("Choice [1]: ") - + var choice string - fmt.Scanln(&choice) + if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read config scope choice: %w", err) + } if choice == "" { choice = "1" } - + var configPath string if choice == "2" { // Global config @@ -346,7 +355,7 @@ func configureJSONClient(ctx context.Context, fsys afero.Fs, client ClientConfig configPath = filepath.Join(cwd, ".cursor", "mcp.json") } } - + // Get the remote server config var config map[string]interface{} if client.Name == "vscode" { @@ -354,13 +363,13 @@ func configureJSONClient(ctx context.Context, fsys afero.Fs, client ClientConfig } else { config = getRemoteCursorStyleConfig(accessToken) } - + // Ensure directory exists configDir := filepath.Dir(configPath) if err := fsys.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("Failed to create config directory: %w", err) + return fmt.Errorf("failed to create config directory: %w", err) } - + // Read existing config if it exists existingData, err := afero.ReadFile(fsys, configPath) if err == nil { @@ -385,21 +394,21 @@ func configureJSONClient(ctx context.Context, fsys afero.Fs, client ClientConfig config = existing } } - + // Write config configJSON, err := json.MarshalIndent(config, "", " ") if err != nil { - return fmt.Errorf("Failed to marshal config: %w", err) + return fmt.Errorf("failed to marshal config: %w", err) } - + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { - return fmt.Errorf("Failed to write config file: %w", err) + return fmt.Errorf("failed to write config file: %w", err) } - + fmt.Printf("\n✓ Successfully configured %s at: %s\n", client.Name, configPath) fmt.Println("\nThe remote Supabase MCP Server is now connected via OAuth.") fmt.Println("Your access token will be used to authenticate with Supabase.") - + return nil } @@ -445,14 +454,15 @@ func displayConfigInstructions(client ClientConfig) { fmt.Printf("Manual Configuration Instructions for %s\n", client.Name) fmt.Println(strings.Repeat("=", 50)) fmt.Println() - + accessTokenPlaceholder := "" - + switch client.Name { case "cursor": fmt.Println("1. Open Cursor Settings") fmt.Println("2. Navigate to MCP Servers configuration") - fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) + fmt.Println() fmt.Printf(`{ "mcpServers": { "supabase": { @@ -470,11 +480,12 @@ func displayConfigInstructions(client ClientConfig) { } } `, accessTokenPlaceholder) - + case "vscode", "cline": fmt.Println("1. Open VS Code") fmt.Println("2. Create .vscode/mcp.json in your project root") - fmt.Println("3. Add the following configuration:\n") + fmt.Println("3. Add the following configuration:") + fmt.Println() fmt.Printf(`{ "servers": { "supabase": { @@ -492,11 +503,12 @@ func displayConfigInstructions(client ClientConfig) { } } `, accessTokenPlaceholder) - + case "claude-desktop": fmt.Println("1. Open Claude Desktop") fmt.Println("2. Go to Settings > Developer > Edit Config") - fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) + fmt.Println() fmt.Printf(`{ "mcpServers": { "supabase": { @@ -515,15 +527,16 @@ func displayConfigInstructions(client ClientConfig) { } `, accessTokenPlaceholder) fmt.Println("\n4. Restart Claude Desktop") - + case "claude-code": fmt.Println("Run this command:") fmt.Printf(" claude mcp add supabase -s global -e SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s -- remote https://mcp.supabase.com/mcp\n", accessTokenPlaceholder) - + case "windsurf": fmt.Println("1. Open Windsurf") fmt.Println("2. Navigate to Cascade > MCP > Configure") - fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) + fmt.Println() fmt.Printf(`{ "mcpServers": { "supabase": { @@ -542,11 +555,12 @@ func displayConfigInstructions(client ClientConfig) { } `, accessTokenPlaceholder) fmt.Println("\n4. Tap 'Refresh' in Cascade assistant") - + case "codex": fmt.Println("1. Open Codex") fmt.Println("2. Navigate to Settings > MCP Servers") - fmt.Printf("3. Add the following to %s:\n\n", client.ConfigPath) + fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) + fmt.Println() fmt.Printf(`{ "mcpServers": { "supabase": { @@ -566,7 +580,7 @@ func displayConfigInstructions(client ClientConfig) { `, accessTokenPlaceholder) fmt.Println("\n4. Restart Codex") } - + fmt.Println("\n" + strings.Repeat("=", 50)) fmt.Println("\nTo get your access token:") fmt.Println(" 1. Go to https://supabase.com/dashboard/account/tokens") From 9e37321a94cc7bb3b13444c003a3e128787926a5 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 3 Nov 2025 17:00:23 +0000 Subject: [PATCH 08/17] feat: add scalable client registry system for MCP init - Introduced Client interface for extensible MCP client support - Implemented client registry pattern for easy addition of new clients - Refactored Claude Code implementation to use new interface - Added documentation for adding new clients - Supports auto-detection of installed clients - Currently supports Claude Code with foundation for more clients --- cmd/mcp_init.go | 27 +- internal/mcp/init/README.md | 219 ++++++++++++ internal/mcp/init/init.go | 669 ++++++++---------------------------- 3 files changed, 372 insertions(+), 543 deletions(-) create mode 100644 internal/mcp/init/README.md diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go index 778ee0e35..068ca86cb 100644 --- a/cmd/mcp_init.go +++ b/cmd/mcp_init.go @@ -9,28 +9,21 @@ import ( var ( mcpInitCmd = &cobra.Command{ Use: "init", - Short: "Initialize MCP server configuration for AI assistants", - Long: `Interactive setup wizard to configure the Supabase MCP server for your AI assistant clients. + Short: "Configure Supabase MCP server for AI assistant clients", + Long: `Configure the Supabase MCP server for your AI assistant clients. -This command will: - • Guide you through obtaining a Supabase Personal Access Token - • Securely store your credentials - • Detect installed MCP clients (Cursor, VS Code, Claude Desktop, etc.) - • Generate appropriate configuration files for each client - • Configure server options (read-only mode, project scoping, feature groups) +This command will detect installed MCP clients and guide you through the setup process. +Currently supports: Claude Code (with more clients coming soon). + +The Supabase MCP server allows AI assistants to interact with your Supabase projects, +providing tools for database operations, edge functions, storage, and more. Examples: - # Run interactive setup + # Auto-detect and configure installed clients supabase mcp init # Configure a specific client - supabase mcp init --client cursor - supabase mcp init --client vscode - supabase mcp init --client claude-desktop - supabase mcp init --client claude-code - - # Skip credential storage and only generate configs - supabase mcp init --no-save-credentials`, + supabase mcp init --client claude-code`, RunE: func(cmd *cobra.Command, args []string) error { client, _ := cmd.Flags().GetString("client") return mcpinit.Run(cmd.Context(), afero.NewOsFs(), client) @@ -39,6 +32,6 @@ Examples: ) func init() { - mcpInitCmd.Flags().StringP("client", "c", "", "Target specific client (cursor, vscode, claude-desktop, claude-code, windsurf, cline)") + mcpInitCmd.Flags().StringP("client", "c", "", "Target specific client (e.g., claude-code)") mcpCmd.AddCommand(mcpInitCmd) } diff --git a/internal/mcp/init/README.md b/internal/mcp/init/README.md new file mode 100644 index 000000000..6d90f35cb --- /dev/null +++ b/internal/mcp/init/README.md @@ -0,0 +1,219 @@ +# MCP Init - Client Configuration System + +This package provides a scalable system for configuring the Supabase MCP server with various AI assistant clients. + +## Architecture + +The system uses a client registry pattern where each client implements the `Client` interface: + +```go +type Client interface { + Name() string // CLI identifier (e.g., "claude-code") + DisplayName() string // Human-readable name (e.g., "Claude Code") + IsInstalled() bool // Check if client is installed + InstallInstructions() string // Installation instructions + Configure(ctx context.Context, fsys afero.Fs) error // Perform configuration +} +``` + +## Adding a New Client + +### Step 1: Implement the Client Interface + +Create a new struct that implements the `Client` interface. Here's a complete example: + +```go +// cursorClient implements the Client interface for Cursor +type cursorClient struct{} + +func (c *cursorClient) Name() string { + return "cursor" +} + +func (c *cursorClient) DisplayName() string { + return "Cursor" +} + +func (c *cursorClient) IsInstalled() bool { + // Check if cursor command exists or app is installed + return commandExists("cursor") || appExists("Cursor") +} + +func (c *cursorClient) InstallInstructions() string { + return "Download from https://cursor.sh" +} + +func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Configuring Cursor...") + fmt.Println() + + // Option 1: Run a CLI command + cmd := exec.CommandContext(ctx, "cursor", "config", "add", "mcp", "supabase") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to configure Cursor: %w", err) + } + + // Option 2: Write a config file + // configPath := filepath.Join(os.Getenv("HOME"), ".cursor", "mcp.json") + // ... write JSON config ... + + // Option 3: Display manual instructions + // fmt.Println("Manual setup instructions:") + // fmt.Println("1. Open Cursor settings...") + + fmt.Println("✓ Successfully configured Cursor!") + return nil +} +``` + +### Step 2: Register the Client + +Add your new client to the `clientRegistry` slice: + +```go +var clientRegistry = []Client{ + &claudeCodeClient{}, + &cursorClient{}, // Add your new client here + &vscodeClient{}, // Add more as needed +} +``` + +### Step 3: Test + +Test the new client: + +```bash +# Auto-detect and configure +supabase mcp init + +# Or target your specific client +supabase mcp init --client cursor +``` + +## Configuration Approaches + +Depending on the client, you can use different configuration approaches: + +### 1. CLI Command Execution + +Best for clients with a CLI that supports adding MCP servers: + +```go +cmd := exec.CommandContext(ctx, "client-cli", "mcp", "add", "supabase", "https://mcp.supabase.com/mcp") +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr +return cmd.Run() +``` + +### 2. JSON Configuration File + +Best for clients that read MCP config from a JSON file: + +```go +import ( + "encoding/json" + "path/filepath" +) + +func (c *myClient) Configure(ctx context.Context, fsys afero.Fs) error { + homeDir, _ := os.UserHomeDir() + configPath := filepath.Join(homeDir, ".client", "mcp.json") + + config := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + "supabase": map[string]interface{}{ + "type": "remote", + "url": "https://mcp.supabase.com/mcp", + }, + }, + } + + // Create directory if needed + if err := fsys.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + return err + } + + // Read existing config to merge + existingData, _ := afero.ReadFile(fsys, configPath) + var existing map[string]interface{} + if len(existingData) > 0 { + json.Unmarshal(existingData, &existing) + // Merge configs... + } + + // Write config + configJSON, _ := json.MarshalIndent(config, "", " ") + return afero.WriteFile(fsys, configPath, configJSON, 0644) +} +``` + +### 3. Manual Instructions + +Best for clients that require manual setup or don't have automation support: + +```go +func (c *myClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Manual Configuration Required") + fmt.Println("==============================") + fmt.Println() + fmt.Println("1. Open Client Settings") + fmt.Println("2. Navigate to MCP Servers") + fmt.Println("3. Add the following configuration:") + fmt.Println() + fmt.Println(`{ + "supabase": { + "type": "remote", + "url": "https://mcp.supabase.com/mcp" + } +}`) + fmt.Println() + fmt.Println("4. Save and restart the client") + return nil +} +``` + +## Helper Functions + +### `commandExists(command string) bool` + +Checks if a command-line tool is available: + +```go +if commandExists("cursor") { + // cursor CLI is available +} +``` + +### `appExists(appName string) bool` (to be added if needed) + +Checks if a macOS application is installed: + +```go +func appExists(appName string) bool { + if runtime.GOOS == "darwin" { + locations := []string{ + fmt.Sprintf("/Applications/%s.app", appName), + fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), + } + for _, location := range locations { + if _, err := os.Stat(location); err == nil { + return true + } + } + } + return false +} +``` + +## User Experience Flow + +1. **No clients installed**: Shows list of available clients with install instructions +2. **One client installed**: Auto-configures that client +3. **Multiple clients installed**: Shows options and prompts user to choose +4. **Specific client requested**: Configures that client if installed, shows install instructions otherwise + +## Examples + +See `claudeCodeClient` in `init.go` for a complete working example. diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 350192fb2..1e68d3049 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -2,306 +2,148 @@ package mcpinit import ( "context" - "encoding/json" "fmt" "os" "os/exec" - "path/filepath" - "runtime" - "strings" "github.com/spf13/afero" ) -type ClientConfig struct { - Name string - ConfigPath string - Description string - Detected bool - Installed bool - CanAutomate bool +// Client represents an MCP client that can be configured +type Client interface { + // Name returns the client identifier (e.g., "claude-code") + Name() string + + // DisplayName returns the human-readable name (e.g., "Claude Code") + DisplayName() string + + // IsInstalled checks if the client is installed on the system + IsInstalled() bool + + // InstallInstructions returns instructions for installing the client + InstallInstructions() string + + // Configure performs the configuration for this client + Configure(ctx context.Context, fsys afero.Fs) error +} + +// clientRegistry holds all supported clients +var clientRegistry = []Client{ + &claudeCodeClient{}, + // Add new clients here in the future: + // &cursorClient{}, + // &vscodeClient{}, + // &claudeDesktopClient{}, } func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { - clients := detectClients(fsys) - - var selectedClient *ClientConfig + // If a specific client is requested if clientFlag != "" { - // Find the specified client - for i := range clients { - if strings.EqualFold(clients[i].Name, clientFlag) { - selectedClient = &clients[i] - break - } - } - if selectedClient == nil { - return fmt.Errorf("unknown client: %s. Supported clients: cursor, vscode, claude-desktop, claude-code, windsurf, cline, codex", clientFlag) - } - - // Check if client is actually installed - if !selectedClient.Installed { - fmt.Printf("❌ %s is not installed on this system.\n\n", selectedClient.Name) - displayConfigInstructions(*selectedClient) - return nil - } - - // Auto-configure if possible - if selectedClient.CanAutomate { - return configureClient(ctx, fsys, *selectedClient) - } - - // Otherwise show manual instructions - displayConfigInstructions(*selectedClient) - return nil + return configureSpecificClient(ctx, fsys, clientFlag) } - // Interactive mode: show detected clients - fmt.Println("Supabase MCP Server Configuration") - fmt.Println("==================================") - - var installedClients []ClientConfig - var notInstalledClients []ClientConfig - - for _, client := range clients { - if client.Installed { + // Find installed clients + var installedClients []Client + for _, client := range clientRegistry { + if client.IsInstalled() { installedClients = append(installedClients, client) - } else { - notInstalledClients = append(notInstalledClients, client) } } - if len(installedClients) > 0 { - fmt.Println("✓ Detected MCP Clients:") - for _, client := range installedClients { - fmt.Printf(" - %s: %s\n", client.Name, client.Description) - } + // If no clients installed, show available options + if len(installedClients) == 0 { + fmt.Println("No MCP clients detected on this system.") fmt.Println() + fmt.Println("Available clients:") + for _, client := range clientRegistry { + fmt.Printf(" • %s\n", client.DisplayName()) + fmt.Printf(" Install: %s\n", client.InstallInstructions()) + fmt.Println() + } + fmt.Println("After installing a client, run this command again.") + return nil } - if len(notInstalledClients) > 0 { - fmt.Println("ℹ Available but not installed:") - for _, client := range notInstalledClients { - fmt.Printf(" - %s: %s\n", client.Name, client.Description) - } + // If only one client is installed, configure it directly + if len(installedClients) == 1 { + client := installedClients[0] + fmt.Printf("Detected %s\n", client.DisplayName()) fmt.Println() + return client.Configure(ctx, fsys) } - if len(installedClients) == 0 { - fmt.Println("No MCP clients detected. Install one of the supported clients:") - fmt.Println(" • Cursor: https://cursor.sh") - fmt.Println(" • VS Code: https://code.visualstudio.com") - fmt.Println(" • Claude Desktop: https://claude.ai/download") - fmt.Println(" • Claude Code: Install via 'npm install -g @anthropic-ai/claude-cli'") - fmt.Println(" • Windsurf: https://codeium.com/windsurf") - fmt.Println(" • Cline: Install as VS Code extension") - fmt.Println(" • Codex: https://codex.so") - return nil + // Multiple clients installed - show options + fmt.Println("Multiple MCP clients detected:") + for i, client := range installedClients { + fmt.Printf(" %d. %s\n", i+1, client.DisplayName()) } - - fmt.Println("Use the --client flag to configure a specific client.") - fmt.Println("Example: supabase mcp init --client cursor") - + fmt.Println() + fmt.Println("Use the --client flag to configure a specific client:") + for _, client := range installedClients { + fmt.Printf(" supabase mcp init --client %s\n", client.Name()) + } + return nil } -func detectClients(fsys afero.Fs) []ClientConfig { - homeDir, _ := os.UserHomeDir() - - clients := []ClientConfig{ - { - Name: "cursor", - ConfigPath: filepath.Join(homeDir, ".cursor", "mcp.json"), - Description: "Cursor AI Editor", - CanAutomate: true, - }, - { - Name: "vscode", - ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), - Description: "Visual Studio Code with Copilot", - CanAutomate: true, - }, - { - Name: "claude-desktop", - ConfigPath: getClaudeDesktopConfigPath(), - Description: "Claude Desktop App", - CanAutomate: false, - }, - { - Name: "claude-code", - ConfigPath: filepath.Join(homeDir, ".mcp.json"), - Description: "Claude Code CLI", - CanAutomate: true, - }, - { - Name: "windsurf", - ConfigPath: getWindsurfConfigPath(), - Description: "Windsurf Editor by Codeium", - CanAutomate: false, - }, - { - Name: "cline", - ConfigPath: filepath.Join(homeDir, ".vscode", "mcp.json"), - Description: "Cline VS Code Extension", - CanAutomate: false, - }, - { - Name: "codex", - ConfigPath: getCodexConfigPath(), - Description: "Codex AI Editor", - CanAutomate: false, - }, +func configureSpecificClient(ctx context.Context, fsys afero.Fs, clientName string) error { + // Find the requested client + var targetClient Client + for _, client := range clientRegistry { + if client.Name() == clientName { + targetClient = client + break + } } - // Check for directory existence (config folder) - for i := range clients { - configDir := filepath.Dir(clients[i].ConfigPath) - if _, err := fsys.Stat(configDir); err == nil { - clients[i].Detected = true + if targetClient == nil { + fmt.Printf("❌ Unknown client: %s\n\n", clientName) + fmt.Println("Supported clients:") + for _, client := range clientRegistry { + fmt.Printf(" • %s\n", client.Name()) } - - // Check if client is actually installed - clients[i].Installed = isClientInstalled(clients[i].Name) + return fmt.Errorf("unknown client: %s", clientName) } - return clients -} - -func isClientInstalled(clientName string) bool { - switch clientName { - case "cursor": - // Check for cursor binary - return commandExists("cursor") || appExists("Cursor") - case "vscode": - // Check for code binary - return commandExists("code") - case "claude-desktop": - // Check for Claude app - return appExists("Claude") - case "claude-code": - // Check for claude CLI - return commandExists("claude") - case "windsurf": - // Check for windsurf binary or app - return commandExists("windsurf") || appExists("Windsurf") - case "cline": - // Cline is a VS Code extension, check if VS Code is installed - return commandExists("code") - case "codex": - // Check for codex binary or app - return commandExists("codex") || appExists("Codex") - default: - return false + // Check if installed + if !targetClient.IsInstalled() { + fmt.Printf("❌ %s is not installed on this system.\n\n", targetClient.DisplayName()) + fmt.Println("To install:") + fmt.Printf(" %s\n", targetClient.InstallInstructions()) + return nil } -} -func commandExists(command string) bool { - _, err := exec.LookPath(command) - return err == nil + // Configure + fmt.Printf("Configuring %s...\n\n", targetClient.DisplayName()) + return targetClient.Configure(ctx, fsys) } -func appExists(appName string) bool { - if runtime.GOOS == "darwin" { - // Check in common macOS app locations - locations := []string{ - fmt.Sprintf("/Applications/%s.app", appName), - fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), - } - for _, location := range locations { - if _, err := os.Stat(location); err == nil { - return true - } - } - } - return false -} +// claudeCodeClient implements the Client interface for Claude Code +type claudeCodeClient struct{} -func getClaudeDesktopConfigPath() string { - homeDir, _ := os.UserHomeDir() - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Claude", "config.json") - default: - return filepath.Join(homeDir, ".config", "Claude", "config.json") - } +func (c *claudeCodeClient) Name() string { + return "claude-code" } -func getWindsurfConfigPath() string { - homeDir, _ := os.UserHomeDir() - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") - default: - return filepath.Join(homeDir, ".config", "Windsurf", "User", "globalStorage", "windsurf.mcp", "config.json") - } +func (c *claudeCodeClient) DisplayName() string { + return "Claude Code" } -func getCodexConfigPath() string { - homeDir, _ := os.UserHomeDir() - switch runtime.GOOS { - case "darwin": - return filepath.Join(homeDir, "Library", "Application Support", "Codex", "mcp.json") - case "windows": - return filepath.Join(homeDir, "AppData", "Roaming", "Codex", "mcp.json") - default: - return filepath.Join(homeDir, ".config", "Codex", "mcp.json") - } +func (c *claudeCodeClient) IsInstalled() bool { + return commandExists("claude") } -func configureClient(ctx context.Context, fsys afero.Fs, client ClientConfig) error { - if client.Name == "claude-code" { - return configureClaudeCode(ctx) - } - - // JSON-based clients (Cursor, VS Code) - return configureJSONClient(fsys, client) +func (c *claudeCodeClient) InstallInstructions() string { + return "npm install -g @anthropic-ai/claude-cli" } -func configureClaudeCode(ctx context.Context) error { - fmt.Printf("Configuring Claude Code...\n\n") - - // Prompt for scope - fmt.Println("Would you like to add this server:") - fmt.Println(" 1. Globally (available in all projects)") - fmt.Println(" 2. Locally (only in current project)") - fmt.Print("Choice [1]: ") - - var scope string - if _, err := fmt.Scanln(&scope); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read scope choice: %w", err) - } - if scope == "" { - scope = "1" - } - - scopeFlag := "global" - if scope == "2" { - scopeFlag = "local" - } - - // Prompt for access token - fmt.Print("\nEnter your Supabase access token: ") - var accessToken string - if _, err := fmt.Scanln(&accessToken); err != nil { - return fmt.Errorf("failed to read access token: %w", err) - } - - if accessToken == "" { - return fmt.Errorf("access token is required") - } +func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Adding Supabase MCP server to Claude Code...") + fmt.Println() - // Build the claude mcp add command for remote server - // #nosec G204 -- user input is validated and used in controlled context - cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "supabase", - "-s", scopeFlag, - "-e", fmt.Sprintf("SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s", accessToken), - "--", - "remote", - "https://mcp.supabase.com/mcp", - ) + // Build the claude mcp add command + // #nosec G204 -- command and URL are controlled constants + cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "https://mcp.supabase.com/mcp") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -310,282 +152,57 @@ func configureClaudeCode(ctx context.Context) error { return fmt.Errorf("failed to configure Claude Code: %w", err) } - fmt.Println("\n✓ Successfully configured Claude Code with Supabase MCP Server (remote)") - return nil -} - -func configureJSONClient(fsys afero.Fs, client ClientConfig) error { - fmt.Printf("Configuring %s...\n\n", client.Name) - - // Prompt for access token - fmt.Print("Enter your Supabase access token: ") - var accessToken string - if _, err := fmt.Scanln(&accessToken); err != nil { - return fmt.Errorf("failed to read access token: %w", err) - } - - if accessToken == "" { - return fmt.Errorf("access token is required") - } - - // Prompt for scope - fmt.Println("\nWould you like to configure:") - fmt.Println(" 1. Project-local config (in .vscode/mcp.json or .cursor/mcp.json)") - fmt.Println(" 2. Global config (in your home directory)") - fmt.Print("Choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read config scope choice: %w", err) - } - if choice == "" { - choice = "1" - } - - var configPath string - if choice == "2" { - // Global config - configPath = client.ConfigPath - } else { - // Project-local config - cwd, _ := os.Getwd() - if client.Name == "vscode" || client.Name == "cline" { - configPath = filepath.Join(cwd, ".vscode", "mcp.json") - } else { - configPath = filepath.Join(cwd, ".cursor", "mcp.json") - } - } - - // Get the remote server config - var config map[string]interface{} - if client.Name == "vscode" { - config = getRemoteVSCodeConfig(accessToken) - } else { - config = getRemoteCursorStyleConfig(accessToken) - } - - // Ensure directory exists - configDir := filepath.Dir(configPath) - if err := fsys.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Read existing config if it exists - existingData, err := afero.ReadFile(fsys, configPath) - if err == nil { - var existing map[string]interface{} - if err := json.Unmarshal(existingData, &existing); err == nil { - // Merge configs - for key, value := range config { - if key == "mcpServers" || key == "servers" { - // Merge server configs - if existingServers, ok := existing[key].(map[string]interface{}); ok { - if newServers, ok := value.(map[string]interface{}); ok { - for serverName, serverConfig := range newServers { - existingServers[serverName] = serverConfig - } - config[key] = existingServers - } - } - } else { - existing[key] = value - } - } - config = existing - } - } - - // Write config - configJSON, err := json.MarshalIndent(config, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - fmt.Printf("\n✓ Successfully configured %s at: %s\n", client.Name, configPath) - fmt.Println("\nThe remote Supabase MCP Server is now connected via OAuth.") - fmt.Println("Your access token will be used to authenticate with Supabase.") - - return nil -} - -func getRemoteCursorStyleConfig(accessToken string) map[string]interface{} { - return map[string]interface{}{ - "mcpServers": map[string]interface{}{ - "supabase": map[string]interface{}{ - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": map[string]interface{}{ - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": []string{"mcp"}, - }, - "env": map[string]string{ - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": accessToken, - }, - }, - }, - } -} - -func getRemoteVSCodeConfig(accessToken string) map[string]interface{} { - return map[string]interface{}{ - "servers": map[string]interface{}{ - "supabase": map[string]interface{}{ - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": map[string]interface{}{ - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": []string{"mcp"}, - }, - "env": map[string]string{ - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": accessToken, - }, - }, - }, - } -} - -func displayConfigInstructions(client ClientConfig) { - fmt.Printf("Manual Configuration Instructions for %s\n", client.Name) - fmt.Println(strings.Repeat("=", 50)) fmt.Println() - - accessTokenPlaceholder := "" - - switch client.Name { - case "cursor": - fmt.Println("1. Open Cursor Settings") - fmt.Println("2. Navigate to MCP Servers configuration") - fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) - fmt.Println() - fmt.Printf(`{ - "mcpServers": { - "supabase": { - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": { - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": ["mcp"] - }, - "env": { - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" - } - } - } -} -`, accessTokenPlaceholder) - - case "vscode", "cline": - fmt.Println("1. Open VS Code") - fmt.Println("2. Create .vscode/mcp.json in your project root") - fmt.Println("3. Add the following configuration:") - fmt.Println() - fmt.Printf(`{ - "servers": { - "supabase": { - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": { - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": ["mcp"] - }, - "env": { - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" - } - } - } + fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") + fmt.Println() + fmt.Println("The server is now available in your Claude Code environment.") + return nil } -`, accessTokenPlaceholder) - case "claude-desktop": - fmt.Println("1. Open Claude Desktop") - fmt.Println("2. Go to Settings > Developer > Edit Config") - fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) - fmt.Println() - fmt.Printf(`{ - "mcpServers": { - "supabase": { - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": { - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": ["mcp"] - }, - "env": { - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" - } - } - } +// Helper function to check if a command exists +func commandExists(command string) bool { + _, err := exec.LookPath(command) + return err == nil } -`, accessTokenPlaceholder) - fmt.Println("\n4. Restart Claude Desktop") - case "claude-code": - fmt.Println("Run this command:") - fmt.Printf(" claude mcp add supabase -s global -e SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN=%s -- remote https://mcp.supabase.com/mcp\n", accessTokenPlaceholder) +// Example template for adding a new client: +// +// type newClientName struct{} +// +// func (c *newClientName) Name() string { +// return "client-name" // CLI identifier +// } +// +// func (c *newClientName) DisplayName() string { +// return "Client Name" // Human-readable name +// } +// +// func (c *newClientName) IsInstalled() bool { +// // Check if client is installed +// return commandExists("client-command") || appExists("ClientApp") +// } +// +// func (c *newClientName) InstallInstructions() string { +// return "Installation instructions here" +// } +// +// func (c *newClientName) Configure(ctx context.Context, fsys afero.Fs) error { +// fmt.Println("Configuring Client Name...") +// fmt.Println() +// +// // Implementation specific to this client +// // Could be: +// // - Running a CLI command +// // - Writing a JSON config file +// // - Manual instructions display +// +// return nil +// } +// +// Then add to clientRegistry: +// var clientRegistry = []Client{ +// &claudeCodeClient{}, +// &newClientName{}, // Add here +// } - case "windsurf": - fmt.Println("1. Open Windsurf") - fmt.Println("2. Navigate to Cascade > MCP > Configure") - fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) - fmt.Println() - fmt.Printf(`{ - "mcpServers": { - "supabase": { - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": { - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": ["mcp"] - }, - "env": { - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" - } - } - } -} -`, accessTokenPlaceholder) - fmt.Println("\n4. Tap 'Refresh' in Cascade assistant") - - case "codex": - fmt.Println("1. Open Codex") - fmt.Println("2. Navigate to Settings > MCP Servers") - fmt.Printf("3. Add the following to %s:\n", client.ConfigPath) - fmt.Println() - fmt.Printf(`{ - "mcpServers": { - "supabase": { - "type": "remote", - "url": "https://mcp.supabase.com/mcp", - "oauth": { - "authorizationServer": "https://api.supabase.com", - "clientId": "mcp-server", - "scopes": ["mcp"] - }, - "env": { - "SUPABASE_MCP_SERVER_PERSONAL_ACCESS_TOKEN": "%s" - } - } - } -} -`, accessTokenPlaceholder) - fmt.Println("\n4. Restart Codex") - } - fmt.Println("\n" + strings.Repeat("=", 50)) - fmt.Println("\nTo get your access token:") - fmt.Println(" 1. Go to https://supabase.com/dashboard/account/tokens") - fmt.Println(" 2. Create a new access token") - fmt.Println(" 3. Copy and use it in the configuration above") - fmt.Println("\nFor more information, visit:") - fmt.Println(" https://supabase.com/docs/guides/getting-started/mcp") -} From 040d01b83c36b9fec2cd462bd7d7a9b9a7672ec1 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 3 Nov 2025 17:03:06 +0000 Subject: [PATCH 09/17] feat: add Cursor support to MCP init - Implemented cursorClient with JSON config file approach - Supports both project-local (.cursor/mcp.json) and global configs - Adds Supabase MCP server URL to mcpServers configuration - Merges with existing config if present - Added appExists helper for macOS app detection - Updated help text to mention Cursor support --- cmd/mcp_init.go | 5 +- internal/mcp/init/init.go | 167 +++++++++++++++++++++++++++++--------- 2 files changed, 130 insertions(+), 42 deletions(-) diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go index 068ca86cb..a0a2820e3 100644 --- a/cmd/mcp_init.go +++ b/cmd/mcp_init.go @@ -13,7 +13,7 @@ var ( Long: `Configure the Supabase MCP server for your AI assistant clients. This command will detect installed MCP clients and guide you through the setup process. -Currently supports: Claude Code (with more clients coming soon). +Currently supports: Claude Code, Cursor (with more clients coming soon). The Supabase MCP server allows AI assistants to interact with your Supabase projects, providing tools for database operations, edge functions, storage, and more. @@ -23,7 +23,8 @@ Examples: supabase mcp init # Configure a specific client - supabase mcp init --client claude-code`, + supabase mcp init --client claude-code + supabase mcp init --client cursor`, RunE: func(cmd *cobra.Command, args []string) error { client, _ := cmd.Flags().GetString("client") return mcpinit.Run(cmd.Context(), afero.NewOsFs(), client) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 1e68d3049..48879f0a7 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -2,9 +2,12 @@ package mcpinit import ( "context" + "encoding/json" "fmt" "os" "os/exec" + "path/filepath" + "runtime" "github.com/spf13/afero" ) @@ -30,8 +33,8 @@ type Client interface { // clientRegistry holds all supported clients var clientRegistry = []Client{ &claudeCodeClient{}, + &cursorClient{}, // Add new clients here in the future: - // &cursorClient{}, // &vscodeClient{}, // &claudeDesktopClient{}, } @@ -165,44 +168,128 @@ func commandExists(command string) bool { return err == nil } -// Example template for adding a new client: -// -// type newClientName struct{} -// -// func (c *newClientName) Name() string { -// return "client-name" // CLI identifier -// } -// -// func (c *newClientName) DisplayName() string { -// return "Client Name" // Human-readable name -// } -// -// func (c *newClientName) IsInstalled() bool { -// // Check if client is installed -// return commandExists("client-command") || appExists("ClientApp") -// } -// -// func (c *newClientName) InstallInstructions() string { -// return "Installation instructions here" -// } -// -// func (c *newClientName) Configure(ctx context.Context, fsys afero.Fs) error { -// fmt.Println("Configuring Client Name...") -// fmt.Println() -// -// // Implementation specific to this client -// // Could be: -// // - Running a CLI command -// // - Writing a JSON config file -// // - Manual instructions display -// -// return nil -// } -// -// Then add to clientRegistry: -// var clientRegistry = []Client{ -// &claudeCodeClient{}, -// &newClientName{}, // Add here -// } +// cursorClient implements the Client interface for Cursor +type cursorClient struct{} + +func (c *cursorClient) Name() string { + return "cursor" +} + +func (c *cursorClient) DisplayName() string { + return "Cursor" +} + +func (c *cursorClient) IsInstalled() bool { + // Check if cursor command exists or app is installed + return commandExists("cursor") || appExists("Cursor") +} + +func (c *cursorClient) InstallInstructions() string { + return "Download from https://cursor.sh" +} + +func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Configuring Cursor...") + fmt.Println() + + // Prompt for config scope + fmt.Println("Where would you like to add the configuration?") + fmt.Println(" 1. Project-local (in .cursor/mcp.json)") + fmt.Println(" 2. Global (in your home directory)") + fmt.Print("Choice [1]: ") + + var choice string + if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read choice: %w", err) + } + if choice == "" { + choice = "1" + } + + var configPath string + if choice == "2" { + // Global config + homeDir, _ := os.UserHomeDir() + configPath = filepath.Join(homeDir, ".cursor", "mcp.json") + } else { + // Project-local config + cwd, _ := os.Getwd() + configPath = filepath.Join(cwd, ".cursor", "mcp.json") + } + + // Prepare the Supabase MCP server config + supabaseConfig := map[string]interface{}{ + "url": "https://mcp.supabase.com/mcp", + } + + // Read existing config if it exists + var config map[string]interface{} + existingData, err := afero.ReadFile(fsys, configPath) + if err == nil && len(existingData) > 0 { + if err := json.Unmarshal(existingData, &config); err != nil { + // If existing file is invalid JSON, start fresh + config = make(map[string]interface{}) + } + } else { + config = make(map[string]interface{}) + } + + // Ensure mcpServers exists + mcpServers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + mcpServers = make(map[string]interface{}) + config["mcpServers"] = mcpServers + } + + // Add or update Supabase server + mcpServers["supabase"] = supabaseConfig + + // Ensure directory exists + configDir := filepath.Dir(configPath) + if err := fsys.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write config + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + fmt.Println() + fmt.Printf("✓ Successfully configured Cursor at: %s\n", configPath) + fmt.Println() + fmt.Println("Configuration added:") + fmt.Println(`{ + "mcpServers": { + "supabase": { + "url": "https://mcp.supabase.com/mcp" + } + } +}`) + fmt.Println() + fmt.Println("The Supabase MCP server is now available in Cursor!") + return nil +} + +// appExists checks if a macOS application is installed +func appExists(appName string) bool { + if runtime.GOOS == "darwin" { + locations := []string{ + fmt.Sprintf("/Applications/%s.app", appName), + fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), + } + for _, location := range locations { + if _, err := os.Stat(location); err == nil { + return true + } + } + } + return false +} From bbfb3d66993ce3a3e0fce7ba47873bc76c5db460 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 3 Nov 2025 17:05:31 +0000 Subject: [PATCH 10/17] refactor: use baseClient composition for DRY client implementation - Created baseClient struct with common Client interface methods - Refactored claudeCodeClient and cursorClient to embed baseClient - Added constructor functions (newClaudeCodeClient, newCursorClient) - Reduced code duplication by using composition pattern - Updated example template to show new pattern - Each client now only implements Configure() method --- internal/mcp/init/init.go | 130 ++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 48879f0a7..2c859433c 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -30,13 +30,41 @@ type Client interface { Configure(ctx context.Context, fsys afero.Fs) error } + +// baseClient provides default implementations for the Client interface +type baseClient struct { + name string + displayName string + installInstructions string + checkInstalled func() bool +} + +func (b *baseClient) Name() string { + return b.name +} + +func (b *baseClient) DisplayName() string { + return b.displayName +} + +func (b *baseClient) IsInstalled() bool { + if b.checkInstalled != nil { + return b.checkInstalled() + } + return false +} + +func (b *baseClient) InstallInstructions() string { + return b.installInstructions +} + // clientRegistry holds all supported clients var clientRegistry = []Client{ - &claudeCodeClient{}, - &cursorClient{}, + newClaudeCodeClient(), + newCursorClient(), // Add new clients here in the future: - // &vscodeClient{}, - // &claudeDesktopClient{}, + // newVSCodeClient(), + // newClaudeDesktopClient(), } func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { @@ -122,22 +150,21 @@ func configureSpecificClient(ctx context.Context, fsys afero.Fs, clientName stri } // claudeCodeClient implements the Client interface for Claude Code -type claudeCodeClient struct{} - -func (c *claudeCodeClient) Name() string { - return "claude-code" -} - -func (c *claudeCodeClient) DisplayName() string { - return "Claude Code" +type claudeCodeClient struct { + baseClient } -func (c *claudeCodeClient) IsInstalled() bool { - return commandExists("claude") -} - -func (c *claudeCodeClient) InstallInstructions() string { - return "npm install -g @anthropic-ai/claude-cli" +func newClaudeCodeClient() *claudeCodeClient { + return &claudeCodeClient{ + baseClient: baseClient{ + name: "claude-code", + displayName: "Claude Code", + installInstructions: "npm install -g @anthropic-ai/claude-cli", + checkInstalled: func() bool { + return commandExists("claude") + }, + }, + } } func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { @@ -169,23 +196,21 @@ func commandExists(command string) bool { } // cursorClient implements the Client interface for Cursor -type cursorClient struct{} - -func (c *cursorClient) Name() string { - return "cursor" +type cursorClient struct { + baseClient } -func (c *cursorClient) DisplayName() string { - return "Cursor" -} - -func (c *cursorClient) IsInstalled() bool { - // Check if cursor command exists or app is installed - return commandExists("cursor") || appExists("Cursor") -} - -func (c *cursorClient) InstallInstructions() string { - return "Download from https://cursor.sh" +func newCursorClient() *cursorClient { + return &cursorClient{ + baseClient: baseClient{ + name: "cursor", + displayName: "Cursor", + installInstructions: "Download from https://cursor.sh", + checkInstalled: func() bool { + return commandExists("cursor") || appExists("Cursor") + }, + }, + } } func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { @@ -292,4 +317,43 @@ func appExists(appName string) bool { return false } +// Example: Adding a new client +// +// 1. Create a struct that embeds baseClient: +// +// type myNewClient struct { +// baseClient +// } +// +// 2. Create a constructor function: +// +// func newMyNewClient() *myNewClient { +// return &myNewClient{ +// baseClient: baseClient{ +// name: "my-client", +// displayName: "My Client", +// installInstructions: "Installation command or URL", +// checkInstalled: func() bool { +// return commandExists("my-cli") || appExists("MyApp") +// }, +// }, +// } +// } +// +// 3. Implement the Configure method: +// +// func (c *myNewClient) Configure(ctx context.Context, fsys afero.Fs) error { +// // Your configuration logic here +// // See claudeCodeClient or cursorClient for examples +// return nil +// } +// +// 4. Add to clientRegistry: +// +// var clientRegistry = []Client{ +// newClaudeCodeClient(), +// newCursorClient(), +// newMyNewClient(), // Add here +// } + From 2bfe1e7a16ac14816598053124d0465fa64eadcc Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 3 Nov 2025 17:07:12 +0000 Subject: [PATCH 11/17] feat: add VS Code support to MCP init - Implemented vscodeClient with JSON config file approach - Supports both project-local (.vscode/mcp.json) and global configs - Adds Supabase MCP server with type: http and URL - Merges with existing config if present - Updated help text to mention VS Code support - Checks for 'code' command availability --- cmd/mcp_init.go | 5 +- internal/mcp/init/init.go | 153 +++++++++++++++++++++++++++----------- 2 files changed, 112 insertions(+), 46 deletions(-) diff --git a/cmd/mcp_init.go b/cmd/mcp_init.go index a0a2820e3..cddceffe7 100644 --- a/cmd/mcp_init.go +++ b/cmd/mcp_init.go @@ -13,7 +13,7 @@ var ( Long: `Configure the Supabase MCP server for your AI assistant clients. This command will detect installed MCP clients and guide you through the setup process. -Currently supports: Claude Code, Cursor (with more clients coming soon). +Currently supports: Claude Code, Cursor, VS Code (with more clients coming soon). The Supabase MCP server allows AI assistants to interact with your Supabase projects, providing tools for database operations, edge functions, storage, and more. @@ -24,7 +24,8 @@ Examples: # Configure a specific client supabase mcp init --client claude-code - supabase mcp init --client cursor`, + supabase mcp init --client cursor + supabase mcp init --client vscode`, RunE: func(cmd *cobra.Command, args []string) error { client, _ := cmd.Flags().GetString("client") return mcpinit.Run(cmd.Context(), afero.NewOsFs(), client) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 2c859433c..f1909731c 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -62,9 +62,7 @@ func (b *baseClient) InstallInstructions() string { var clientRegistry = []Client{ newClaudeCodeClient(), newCursorClient(), - // Add new clients here in the future: - // newVSCodeClient(), - // newClaudeDesktopClient(), + newVSCodeClient(), } func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { @@ -301,6 +299,114 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { return nil } +// vscodeClient implements the Client interface for VS Code +type vscodeClient struct { + baseClient +} + +func newVSCodeClient() *vscodeClient { + return &vscodeClient{ + baseClient: baseClient{ + name: "vscode", + displayName: "VS Code", + installInstructions: "Download from https://code.visualstudio.com", + checkInstalled: func() bool { + return commandExists("code") + }, + }, + } +} + +func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Configuring VS Code...") + fmt.Println() + + // Prompt for config scope + fmt.Println("Where would you like to add the configuration?") + fmt.Println(" 1. Project-local (in .vscode/mcp.json)") + fmt.Println(" 2. Global (in your home directory)") + fmt.Print("Choice [1]: ") + + var choice string + if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read choice: %w", err) + } + if choice == "" { + choice = "1" + } + + var configPath string + if choice == "2" { + // Global config + homeDir, _ := os.UserHomeDir() + configPath = filepath.Join(homeDir, ".vscode", "mcp.json") + } else { + // Project-local config + cwd, _ := os.Getwd() + configPath = filepath.Join(cwd, ".vscode", "mcp.json") + } + + // Prepare the Supabase MCP server config + supabaseConfig := map[string]interface{}{ + "type": "http", + "url": "https://mcp.supabase.com/mcp", + } + + // Read existing config if it exists + var config map[string]interface{} + existingData, err := afero.ReadFile(fsys, configPath) + if err == nil && len(existingData) > 0 { + if err := json.Unmarshal(existingData, &config); err != nil { + // If existing file is invalid JSON, start fresh + config = make(map[string]interface{}) + } + } else { + config = make(map[string]interface{}) + } + + // Ensure mcpServers exists + mcpServers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + mcpServers = make(map[string]interface{}) + config["mcpServers"] = mcpServers + } + + // Add or update Supabase server + mcpServers["supabase"] = supabaseConfig + + // Ensure directory exists + configDir := filepath.Dir(configPath) + if err := fsys.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write config + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + fmt.Println() + fmt.Printf("✓ Successfully configured VS Code at: %s\n", configPath) + fmt.Println() + fmt.Println("Configuration added:") + fmt.Println(`{ + "mcpServers": { + "supabase": { + "type": "http", + "url": "https://mcp.supabase.com/mcp" + } + } +}`) + fmt.Println() + fmt.Println("The Supabase MCP server is now available in VS Code!") + return nil +} + // appExists checks if a macOS application is installed func appExists(appName string) bool { if runtime.GOOS == "darwin" { @@ -316,44 +422,3 @@ func appExists(appName string) bool { } return false } - -// Example: Adding a new client -// -// 1. Create a struct that embeds baseClient: -// -// type myNewClient struct { -// baseClient -// } -// -// 2. Create a constructor function: -// -// func newMyNewClient() *myNewClient { -// return &myNewClient{ -// baseClient: baseClient{ -// name: "my-client", -// displayName: "My Client", -// installInstructions: "Installation command or URL", -// checkInstalled: func() bool { -// return commandExists("my-cli") || appExists("MyApp") -// }, -// }, -// } -// } -// -// 3. Implement the Configure method: -// -// func (c *myNewClient) Configure(ctx context.Context, fsys afero.Fs) error { -// // Your configuration logic here -// // See claudeCodeClient or cursorClient for examples -// return nil -// } -// -// 4. Add to clientRegistry: -// -// var clientRegistry = []Client{ -// newClaudeCodeClient(), -// newCursorClient(), -// newMyNewClient(), // Add here -// } - - From a725a0ad8b779072f31aeee8e4b6c6e3279485a4 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 3 Nov 2025 17:11:15 +0000 Subject: [PATCH 12/17] refactor: split client implementations into separate files Reorganized MCP init package for better scalability and maintainability: - client.go: Base Client interface and baseClient implementation - claude_code.go: Claude Code client implementation - cursor.go: Cursor client implementation - vscode.go: VS Code client implementation - utils.go: Helper functions (appExists) - init.go: Core logic, client registry, and orchestration Benefits: - Each client is now in its own file for easy maintenance - Adding new clients is simpler - just create a new file - Reduced init.go from 425 to ~100 lines - Better separation of concerns - Easier to navigate and understand --- internal/mcp/init/claude_code.go | 50 +++++ internal/mcp/init/client.go | 59 ++++++ internal/mcp/init/cursor.go | 117 +++++++++++ internal/mcp/init/init.go | 327 ------------------------------- internal/mcp/init/utils.go | 23 +++ internal/mcp/init/vscode.go | 119 +++++++++++ 6 files changed, 368 insertions(+), 327 deletions(-) create mode 100644 internal/mcp/init/claude_code.go create mode 100644 internal/mcp/init/client.go create mode 100644 internal/mcp/init/cursor.go create mode 100644 internal/mcp/init/utils.go create mode 100644 internal/mcp/init/vscode.go diff --git a/internal/mcp/init/claude_code.go b/internal/mcp/init/claude_code.go new file mode 100644 index 000000000..fb02b26df --- /dev/null +++ b/internal/mcp/init/claude_code.go @@ -0,0 +1,50 @@ +package mcpinit + +import ( + "context" + "fmt" + "os" + "os/exec" + + "github.com/spf13/afero" +) + +// claudeCodeClient implements the Client interface for Claude Code +type claudeCodeClient struct { + baseClient +} + +func newClaudeCodeClient() *claudeCodeClient { + return &claudeCodeClient{ + baseClient: baseClient{ + name: "claude-code", + displayName: "Claude Code", + installInstructions: "npm install -g @anthropic-ai/claude-cli", + checkInstalled: func() bool { + return commandExists("claude") + }, + }, + } +} + +func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Adding Supabase MCP server to Claude Code...") + fmt.Println() + + // Build the claude mcp add command + // #nosec G204 -- command and URL are controlled constants + cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "https://mcp.supabase.com/mcp") + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to configure Claude Code: %w", err) + } + + fmt.Println() + fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") + fmt.Println() + fmt.Println("The server is now available in your Claude Code environment.") + return nil +} diff --git a/internal/mcp/init/client.go b/internal/mcp/init/client.go new file mode 100644 index 000000000..ae80203b8 --- /dev/null +++ b/internal/mcp/init/client.go @@ -0,0 +1,59 @@ +package mcpinit + +import ( + "context" + "os/exec" + + "github.com/spf13/afero" +) + +// Client represents an MCP client that can be configured +type Client interface { + // Name returns the client identifier (e.g., "claude-code") + Name() string + + // DisplayName returns the human-readable name (e.g., "Claude Code") + DisplayName() string + + // IsInstalled checks if the client is installed on the system + IsInstalled() bool + + // InstallInstructions returns instructions for installing the client + InstallInstructions() string + + // Configure performs the configuration for this client + Configure(ctx context.Context, fsys afero.Fs) error +} + +// baseClient provides default implementations for the Client interface +type baseClient struct { + name string + displayName string + installInstructions string + checkInstalled func() bool +} + +func (b *baseClient) Name() string { + return b.name +} + +func (b *baseClient) DisplayName() string { + return b.displayName +} + +func (b *baseClient) IsInstalled() bool { + if b.checkInstalled != nil { + return b.checkInstalled() + } + return false +} + +func (b *baseClient) InstallInstructions() string { + return b.installInstructions +} + +// commandExists checks if a command-line tool is available +func commandExists(command string) bool { + _, err := exec.LookPath(command) + return err == nil +} diff --git a/internal/mcp/init/cursor.go b/internal/mcp/init/cursor.go new file mode 100644 index 000000000..57b505f2f --- /dev/null +++ b/internal/mcp/init/cursor.go @@ -0,0 +1,117 @@ +package mcpinit + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/afero" +) + +// cursorClient implements the Client interface for Cursor +type cursorClient struct { + baseClient +} + +func newCursorClient() *cursorClient { + return &cursorClient{ + baseClient: baseClient{ + name: "cursor", + displayName: "Cursor", + installInstructions: "Download from https://cursor.sh", + checkInstalled: func() bool { + return commandExists("cursor") || appExists("Cursor") + }, + }, + } +} + +func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Configuring Cursor...") + fmt.Println() + + // Prompt for config scope + fmt.Println("Where would you like to add the configuration?") + fmt.Println(" 1. Project-local (in .cursor/mcp.json)") + fmt.Println(" 2. Global (in your home directory)") + fmt.Print("Choice [1]: ") + + var choice string + if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read choice: %w", err) + } + if choice == "" { + choice = "1" + } + + var configPath string + if choice == "2" { + // Global config + homeDir, _ := os.UserHomeDir() + configPath = filepath.Join(homeDir, ".cursor", "mcp.json") + } else { + // Project-local config + cwd, _ := os.Getwd() + configPath = filepath.Join(cwd, ".cursor", "mcp.json") + } + + // Prepare the Supabase MCP server config + supabaseConfig := map[string]interface{}{ + "url": "https://mcp.supabase.com/mcp", + } + + // Read existing config if it exists + var config map[string]interface{} + existingData, err := afero.ReadFile(fsys, configPath) + if err == nil && len(existingData) > 0 { + if err := json.Unmarshal(existingData, &config); err != nil { + // If existing file is invalid JSON, start fresh + config = make(map[string]interface{}) + } + } else { + config = make(map[string]interface{}) + } + + // Ensure mcpServers exists + mcpServers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + mcpServers = make(map[string]interface{}) + config["mcpServers"] = mcpServers + } + + // Add or update Supabase server + mcpServers["supabase"] = supabaseConfig + + // Ensure directory exists + configDir := filepath.Dir(configPath) + if err := fsys.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write config + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + fmt.Println() + fmt.Printf("✓ Successfully configured Cursor at: %s\n", configPath) + fmt.Println() + fmt.Println("Configuration added:") + fmt.Println(`{ + "mcpServers": { + "supabase": { + "url": "https://mcp.supabase.com/mcp" + } + } +}`) + fmt.Println() + fmt.Println("The Supabase MCP server is now available in Cursor!") + return nil +} diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index f1909731c..e6ec908ac 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -2,62 +2,11 @@ package mcpinit import ( "context" - "encoding/json" "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" "github.com/spf13/afero" ) -// Client represents an MCP client that can be configured -type Client interface { - // Name returns the client identifier (e.g., "claude-code") - Name() string - - // DisplayName returns the human-readable name (e.g., "Claude Code") - DisplayName() string - - // IsInstalled checks if the client is installed on the system - IsInstalled() bool - - // InstallInstructions returns instructions for installing the client - InstallInstructions() string - - // Configure performs the configuration for this client - Configure(ctx context.Context, fsys afero.Fs) error -} - - -// baseClient provides default implementations for the Client interface -type baseClient struct { - name string - displayName string - installInstructions string - checkInstalled func() bool -} - -func (b *baseClient) Name() string { - return b.name -} - -func (b *baseClient) DisplayName() string { - return b.displayName -} - -func (b *baseClient) IsInstalled() bool { - if b.checkInstalled != nil { - return b.checkInstalled() - } - return false -} - -func (b *baseClient) InstallInstructions() string { - return b.installInstructions -} - // clientRegistry holds all supported clients var clientRegistry = []Client{ newClaudeCodeClient(), @@ -146,279 +95,3 @@ func configureSpecificClient(ctx context.Context, fsys afero.Fs, clientName stri fmt.Printf("Configuring %s...\n\n", targetClient.DisplayName()) return targetClient.Configure(ctx, fsys) } - -// claudeCodeClient implements the Client interface for Claude Code -type claudeCodeClient struct { - baseClient -} - -func newClaudeCodeClient() *claudeCodeClient { - return &claudeCodeClient{ - baseClient: baseClient{ - name: "claude-code", - displayName: "Claude Code", - installInstructions: "npm install -g @anthropic-ai/claude-cli", - checkInstalled: func() bool { - return commandExists("claude") - }, - }, - } -} - -func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { - fmt.Println("Adding Supabase MCP server to Claude Code...") - fmt.Println() - - // Build the claude mcp add command - // #nosec G204 -- command and URL are controlled constants - cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "https://mcp.supabase.com/mcp") - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to configure Claude Code: %w", err) - } - - fmt.Println() - fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") - fmt.Println() - fmt.Println("The server is now available in your Claude Code environment.") - return nil -} - -// Helper function to check if a command exists -func commandExists(command string) bool { - _, err := exec.LookPath(command) - return err == nil -} - -// cursorClient implements the Client interface for Cursor -type cursorClient struct { - baseClient -} - -func newCursorClient() *cursorClient { - return &cursorClient{ - baseClient: baseClient{ - name: "cursor", - displayName: "Cursor", - installInstructions: "Download from https://cursor.sh", - checkInstalled: func() bool { - return commandExists("cursor") || appExists("Cursor") - }, - }, - } -} - -func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { - fmt.Println("Configuring Cursor...") - fmt.Println() - - // Prompt for config scope - fmt.Println("Where would you like to add the configuration?") - fmt.Println(" 1. Project-local (in .cursor/mcp.json)") - fmt.Println(" 2. Global (in your home directory)") - fmt.Print("Choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read choice: %w", err) - } - if choice == "" { - choice = "1" - } - - var configPath string - if choice == "2" { - // Global config - homeDir, _ := os.UserHomeDir() - configPath = filepath.Join(homeDir, ".cursor", "mcp.json") - } else { - // Project-local config - cwd, _ := os.Getwd() - configPath = filepath.Join(cwd, ".cursor", "mcp.json") - } - - // Prepare the Supabase MCP server config - supabaseConfig := map[string]interface{}{ - "url": "https://mcp.supabase.com/mcp", - } - - // Read existing config if it exists - var config map[string]interface{} - existingData, err := afero.ReadFile(fsys, configPath) - if err == nil && len(existingData) > 0 { - if err := json.Unmarshal(existingData, &config); err != nil { - // If existing file is invalid JSON, start fresh - config = make(map[string]interface{}) - } - } else { - config = make(map[string]interface{}) - } - - // Ensure mcpServers exists - mcpServers, ok := config["mcpServers"].(map[string]interface{}) - if !ok { - mcpServers = make(map[string]interface{}) - config["mcpServers"] = mcpServers - } - - // Add or update Supabase server - mcpServers["supabase"] = supabaseConfig - - // Ensure directory exists - configDir := filepath.Dir(configPath) - if err := fsys.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Write config - configJSON, err := json.MarshalIndent(config, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - fmt.Println() - fmt.Printf("✓ Successfully configured Cursor at: %s\n", configPath) - fmt.Println() - fmt.Println("Configuration added:") - fmt.Println(`{ - "mcpServers": { - "supabase": { - "url": "https://mcp.supabase.com/mcp" - } - } -}`) - fmt.Println() - fmt.Println("The Supabase MCP server is now available in Cursor!") - return nil -} - -// vscodeClient implements the Client interface for VS Code -type vscodeClient struct { - baseClient -} - -func newVSCodeClient() *vscodeClient { - return &vscodeClient{ - baseClient: baseClient{ - name: "vscode", - displayName: "VS Code", - installInstructions: "Download from https://code.visualstudio.com", - checkInstalled: func() bool { - return commandExists("code") - }, - }, - } -} - -func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { - fmt.Println("Configuring VS Code...") - fmt.Println() - - // Prompt for config scope - fmt.Println("Where would you like to add the configuration?") - fmt.Println(" 1. Project-local (in .vscode/mcp.json)") - fmt.Println(" 2. Global (in your home directory)") - fmt.Print("Choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read choice: %w", err) - } - if choice == "" { - choice = "1" - } - - var configPath string - if choice == "2" { - // Global config - homeDir, _ := os.UserHomeDir() - configPath = filepath.Join(homeDir, ".vscode", "mcp.json") - } else { - // Project-local config - cwd, _ := os.Getwd() - configPath = filepath.Join(cwd, ".vscode", "mcp.json") - } - - // Prepare the Supabase MCP server config - supabaseConfig := map[string]interface{}{ - "type": "http", - "url": "https://mcp.supabase.com/mcp", - } - - // Read existing config if it exists - var config map[string]interface{} - existingData, err := afero.ReadFile(fsys, configPath) - if err == nil && len(existingData) > 0 { - if err := json.Unmarshal(existingData, &config); err != nil { - // If existing file is invalid JSON, start fresh - config = make(map[string]interface{}) - } - } else { - config = make(map[string]interface{}) - } - - // Ensure mcpServers exists - mcpServers, ok := config["mcpServers"].(map[string]interface{}) - if !ok { - mcpServers = make(map[string]interface{}) - config["mcpServers"] = mcpServers - } - - // Add or update Supabase server - mcpServers["supabase"] = supabaseConfig - - // Ensure directory exists - configDir := filepath.Dir(configPath) - if err := fsys.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Write config - configJSON, err := json.MarshalIndent(config, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - fmt.Println() - fmt.Printf("✓ Successfully configured VS Code at: %s\n", configPath) - fmt.Println() - fmt.Println("Configuration added:") - fmt.Println(`{ - "mcpServers": { - "supabase": { - "type": "http", - "url": "https://mcp.supabase.com/mcp" - } - } -}`) - fmt.Println() - fmt.Println("The Supabase MCP server is now available in VS Code!") - return nil -} - -// appExists checks if a macOS application is installed -func appExists(appName string) bool { - if runtime.GOOS == "darwin" { - locations := []string{ - fmt.Sprintf("/Applications/%s.app", appName), - fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), - } - for _, location := range locations { - if _, err := os.Stat(location); err == nil { - return true - } - } - } - return false -} diff --git a/internal/mcp/init/utils.go b/internal/mcp/init/utils.go new file mode 100644 index 000000000..fbd1e6784 --- /dev/null +++ b/internal/mcp/init/utils.go @@ -0,0 +1,23 @@ +package mcpinit + +import ( + "fmt" + "os" + "runtime" +) + +// appExists checks if a macOS application is installed +func appExists(appName string) bool { + if runtime.GOOS == "darwin" { + locations := []string{ + fmt.Sprintf("/Applications/%s.app", appName), + fmt.Sprintf("%s/Applications/%s.app", os.Getenv("HOME"), appName), + } + for _, location := range locations { + if _, err := os.Stat(location); err == nil { + return true + } + } + } + return false +} diff --git a/internal/mcp/init/vscode.go b/internal/mcp/init/vscode.go new file mode 100644 index 000000000..703550af6 --- /dev/null +++ b/internal/mcp/init/vscode.go @@ -0,0 +1,119 @@ +package mcpinit + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/afero" +) + +// vscodeClient implements the Client interface for VS Code +type vscodeClient struct { + baseClient +} + +func newVSCodeClient() *vscodeClient { + return &vscodeClient{ + baseClient: baseClient{ + name: "vscode", + displayName: "VS Code", + installInstructions: "Download from https://code.visualstudio.com", + checkInstalled: func() bool { + return commandExists("code") + }, + }, + } +} + +func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { + fmt.Println("Configuring VS Code...") + fmt.Println() + + // Prompt for config scope + fmt.Println("Where would you like to add the configuration?") + fmt.Println(" 1. Project-local (in .vscode/mcp.json)") + fmt.Println(" 2. Global (in your home directory)") + fmt.Print("Choice [1]: ") + + var choice string + if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { + return fmt.Errorf("failed to read choice: %w", err) + } + if choice == "" { + choice = "1" + } + + var configPath string + if choice == "2" { + // Global config + homeDir, _ := os.UserHomeDir() + configPath = filepath.Join(homeDir, ".vscode", "mcp.json") + } else { + // Project-local config + cwd, _ := os.Getwd() + configPath = filepath.Join(cwd, ".vscode", "mcp.json") + } + + // Prepare the Supabase MCP server config + supabaseConfig := map[string]interface{}{ + "type": "http", + "url": "https://mcp.supabase.com/mcp", + } + + // Read existing config if it exists + var config map[string]interface{} + existingData, err := afero.ReadFile(fsys, configPath) + if err == nil && len(existingData) > 0 { + if err := json.Unmarshal(existingData, &config); err != nil { + // If existing file is invalid JSON, start fresh + config = make(map[string]interface{}) + } + } else { + config = make(map[string]interface{}) + } + + // Ensure mcpServers exists + mcpServers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + mcpServers = make(map[string]interface{}) + config["mcpServers"] = mcpServers + } + + // Add or update Supabase server + mcpServers["supabase"] = supabaseConfig + + // Ensure directory exists + configDir := filepath.Dir(configPath) + if err := fsys.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write config + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + fmt.Println() + fmt.Printf("✓ Successfully configured VS Code at: %s\n", configPath) + fmt.Println() + fmt.Println("Configuration added:") + fmt.Println(`{ + "mcpServers": { + "supabase": { + "type": "http", + "url": "https://mcp.supabase.com/mcp" + } + } +}`) + fmt.Println() + fmt.Println("The Supabase MCP server is now available in VS Code!") + return nil +} From 7d6d97d47df9dccfb822801eb41d7b8723fc416e Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 5 Nov 2025 14:24:05 +0000 Subject: [PATCH 13/17] feat(init): add --mcp-client flag to configure MCP during project init - Adds --mcp-client flag to supabase init (e.g., --mcp-client=claude-code) - Wires to internal MCP setup after project scaffolding - Addresses review feedback to avoid a top-level mcp command for config --- cmd/init.go | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index a9d2d90a8..8cc7ee563 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" _init "github.com/supabase/cli/internal/init" + mcpinit "github.com/supabase/cli/internal/mcp/init" "github.com/supabase/cli/internal/utils" ) @@ -34,16 +35,29 @@ var ( } }, RunE: func(cmd *cobra.Command, args []string) error { - fsys := afero.NewOsFs() - if !cmd.Flags().Changed("with-vscode-settings") && !cmd.Flags().Changed("with-vscode-workspace") { - createVscodeSettings = nil - } + fsys := afero.NewOsFs() + if !cmd.Flags().Changed("with-vscode-settings") && !cmd.Flags().Changed("with-vscode-workspace") { + createVscodeSettings = nil + } - if !cmd.Flags().Changed("with-intellij-settings") { - createIntellijSettings = nil - } - ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - return _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams) + if !cmd.Flags().Changed("with-intellij-settings") { + createIntellijSettings = nil + } + ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) + + // Run core project init first + if err := _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams); err != nil { + return err + } + + // Optionally configure MCP client if flag is provided + mcpClient, _ := cmd.Flags().GetString("mcp-client") + if mcpClient != "" { + if err := mcpinit.Run(ctx, fsys, mcpClient); err != nil { + return err + } + } + return nil }, PostRun: func(cmd *cobra.Command, args []string) { fmt.Println("Finished " + utils.Aqua("supabase init") + ".") @@ -59,5 +73,6 @@ func init() { flags.BoolVar(createIntellijSettings, "with-intellij-settings", false, "Generate IntelliJ IDEA settings for Deno.") flags.BoolVar(&initParams.UseOrioleDB, "use-orioledb", false, "Use OrioleDB storage engine for Postgres.") flags.BoolVar(&initParams.Overwrite, "force", false, "Overwrite existing "+utils.ConfigPath+".") + flags.String("mcp-client", "", "Configure Supabase MCP for a client (e.g., claude-code, cursor, vscode). Runs after project init if provided.") rootCmd.AddCommand(initCmd) } From 538f4b1339815792bb5e5c4e0da42b45d51ca98c Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 5 Nov 2025 14:34:04 +0000 Subject: [PATCH 14/17] fix: apply gofmt formatting to cmd/init.go and internal/mcp/init/init.go --- cmd/init.go | 38 +++++++++++++++++++------------------- internal/mcp/init/init.go | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 8cc7ee563..d02f13a34 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -35,29 +35,29 @@ var ( } }, RunE: func(cmd *cobra.Command, args []string) error { - fsys := afero.NewOsFs() - if !cmd.Flags().Changed("with-vscode-settings") && !cmd.Flags().Changed("with-vscode-workspace") { - createVscodeSettings = nil - } + fsys := afero.NewOsFs() + if !cmd.Flags().Changed("with-vscode-settings") && !cmd.Flags().Changed("with-vscode-workspace") { + createVscodeSettings = nil + } - if !cmd.Flags().Changed("with-intellij-settings") { - createIntellijSettings = nil - } - ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) + if !cmd.Flags().Changed("with-intellij-settings") { + createIntellijSettings = nil + } + ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - // Run core project init first - if err := _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams); err != nil { - return err - } + // Run core project init first + if err := _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams); err != nil { + return err + } - // Optionally configure MCP client if flag is provided - mcpClient, _ := cmd.Flags().GetString("mcp-client") - if mcpClient != "" { - if err := mcpinit.Run(ctx, fsys, mcpClient); err != nil { - return err - } + // Optionally configure MCP client if flag is provided + mcpClient, _ := cmd.Flags().GetString("mcp-client") + if mcpClient != "" { + if err := mcpinit.Run(ctx, fsys, mcpClient); err != nil { + return err } - return nil + } + return nil }, PostRun: func(cmd *cobra.Command, args []string) { fmt.Println("Finished " + utils.Aqua("supabase init") + ".") diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index e6ec908ac..8e0281719 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -60,7 +60,7 @@ func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { for _, client := range installedClients { fmt.Printf(" supabase mcp init --client %s\n", client.Name()) } - + return nil } From b4f49dc6e831df2fe12ee338485ec20f8d23a6c6 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Fri, 7 Nov 2025 11:09:01 +0000 Subject: [PATCH 15/17] feat: add interactive MCP configuration to supabase init - Replace --mcp-client flag with interactive prompts - Add yes/no prompt: 'Configure Supabase MCP server locally?' - Add dropdown selection for client choice (Claude Code, Cursor, VS Code, Other) - Add 'Other' option for manual configuration - Update all clients to use local MCP server (http://localhost:54321/mcp) - Fix VS Code config property from 'mcpServers' to 'servers' - Replace VS Code scope selection with interactive dropdown (project/global) This implements the requirements from Linear issue AI-81. --- cmd/init.go | 19 ++++++++++---- internal/mcp/init/claude_code.go | 7 +++-- internal/mcp/init/cursor.go | 4 +-- internal/mcp/init/init.go | 26 ++++++++++++++++++ internal/mcp/init/vscode.go | 45 ++++++++++++++++++-------------- 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index d02f13a34..515423026 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -50,13 +50,23 @@ var ( return err } - // Optionally configure MCP client if flag is provided - mcpClient, _ := cmd.Flags().GetString("mcp-client") - if mcpClient != "" { - if err := mcpinit.Run(ctx, fsys, mcpClient); err != nil { + // Prompt for MCP configuration if in interactive mode + console := utils.NewConsole() + if configureMCP, err := console.PromptYesNo(ctx, "Configure Supabase MCP server locally?", false); err != nil { + return err + } else if configureMCP { + clientName, err := mcpinit.PromptMCPClient(ctx) + if err != nil { return err } + // Skip configuration if user selected "other" + if clientName != "other" { + if err := mcpinit.Run(ctx, fsys, clientName); err != nil { + return err + } + } } + return nil }, PostRun: func(cmd *cobra.Command, args []string) { @@ -73,6 +83,5 @@ func init() { flags.BoolVar(createIntellijSettings, "with-intellij-settings", false, "Generate IntelliJ IDEA settings for Deno.") flags.BoolVar(&initParams.UseOrioleDB, "use-orioledb", false, "Use OrioleDB storage engine for Postgres.") flags.BoolVar(&initParams.Overwrite, "force", false, "Overwrite existing "+utils.ConfigPath+".") - flags.String("mcp-client", "", "Configure Supabase MCP for a client (e.g., claude-code, cursor, vscode). Runs after project init if provided.") rootCmd.AddCommand(initCmd) } diff --git a/internal/mcp/init/claude_code.go b/internal/mcp/init/claude_code.go index fb02b26df..7f48f107c 100644 --- a/internal/mcp/init/claude_code.go +++ b/internal/mcp/init/claude_code.go @@ -28,12 +28,11 @@ func newClaudeCodeClient() *claudeCodeClient { } func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { - fmt.Println("Adding Supabase MCP server to Claude Code...") + fmt.Println("Configuring Claude Code...") fmt.Println() - // Build the claude mcp add command - // #nosec G204 -- command and URL are controlled constants - cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "https://mcp.supabase.com/mcp") + // Run the 'claude mcp add' command + cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "http://localhost:54321/mcp") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/mcp/init/cursor.go b/internal/mcp/init/cursor.go index 57b505f2f..def8717c4 100644 --- a/internal/mcp/init/cursor.go +++ b/internal/mcp/init/cursor.go @@ -59,7 +59,7 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { // Prepare the Supabase MCP server config supabaseConfig := map[string]interface{}{ - "url": "https://mcp.supabase.com/mcp", + "url": "http://localhost:54321/mcp", } // Read existing config if it exists @@ -107,7 +107,7 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println(`{ "mcpServers": { "supabase": { - "url": "https://mcp.supabase.com/mcp" + "url": "http://localhost:54321/mcp" } } }`) diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index 8e0281719..feb4ff8ed 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + tea "github.com/charmbracelet/bubbletea" "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" ) // clientRegistry holds all supported clients @@ -14,6 +16,30 @@ var clientRegistry = []Client{ newVSCodeClient(), } +// PromptMCPClient prompts the user to select an MCP client from the available options +func PromptMCPClient(ctx context.Context, opts ...tea.ProgramOption) (string, error) { + // Add all clients plus "Other" option + items := make([]utils.PromptItem, len(clientRegistry)+1) + for i, client := range clientRegistry { + items[i] = utils.PromptItem{ + Summary: client.Name(), + Details: client.DisplayName(), + } + } + // Add "Other" option at the end + items[len(clientRegistry)] = utils.PromptItem{ + Summary: "other", + Details: "Configure it manually", + } + + choice, err := utils.PromptChoice(ctx, "Which client do you want to configure?", items, opts...) + if err != nil { + return "", err + } + + return choice.Summary, nil +} + func Run(ctx context.Context, fsys afero.Fs, clientFlag string) error { // If a specific client is requested if clientFlag != "" { diff --git a/internal/mcp/init/vscode.go b/internal/mcp/init/vscode.go index 703550af6..f6c4bd89f 100644 --- a/internal/mcp/init/vscode.go +++ b/internal/mcp/init/vscode.go @@ -7,7 +7,9 @@ import ( "os" "path/filepath" + tea "github.com/charmbracelet/bubbletea" "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" ) // vscodeClient implements the Client interface for VS Code @@ -32,22 +34,25 @@ func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println("Configuring VS Code...") fmt.Println() - // Prompt for config scope - fmt.Println("Where would you like to add the configuration?") - fmt.Println(" 1. Project-local (in .vscode/mcp.json)") - fmt.Println(" 2. Global (in your home directory)") - fmt.Print("Choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read choice: %w", err) + // Prompt for config scope using dropdown + items := []utils.PromptItem{ + { + Summary: "project", + Details: "Project-local (in .vscode/mcp.json)", + }, + { + Summary: "global", + Details: "Global (in your home directory)", + }, } - if choice == "" { - choice = "1" + + choice, err := utils.PromptChoice(ctx, "Where would you like to add the configuration?", items, tea.WithOutput(os.Stderr)) + if err != nil { + return err } var configPath string - if choice == "2" { + if choice.Summary == "global" { // Global config homeDir, _ := os.UserHomeDir() configPath = filepath.Join(homeDir, ".vscode", "mcp.json") @@ -60,7 +65,7 @@ func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { // Prepare the Supabase MCP server config supabaseConfig := map[string]interface{}{ "type": "http", - "url": "https://mcp.supabase.com/mcp", + "url": "http://localhost:54321/mcp", } // Read existing config if it exists @@ -75,15 +80,15 @@ func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { config = make(map[string]interface{}) } - // Ensure mcpServers exists - mcpServers, ok := config["mcpServers"].(map[string]interface{}) + // Ensure servers exists + servers, ok := config["servers"].(map[string]interface{}) if !ok { - mcpServers = make(map[string]interface{}) - config["mcpServers"] = mcpServers + servers = make(map[string]interface{}) + config["servers"] = servers } // Add or update Supabase server - mcpServers["supabase"] = supabaseConfig + servers["supabase"] = supabaseConfig // Ensure directory exists configDir := filepath.Dir(configPath) @@ -106,10 +111,10 @@ func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println() fmt.Println("Configuration added:") fmt.Println(`{ - "mcpServers": { + "servers": { "supabase": { "type": "http", - "url": "https://mcp.supabase.com/mcp" + "url": "http://localhost:54321/mcp" } } }`) From 5a67491facd4fa5b6d119ef2b4e280d89a40fab9 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Fri, 7 Nov 2025 14:41:30 +0000 Subject: [PATCH 16/17] minor fixes --- internal/mcp/init/claude_code.go | 45 +++++++++++++++++++++++++------ internal/mcp/init/cursor.go | 46 +++++++++++++++++--------------- internal/mcp/init/vscode.go | 16 +++++------ 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/internal/mcp/init/claude_code.go b/internal/mcp/init/claude_code.go index 7f48f107c..25abff730 100644 --- a/internal/mcp/init/claude_code.go +++ b/internal/mcp/init/claude_code.go @@ -7,6 +7,7 @@ import ( "os/exec" "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" ) // claudeCodeClient implements the Client interface for Claude Code @@ -31,19 +32,47 @@ func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println("Configuring Claude Code...") fmt.Println() - // Run the 'claude mcp add' command - cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "http://localhost:54321/mcp") + // Use utils.PromptChoice for dropdown + choice, err := utils.PromptChoice(ctx, "Where would you like to add the Claude Code MCP server?", []utils.PromptItem{ + {Summary: "local", Details: "Local (only for you in this project)"}, + {Summary: "project", Details: "Project (shared via .mcp.json in project root)"}, + {Summary: "user", Details: "User (available across all projects for your user)"}, + }) + if err != nil { + fmt.Printf("⚠️ Warning: failed to select scope for Claude Code MCP server: %v\n", err) + fmt.Println("Defaulting to local scope.") + choice = utils.PromptItem{Summary: "local"} + } + + cmdArgs := []string{"mcp", "add", "--transport", "http", "supabase", "http://localhost:54321/mcp"} + if choice.Summary != "local" { + cmdArgs = append(cmdArgs, "--scope", choice.Summary) + } + cmd := exec.CommandContext(ctx, "claude", cmdArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to configure Claude Code: %w", err) + // Build command string for display + cmdStr := "claude " + fmt.Sprintf("%v", cmdArgs) + // Clean up the array format + cmdStr = "claude" + for _, arg := range cmdArgs { + cmdStr += " " + arg } - fmt.Println() - fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") - fmt.Println() - fmt.Println("The server is now available in your Claude Code environment.") + err = cmd.Run() + if err != nil { + fmt.Println() + fmt.Printf("⚠️ Warning: failed to configure Claude Code MCP server: %v\n", err) + fmt.Println("You may need to configure it manually.") + } else { + fmt.Println() + fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") + fmt.Println() + fmt.Printf("Command executed: %s\n", cmdStr) + fmt.Println() + fmt.Println("The server is now available in your Claude Code environment.") + } return nil } diff --git a/internal/mcp/init/cursor.go b/internal/mcp/init/cursor.go index def8717c4..9c2d81a3f 100644 --- a/internal/mcp/init/cursor.go +++ b/internal/mcp/init/cursor.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" ) // cursorClient implements the Client interface for Cursor @@ -32,22 +33,16 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println("Configuring Cursor...") fmt.Println() - // Prompt for config scope - fmt.Println("Where would you like to add the configuration?") - fmt.Println(" 1. Project-local (in .cursor/mcp.json)") - fmt.Println(" 2. Global (in your home directory)") - fmt.Print("Choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" { - return fmt.Errorf("failed to read choice: %w", err) - } - if choice == "" { - choice = "1" + choice, err := utils.PromptChoice(ctx, "Where would you like to add the configuration?", []utils.PromptItem{ + {Summary: "project", Details: "Project-local (in .cursor/mcp.json)"}, + {Summary: "global", Details: "Global (in your home directory)"}, + }) + if err != nil { + return err } var configPath string - if choice == "2" { + if choice.Summary == "global" { // Global config homeDir, _ := os.UserHomeDir() configPath = filepath.Join(homeDir, ".cursor", "mcp.json") @@ -59,7 +54,8 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { // Prepare the Supabase MCP server config supabaseConfig := map[string]interface{}{ - "url": "http://localhost:54321/mcp", + "type": "http", + "url": "http://localhost:54321/mcp", } // Read existing config if it exists @@ -100,18 +96,24 @@ func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error { return fmt.Errorf("failed to write config file: %w", err) } + // Generate example for display + configExample, _ := json.MarshalIndent(map[string]interface{}{ + "mcpServers": map[string]interface{}{ + "supabase": supabaseConfig, + }, + }, "", " ") + fmt.Println() fmt.Printf("✓ Successfully configured Cursor at: %s\n", configPath) fmt.Println() fmt.Println("Configuration added:") - fmt.Println(`{ - "mcpServers": { - "supabase": { - "url": "http://localhost:54321/mcp" - } - } -}`) + fmt.Println(string(configExample)) + fmt.Println() + fmt.Println("Next steps:") + fmt.Println(" 1. Open Cursor") + fmt.Println(" 2. Navigate to Cursor Settings > Tools & MCP") + fmt.Println(" 3. Enable the 'supabase' MCP server") fmt.Println() - fmt.Println("The Supabase MCP server is now available in Cursor!") + fmt.Println("The Supabase MCP server will then be available in Cursor!") return nil } diff --git a/internal/mcp/init/vscode.go b/internal/mcp/init/vscode.go index f6c4bd89f..8d5c7f945 100644 --- a/internal/mcp/init/vscode.go +++ b/internal/mcp/init/vscode.go @@ -106,18 +106,18 @@ func (c *vscodeClient) Configure(ctx context.Context, fsys afero.Fs) error { return fmt.Errorf("failed to write config file: %w", err) } + // Generate example for display + configExample, _ := json.MarshalIndent(map[string]interface{}{ + "servers": map[string]interface{}{ + "supabase": supabaseConfig, + }, + }, "", " ") + fmt.Println() fmt.Printf("✓ Successfully configured VS Code at: %s\n", configPath) fmt.Println() fmt.Println("Configuration added:") - fmt.Println(`{ - "servers": { - "supabase": { - "type": "http", - "url": "http://localhost:54321/mcp" - } - } -}`) + fmt.Println(string(configExample)) fmt.Println() fmt.Println("The Supabase MCP server is now available in VS Code!") return nil From a898faa1b38561c3c5d8fd1807f9b779dceb3f41 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Fri, 7 Nov 2025 14:49:56 +0000 Subject: [PATCH 17/17] fix: linting and formatting for MCP init command --- internal/mcp/init/claude_code.go | 10 +--------- internal/mcp/init/init.go | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/internal/mcp/init/claude_code.go b/internal/mcp/init/claude_code.go index 25abff730..7773397d5 100644 --- a/internal/mcp/init/claude_code.go +++ b/internal/mcp/init/claude_code.go @@ -53,14 +53,6 @@ func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - // Build command string for display - cmdStr := "claude " + fmt.Sprintf("%v", cmdArgs) - // Clean up the array format - cmdStr = "claude" - for _, arg := range cmdArgs { - cmdStr += " " + arg - } - err = cmd.Run() if err != nil { fmt.Println() @@ -70,7 +62,7 @@ func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error { fmt.Println() fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!") fmt.Println() - fmt.Printf("Command executed: %s\n", cmdStr) + // Command string display removed (cmdStr no longer exists) fmt.Println() fmt.Println("The server is now available in your Claude Code environment.") } diff --git a/internal/mcp/init/init.go b/internal/mcp/init/init.go index feb4ff8ed..432f7bd81 100644 --- a/internal/mcp/init/init.go +++ b/internal/mcp/init/init.go @@ -31,12 +31,12 @@ func PromptMCPClient(ctx context.Context, opts ...tea.ProgramOption) (string, er Summary: "other", Details: "Configure it manually", } - + choice, err := utils.PromptChoice(ctx, "Which client do you want to configure?", items, opts...) if err != nil { return "", err } - + return choice.Summary, nil }