Skip to content

Commit 6d46f17

Browse files
committed
MCP: Configure auth on install
1 parent 5759c24 commit 6d46f17

File tree

5 files changed

+297
-34
lines changed

5 files changed

+297
-34
lines changed

experimental/apps-mcp/cmd/install.go

Lines changed: 250 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@ package mcp
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"os"
8+
"slices"
9+
"strings"
710
"time"
811

912
"github.com/databricks/cli/experimental/apps-mcp/lib/agents"
13+
"github.com/databricks/cli/experimental/apps-mcp/lib/middlewares"
1014
"github.com/databricks/cli/libs/cmdio"
15+
"github.com/databricks/cli/libs/databrickscfg/profile"
16+
"github.com/databricks/cli/libs/env"
17+
"github.com/databricks/databricks-sdk-go"
18+
"github.com/databricks/databricks-sdk-go/config"
19+
"github.com/databricks/databricks-sdk-go/httpclient"
20+
"github.com/databricks/databricks-sdk-go/service/sql"
1121
"github.com/fatih/color"
22+
"github.com/manifoldco/promptui"
1223
"github.com/spf13/cobra"
1324
)
1425

@@ -18,14 +29,20 @@ func newInstallCmd() *cobra.Command {
1829
Short: "Install the Apps MCP server in coding agents",
1930
Long: `Install the Databricks Apps MCP server in coding agents like Claude Code and Cursor.`,
2031
RunE: func(cmd *cobra.Command, args []string) error {
21-
return runInstall(cmd.Context())
32+
return runInstall(cmd)
2233
},
2334
}
2435

36+
cmd.Flags().StringP("profile", "p", "", "~/.databrickscfg profile")
37+
cmd.RegisterFlagCompletionFunc("profile", profile.ProfileCompletion)
38+
cmd.Flags().StringP("warehouse-id", "w", "", "Databricks SQL warehouse ID")
39+
cmd.Flags().StringSliceP("agent", "a", []string{}, "Agents to install the MCP server for (valid values: claude, cursor)")
40+
2541
return cmd
2642
}
2743

28-
func runInstall(ctx context.Context) error {
44+
func runInstall(cmd *cobra.Command) error {
45+
ctx := cmd.Context()
2946
cmdio.LogString(ctx, "")
3047
green := color.New(color.FgGreen).SprintFunc()
3148
cmdio.LogString(ctx, " "+green("[")+"████████"+green("]")+" Databricks Experimental Apps MCP")
@@ -39,18 +56,57 @@ func runInstall(ctx context.Context) error {
3956
cmdio.LogString(ctx, yellow("╚════════════════════════════════════════════════════════════════╝"))
4057
cmdio.LogString(ctx, "")
4158

42-
cmdio.LogString(ctx, "Which coding agents would you like to install the MCP server for?")
59+
// Check for profile configuration
60+
selectedProfile, err := selectProfile(cmd)
61+
if err != nil {
62+
return err
63+
}
64+
4365
cmdio.LogString(ctx, "")
66+
cmdio.LogString(ctx, fmt.Sprintf("Using profile: %s (%s)", color.CyanString(selectedProfile.Name), selectedProfile.Host))
4467

45-
anySuccess := false
68+
warehouse, err := selectAndValidateWarehouse(ctx, cmd.Flag("warehouse-id").Value.String(), selectedProfile)
69+
if err != nil {
70+
return err
71+
}
72+
cmdio.LogString(ctx, fmt.Sprintf("Using warehouse: %s (%s)", color.CyanString(warehouse.Name), warehouse.Id))
73+
cmdio.LogString(ctx, "")
4674

47-
ans, err := cmdio.AskSelect(ctx, "Install for Claude Code?", []string{"yes", "no"})
75+
// Check if --agent flag is set
76+
requestedAgents, err := cmd.Flags().GetStringSlice("agent")
4877
if err != nil {
4978
return err
5079
}
51-
if ans == "yes" {
80+
81+
// Normalize and validate agent names
82+
for i, agent := range requestedAgents {
83+
agent = strings.TrimSpace(strings.ToLower(agent))
84+
requestedAgents[i] = agent
85+
if agent != "" && agent != "claude" && agent != "cursor" {
86+
return fmt.Errorf("invalid agent %q. Valid agents are: claude, cursor", agent)
87+
}
88+
}
89+
90+
anySuccess := false
91+
92+
// Install for Claude Code
93+
installClaude := false
94+
if len(requestedAgents) > 0 {
95+
installClaude = slices.Contains(requestedAgents, "claude")
96+
} else {
97+
// Prompt the user
98+
cmdio.LogString(ctx, "Which coding agents would you like to install the MCP server for?")
99+
cmdio.LogString(ctx, "")
100+
ans, err := cmdio.AskSelect(ctx, "Install for Claude Code?", []string{"yes", "no"})
101+
if err != nil {
102+
return err
103+
}
104+
installClaude = ans == "yes"
105+
}
106+
107+
if installClaude {
52108
fmt.Fprint(os.Stderr, "Installing MCP server for Claude Code...")
53-
if err := agents.InstallClaude(); err != nil {
109+
if err := agents.InstallClaude(selectedProfile, warehouse.Id); err != nil {
54110
fmt.Fprint(os.Stderr, "\r"+color.YellowString("⊘ Skipped Claude Code: "+err.Error())+"\n")
55111
} else {
56112
fmt.Fprint(os.Stderr, "\r"+color.GreenString("✓ Installed for Claude Code")+" \n")
@@ -59,13 +115,22 @@ func runInstall(ctx context.Context) error {
59115
cmdio.LogString(ctx, "")
60116
}
61117

62-
ans, err = cmdio.AskSelect(ctx, "Install for Cursor?", []string{"yes", "no"})
63-
if err != nil {
64-
return err
118+
// Install for Cursor
119+
installCursor := false
120+
if len(requestedAgents) > 0 {
121+
installCursor = slices.Contains(requestedAgents, "cursor")
122+
} else {
123+
// Prompt the user
124+
ans, err := cmdio.AskSelect(ctx, "Install for Cursor?", []string{"yes", "no"})
125+
if err != nil {
126+
return err
127+
}
128+
installCursor = ans == "yes"
65129
}
66-
if ans == "yes" {
130+
131+
if installCursor {
67132
fmt.Fprint(os.Stderr, "Installing MCP server for Cursor...")
68-
if err := agents.InstallCursor(); err != nil {
133+
if err := agents.InstallCursor(selectedProfile, warehouse.Id); err != nil {
69134
fmt.Fprint(os.Stderr, "\r"+color.YellowString("⊘ Skipped Cursor: "+err.Error())+"\n")
70135
} else {
71136
// Brief delay so users see the "Installing..." message before it's replaced
@@ -76,14 +141,17 @@ func runInstall(ctx context.Context) error {
76141
cmdio.LogString(ctx, "")
77142
}
78143

79-
ans, err = cmdio.AskSelect(ctx, "Show manual installation instructions for other agents?", []string{"yes", "no"})
80-
if err != nil {
81-
return err
82-
}
83-
if ans == "yes" {
84-
if err := agents.ShowCustomInstructions(ctx); err != nil {
144+
// Only show custom instructions if no agents were specified or installed
145+
if len(requestedAgents) == 0 {
146+
ans, err := cmdio.AskSelect(ctx, "Show manual installation instructions for other agents?", []string{"yes", "no"})
147+
if err != nil {
85148
return err
86149
}
150+
if ans == "yes" {
151+
if err := agents.ShowCustomInstructions(ctx, selectedProfile, warehouse.Id); err != nil {
152+
return err
153+
}
154+
}
87155
}
88156

89157
if anySuccess {
@@ -95,3 +163,167 @@ func runInstall(ctx context.Context) error {
95163

96164
return nil
97165
}
166+
167+
func selectAndValidateWarehouse(ctx context.Context, warehouseIdFlag string, selectedProfile *profile.Profile) (*sql.EndpointInfo, error) {
168+
w, err := databricks.NewWorkspaceClient(&databricks.Config{
169+
Profile: selectedProfile.Name,
170+
})
171+
if err != nil {
172+
return nil, err
173+
}
174+
175+
var warehouse *sql.EndpointInfo
176+
if warehouseIdFlag != "" {
177+
warehouseResponse, err := w.Warehouses.Get(ctx, sql.GetWarehouseRequest{
178+
Id: warehouseIdFlag,
179+
})
180+
if err != nil {
181+
return nil, fmt.Errorf("get warehouse: %w", err)
182+
}
183+
warehouse = &sql.EndpointInfo{
184+
Id: warehouseResponse.Id,
185+
Name: warehouseResponse.Name,
186+
State: warehouseResponse.State,
187+
}
188+
} else {
189+
// Auto-detect warehouse
190+
191+
clientCfg, err := config.HTTPClientConfigFromConfig(w.Config)
192+
if err != nil {
193+
return nil, fmt.Errorf("failed to create HTTP client config: %w", err)
194+
}
195+
apiClient := httpclient.NewApiClient(clientCfg)
196+
warehouse, err = middlewares.GetDefaultWarehouse(ctx, apiClient)
197+
if err != nil {
198+
return nil, err
199+
}
200+
}
201+
202+
if warehouse == nil {
203+
return nil, errors.New("no warehouse found")
204+
}
205+
206+
// Validate warehouse connection with a simple query
207+
_, err = w.StatementExecution.ExecuteAndWait(ctx, sql.ExecuteStatementRequest{
208+
WarehouseId: warehouse.Id,
209+
Statement: "SELECT 1",
210+
WaitTimeout: "30s",
211+
})
212+
if err != nil {
213+
return nil, fmt.Errorf("failed to validate warehouse connection: %w", err)
214+
}
215+
216+
return warehouse, nil
217+
}
218+
219+
// selectProfile checks if a profile is available and prompts the user to select one if needed.
220+
func selectProfile(cmd *cobra.Command) (*profile.Profile, error) {
221+
ctx := cmd.Context()
222+
profiler := profile.GetProfiler(ctx)
223+
224+
// Load all workspace profiles
225+
profiles, err := profiler.LoadProfiles(ctx, profile.MatchWorkspaceProfiles)
226+
if err != nil {
227+
return nil, fmt.Errorf("failed to load profiles: %w", err)
228+
}
229+
230+
// If no profiles are available, ask the user to login
231+
if len(profiles) == 0 {
232+
cmdio.LogString(ctx, color.RedString("No Databricks profiles found."))
233+
cmdio.LogString(ctx, "")
234+
cmdio.LogString(ctx, "To authenticate, please run:")
235+
cmdio.LogString(ctx, " "+color.YellowString("databricks auth login --host <workspace-url>"))
236+
cmdio.LogString(ctx, "")
237+
cmdio.LogString(ctx, "Then run this command again.")
238+
return nil, errors.New("no profiles configured")
239+
}
240+
241+
// Check if --profile flag is set
242+
profileFlag := cmd.Flag("profile")
243+
if profileFlag != nil && profileFlag.Value.String() != "" {
244+
requestedProfile := profileFlag.Value.String()
245+
246+
// Find the requested profile
247+
var found *profile.Profile
248+
for i := range profiles {
249+
if profiles[i].Name == requestedProfile {
250+
found = &profiles[i]
251+
break
252+
}
253+
}
254+
255+
if found == nil {
256+
return nil, fmt.Errorf("profile %q not found in ~/.databrickscfg. Run `databricks auth login <workspace-url> -p %s` to create this profile and then run this command again", requestedProfile, requestedProfile)
257+
}
258+
259+
return found, nil
260+
}
261+
262+
// Get the current profile name from environment variable
263+
currentProfileName := env.Get(ctx, "DATABRICKS_CONFIG_PROFILE")
264+
if currentProfileName == "" {
265+
currentProfileName = "DEFAULT"
266+
}
267+
268+
// Find the current profile in the list
269+
var currentProfile *profile.Profile
270+
for i := range profiles {
271+
if profiles[i].Name == currentProfileName {
272+
currentProfile = &profiles[i]
273+
break
274+
}
275+
}
276+
277+
// If a profile is already selected, show it and ask if they want to use it
278+
if currentProfile != nil {
279+
cmdio.LogString(ctx, "Current Databricks profile:")
280+
cmdio.LogString(ctx, " Name: "+color.CyanString(currentProfile.Name))
281+
cmdio.LogString(ctx, " Host: "+color.CyanString(currentProfile.Host))
282+
cmdio.LogString(ctx, "")
283+
284+
ans, err := cmdio.AskSelect(ctx, "Use this profile?", []string{"yes", "no"})
285+
if err != nil {
286+
return nil, err
287+
}
288+
289+
if ans == "yes" {
290+
return currentProfile, nil
291+
}
292+
}
293+
294+
// User wants to select a different profile, or no current profile set
295+
// Show all available profiles for selection
296+
if len(profiles) == 1 {
297+
// Only one profile available, use it
298+
selectedProfile := profiles[0]
299+
cmdio.LogString(ctx, fmt.Sprintf("Using profile: %s (%s)", color.CyanString(selectedProfile.Name), selectedProfile.Host))
300+
cmdio.LogString(ctx, "")
301+
cmdio.LogString(ctx, "Set this profile by running:")
302+
cmdio.LogString(ctx, " "+color.YellowString("export DATABRICKS_CONFIG_PROFILE="+selectedProfile.Name))
303+
return &selectedProfile, nil
304+
}
305+
306+
cmdio.LogString(ctx, "Which Databricks profile would you like to use with the MCP server?")
307+
cmdio.LogString(ctx, "(You can change the profile later by running this install command again)")
308+
cmdio.LogString(ctx, "")
309+
310+
// Multiple profiles available, let the user select
311+
i, _, err := cmdio.RunSelect(ctx, &promptui.Select{
312+
Label: "Select a Databricks profile",
313+
Items: profiles,
314+
Searcher: profiles.SearchCaseInsensitive,
315+
StartInSearchMode: true,
316+
Templates: &promptui.SelectTemplates{
317+
Label: "{{ . | faint }}",
318+
Active: `{{.Name | bold}} ({{.Host|faint}})`,
319+
Inactive: `{{.Name}} ({{.Host}})`,
320+
Selected: `{{ "Selected profile" | faint }}: {{ .Name | bold }}`,
321+
},
322+
})
323+
if err != nil {
324+
return nil, err
325+
}
326+
327+
selectedProfile := profiles[i]
328+
return &selectedProfile, nil
329+
}

experimental/apps-mcp/lib/agents/claude.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"os"
77
"os/exec"
8+
9+
"github.com/databricks/cli/libs/databrickscfg/profile"
810
)
911

1012
// DetectClaude checks if Claude Code CLI is installed and available on PATH.
@@ -14,7 +16,7 @@ func DetectClaude() bool {
1416
}
1517

1618
// InstallClaude installs the Databricks MCP server in Claude Code.
17-
func InstallClaude() error {
19+
func InstallClaude(profile *profile.Profile, warehouseID string) error {
1820
if !DetectClaude() {
1921
return errors.New("claude Code CLI is not installed or not on PATH\n\nPlease install Claude Code and ensure 'claude' is available on your system PATH.\nFor installation instructions, visit: https://docs.anthropic.com/en/docs/claude-code")
2022
}
@@ -27,12 +29,19 @@ func InstallClaude() error {
2729
removeCmd := exec.Command("claude", "mcp", "remove", "--scope", "user", "databricks-mcp")
2830
_ = removeCmd.Run()
2931

30-
cmd := exec.Command("claude", "mcp", "add",
32+
args := []string{
33+
"mcp", "add",
3134
"--scope", "user",
3235
"--transport", "stdio",
3336
"databricks-mcp",
34-
"--",
35-
databricksPath, "experimental", "apps-mcp")
37+
"--env", "DATABRICKS_CONFIG_PROFILE=" + profile.Name,
38+
"--env", "DATABRICKS_HOST=" + profile.Host,
39+
"--env", "DATABRICKS_WAREHOUSE_ID=" + warehouseID,
40+
}
41+
42+
args = append(args, "--", databricksPath, "experimental", "apps-mcp")
43+
44+
cmd := exec.Command("claude", args...)
3645

3746
output, err := cmd.CombinedOutput()
3847
if err != nil {

0 commit comments

Comments
 (0)