Skip to content

Commit ebded41

Browse files
committed
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
1 parent 0ee3029 commit ebded41

File tree

6 files changed

+368
-327
lines changed

6 files changed

+368
-327
lines changed

internal/mcp/init/claude_code.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package mcpinit
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
9+
"github.com/spf13/afero"
10+
)
11+
12+
// claudeCodeClient implements the Client interface for Claude Code
13+
type claudeCodeClient struct {
14+
baseClient
15+
}
16+
17+
func newClaudeCodeClient() *claudeCodeClient {
18+
return &claudeCodeClient{
19+
baseClient: baseClient{
20+
name: "claude-code",
21+
displayName: "Claude Code",
22+
installInstructions: "npm install -g @anthropic-ai/claude-cli",
23+
checkInstalled: func() bool {
24+
return commandExists("claude")
25+
},
26+
},
27+
}
28+
}
29+
30+
func (c *claudeCodeClient) Configure(ctx context.Context, fsys afero.Fs) error {
31+
fmt.Println("Adding Supabase MCP server to Claude Code...")
32+
fmt.Println()
33+
34+
// Build the claude mcp add command
35+
// #nosec G204 -- command and URL are controlled constants
36+
cmd := exec.CommandContext(ctx, "claude", "mcp", "add", "--transport", "http", "supabase", "https://mcp.supabase.com/mcp")
37+
38+
cmd.Stdout = os.Stdout
39+
cmd.Stderr = os.Stderr
40+
41+
if err := cmd.Run(); err != nil {
42+
return fmt.Errorf("failed to configure Claude Code: %w", err)
43+
}
44+
45+
fmt.Println()
46+
fmt.Println("✓ Successfully added Supabase MCP server to Claude Code!")
47+
fmt.Println()
48+
fmt.Println("The server is now available in your Claude Code environment.")
49+
return nil
50+
}

internal/mcp/init/client.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package mcpinit
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
7+
"github.com/spf13/afero"
8+
)
9+
10+
// Client represents an MCP client that can be configured
11+
type Client interface {
12+
// Name returns the client identifier (e.g., "claude-code")
13+
Name() string
14+
15+
// DisplayName returns the human-readable name (e.g., "Claude Code")
16+
DisplayName() string
17+
18+
// IsInstalled checks if the client is installed on the system
19+
IsInstalled() bool
20+
21+
// InstallInstructions returns instructions for installing the client
22+
InstallInstructions() string
23+
24+
// Configure performs the configuration for this client
25+
Configure(ctx context.Context, fsys afero.Fs) error
26+
}
27+
28+
// baseClient provides default implementations for the Client interface
29+
type baseClient struct {
30+
name string
31+
displayName string
32+
installInstructions string
33+
checkInstalled func() bool
34+
}
35+
36+
func (b *baseClient) Name() string {
37+
return b.name
38+
}
39+
40+
func (b *baseClient) DisplayName() string {
41+
return b.displayName
42+
}
43+
44+
func (b *baseClient) IsInstalled() bool {
45+
if b.checkInstalled != nil {
46+
return b.checkInstalled()
47+
}
48+
return false
49+
}
50+
51+
func (b *baseClient) InstallInstructions() string {
52+
return b.installInstructions
53+
}
54+
55+
// commandExists checks if a command-line tool is available
56+
func commandExists(command string) bool {
57+
_, err := exec.LookPath(command)
58+
return err == nil
59+
}

internal/mcp/init/cursor.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package mcpinit
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/spf13/afero"
11+
)
12+
13+
// cursorClient implements the Client interface for Cursor
14+
type cursorClient struct {
15+
baseClient
16+
}
17+
18+
func newCursorClient() *cursorClient {
19+
return &cursorClient{
20+
baseClient: baseClient{
21+
name: "cursor",
22+
displayName: "Cursor",
23+
installInstructions: "Download from https://cursor.sh",
24+
checkInstalled: func() bool {
25+
return commandExists("cursor") || appExists("Cursor")
26+
},
27+
},
28+
}
29+
}
30+
31+
func (c *cursorClient) Configure(ctx context.Context, fsys afero.Fs) error {
32+
fmt.Println("Configuring Cursor...")
33+
fmt.Println()
34+
35+
// Prompt for config scope
36+
fmt.Println("Where would you like to add the configuration?")
37+
fmt.Println(" 1. Project-local (in .cursor/mcp.json)")
38+
fmt.Println(" 2. Global (in your home directory)")
39+
fmt.Print("Choice [1]: ")
40+
41+
var choice string
42+
if _, err := fmt.Scanln(&choice); err != nil && err.Error() != "unexpected newline" {
43+
return fmt.Errorf("failed to read choice: %w", err)
44+
}
45+
if choice == "" {
46+
choice = "1"
47+
}
48+
49+
var configPath string
50+
if choice == "2" {
51+
// Global config
52+
homeDir, _ := os.UserHomeDir()
53+
configPath = filepath.Join(homeDir, ".cursor", "mcp.json")
54+
} else {
55+
// Project-local config
56+
cwd, _ := os.Getwd()
57+
configPath = filepath.Join(cwd, ".cursor", "mcp.json")
58+
}
59+
60+
// Prepare the Supabase MCP server config
61+
supabaseConfig := map[string]interface{}{
62+
"url": "https://mcp.supabase.com/mcp",
63+
}
64+
65+
// Read existing config if it exists
66+
var config map[string]interface{}
67+
existingData, err := afero.ReadFile(fsys, configPath)
68+
if err == nil && len(existingData) > 0 {
69+
if err := json.Unmarshal(existingData, &config); err != nil {
70+
// If existing file is invalid JSON, start fresh
71+
config = make(map[string]interface{})
72+
}
73+
} else {
74+
config = make(map[string]interface{})
75+
}
76+
77+
// Ensure mcpServers exists
78+
mcpServers, ok := config["mcpServers"].(map[string]interface{})
79+
if !ok {
80+
mcpServers = make(map[string]interface{})
81+
config["mcpServers"] = mcpServers
82+
}
83+
84+
// Add or update Supabase server
85+
mcpServers["supabase"] = supabaseConfig
86+
87+
// Ensure directory exists
88+
configDir := filepath.Dir(configPath)
89+
if err := fsys.MkdirAll(configDir, 0755); err != nil {
90+
return fmt.Errorf("failed to create config directory: %w", err)
91+
}
92+
93+
// Write config
94+
configJSON, err := json.MarshalIndent(config, "", " ")
95+
if err != nil {
96+
return fmt.Errorf("failed to marshal config: %w", err)
97+
}
98+
99+
if err := afero.WriteFile(fsys, configPath, configJSON, 0644); err != nil {
100+
return fmt.Errorf("failed to write config file: %w", err)
101+
}
102+
103+
fmt.Println()
104+
fmt.Printf("✓ Successfully configured Cursor at: %s\n", configPath)
105+
fmt.Println()
106+
fmt.Println("Configuration added:")
107+
fmt.Println(`{
108+
"mcpServers": {
109+
"supabase": {
110+
"url": "https://mcp.supabase.com/mcp"
111+
}
112+
}
113+
}`)
114+
fmt.Println()
115+
fmt.Println("The Supabase MCP server is now available in Cursor!")
116+
return nil
117+
}

0 commit comments

Comments
 (0)