Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -43,7 +44,30 @@ var (
createIntellijSettings = nil
}
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
return _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams)

// Run core project init first
if err := _init.Run(ctx, fsys, createVscodeSettings, createIntellijSettings, initParams); err != nil {
return err
}

// 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) {
fmt.Println("Finished " + utils.Aqua("supabase init") + ".")
Expand Down
18 changes: 18 additions & 0 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cmd

import (
"github.com/spf13/cobra"
)

var (
mcpCmd = &cobra.Command{
GroupID: groupQuickStart,
Use: "mcp",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we plan to add other sub commands under supabase mcp? If not, I'd probably add new flags to supabase init instead.

We currently prompt users to create deno config when running supabase init which is logically similar to vscode config for mcp.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have plans to extend the mcp command to a dev env for building mcp servers
https://www.notion.so/supabase/Supabase-CLI-Integration-635fe64201f944c1a97856cf83c565c9?source=copy_link

The Notion page is still under construction so please disregard the exact commands stated but the general idea is there.

But since these plans are for developing mcp servers using Supabase and not to configure the SUpabase MCP server using our CLI, I agree that a flag to supabase init would suit this use case better.

Something like supabase init --mcp-client=claude-code?

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)
}
39 changes: 39 additions & 0 deletions cmd/mcp_init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cmd

import (
"github.com/spf13/afero"
"github.com/spf13/cobra"
mcpinit "github.com/supabase/cli/internal/mcp/init"
)

var (
mcpInitCmd = &cobra.Command{
Use: "init",
Short: "Configure Supabase MCP server for AI assistant clients",
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, 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.

Examples:
# Auto-detect and configure installed clients
supabase mcp init

# Configure a specific client
supabase mcp init --client claude-code
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)
},
}
)

func init() {
mcpInitCmd.Flags().StringP("client", "c", "", "Target specific client (e.g., claude-code)")
mcpCmd.AddCommand(mcpInitCmd)
}
219 changes: 219 additions & 0 deletions internal/mcp/init/README.md
Original file line number Diff line number Diff line change
@@ -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.
70 changes: 70 additions & 0 deletions internal/mcp/init/claude_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package mcpinit

import (
"context"
"fmt"
"os"
"os/exec"

"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
)

// 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("Configuring Claude Code...")
fmt.Println()

// 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

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()
// Command string display removed (cmdStr no longer exists)
fmt.Println()
fmt.Println("The server is now available in your Claude Code environment.")
}
return nil
}
Loading
Loading