Skip to content

Commit 1e978fd

Browse files
Add an experimental MCP server (#3907)
## Changes This adds an experimental MCP server: ``` experimental/aitools/ ├── agents/ # Agent detection and installation (Claude Code, Cursor, custom) ├── auth/ # Authentication checking ├── tools/ # MCP tools: init_project, add_project_resource, analyze_project, explore, invoke_databricks_cli │ ├── resources/ # Resource types for add_project_resource: apps, dashboards, jobs, pipelines │ └── prompts/ # AI-facing prompts as .tmpl files ├── install.go # Installation command ├── server.go # MCP server implementation └── uninstall.go # Uninstallation command ``` ``` $ databricks experimental aitools install [████████] Databricks Experimental AI agent skills & MCP [██▌ ▐██] [████████] AI-powered Databricks development and exploration ╔════════════════════════════════════════════════════════════════╗ ║ ⚠️ EXPERIMENTAL: This command may change in future versions ║ ╚════════════════════════════════════════════════════════════════╝ Which coding agents would you like to install the MCP server for? Install for Claude Code? [Use arrows to move] > yes no ✓ Installed for Cursor Install for Cursor? [Use arrows to move] > yes no ✓ Installed for Claude Code Show manual installation instructions for other agents? [Use arrows to move] > yes no You can now use your coding agent to interact with Databricks. Try asking: 'Create a new Databricks project with a job or an app' ``` --------- Co-authored-by: Claude <[email protected]>
1 parent 87c9e86 commit 1e978fd

40 files changed

+2792
-1
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/acceptance/pipelines/ @jefferycheng1 @kanterov @lennartkats-db
55
/cmd/pipelines/ @jefferycheng1 @kanterov @lennartkats-db
66
/cmd/labs/ @alexott @nfx
7+
/experimental/aitools/ @databricks/eng-app-devex @lennartkats-db
78
/cmd/workspace/apps/ @databricks/eng-app-devex
89
/libs/apps/ @databricks/eng-app-devex
910
/acceptance/apps/ @databricks/eng-app-devex

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
default: checks fmt lint
22

33
# gotestsum: when go test args are used with --rerun-fails the list of packages to test must be specified by the --packages flag
4-
PACKAGES=--packages "./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... ./experimental/ssh/... ."
4+
PACKAGES=--packages "./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... ./experimental/aitools/... ./experimental/ssh/... ."
55

66
GO_TOOL ?= go tool -modfile=tools/go.mod
77
GOTESTSUM_FORMAT ?= pkgname-and-test-fails

cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/databricks/cli/cmd/auth"
1313
"github.com/databricks/cli/cmd/bundle"
1414
"github.com/databricks/cli/cmd/configure"
15+
"github.com/databricks/cli/cmd/experimental"
1516
"github.com/databricks/cli/cmd/fs"
1617
"github.com/databricks/cli/cmd/labs"
1718
"github.com/databricks/cli/cmd/pipelines"
@@ -70,6 +71,7 @@ func New(ctx context.Context) *cobra.Command {
7071
cli.AddCommand(api.New())
7172
cli.AddCommand(auth.New())
7273
cli.AddCommand(bundle.New())
74+
cli.AddCommand(experimental.New())
7375
cli.AddCommand(psql.New())
7476
cli.AddCommand(configure.New())
7577
cli.AddCommand(fs.New())

cmd/experimental/experimental.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package experimental
2+
3+
import (
4+
"github.com/databricks/cli/experimental/aitools"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
func New() *cobra.Command {
9+
cmd := &cobra.Command{
10+
Use: "experimental",
11+
Short: "Experimental commands that may change in future versions",
12+
Hidden: true,
13+
Long: `Experimental commands that may change in future versions.
14+
15+
╔════════════════════════════════════════════════════════════════╗
16+
║ ⚠️ EXPERIMENTAL: These commands may change in future versions ║
17+
╚════════════════════════════════════════════════════════════════╝
18+
19+
These commands provide early access to new features that are still under
20+
development. They may change or be removed in future versions without notice.`,
21+
}
22+
23+
cmd.AddCommand(aitools.New())
24+
25+
return cmd
26+
}

experimental/aitools/DESIGN.md

Lines changed: 227 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package agents
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os/exec"
7+
8+
"github.com/databricks/cli/experimental/aitools/tools"
9+
)
10+
11+
// DetectClaude checks if Claude Code CLI is installed and available on PATH.
12+
func DetectClaude() bool {
13+
_, err := exec.LookPath("claude")
14+
return err == nil
15+
}
16+
17+
// InstallClaude installs the Databricks MCP server in Claude Code.
18+
func InstallClaude() error {
19+
if !DetectClaude() {
20+
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")
21+
}
22+
23+
databricksPath, err := tools.GetDatabricksPath()
24+
if err != nil {
25+
return err
26+
}
27+
28+
removeCmd := exec.Command("claude", "mcp", "remove", "databricks-aitools")
29+
_ = removeCmd.Run()
30+
31+
cmd := exec.Command("claude", "mcp", "add",
32+
"--transport", "stdio",
33+
"databricks-aitools",
34+
"--",
35+
databricksPath, "experimental", "aitools", "server")
36+
37+
output, err := cmd.CombinedOutput()
38+
if err != nil {
39+
return fmt.Errorf("failed to install MCP server in Claude Code: %w\nOutput: %s", err, string(output))
40+
}
41+
42+
return nil
43+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package agents
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
)
10+
11+
type cursorConfig struct {
12+
McpServers map[string]mcpServer `json:"mcpServers"`
13+
}
14+
15+
type mcpServer struct {
16+
Command string `json:"command"`
17+
Args []string `json:"args,omitempty"`
18+
Env map[string]string `json:"env,omitempty"`
19+
}
20+
21+
func getCursorConfigPath() (string, error) {
22+
if runtime.GOOS == "windows" {
23+
userProfile := os.Getenv("USERPROFILE")
24+
if userProfile == "" {
25+
return "", os.ErrNotExist
26+
}
27+
return filepath.Join(userProfile, ".cursor", "mcp.json"), nil
28+
}
29+
30+
home, err := os.UserHomeDir()
31+
if err != nil {
32+
return "", err
33+
}
34+
return filepath.Join(home, ".cursor", "mcp.json"), nil
35+
}
36+
37+
// DetectCursor checks if Cursor is installed by looking for its config directory.
38+
func DetectCursor() bool {
39+
configPath, err := getCursorConfigPath()
40+
if err != nil {
41+
return false
42+
}
43+
// Check if the .cursor directory exists (not just the mcp.json file)
44+
cursorDir := filepath.Dir(configPath)
45+
_, err = os.Stat(cursorDir)
46+
return err == nil
47+
}
48+
49+
// InstallCursor installs the Databricks MCP server in Cursor.
50+
func InstallCursor() error {
51+
configPath, err := getCursorConfigPath()
52+
if err != nil {
53+
return fmt.Errorf("failed to determine Cursor config path: %w", err)
54+
}
55+
56+
// Check if .cursor directory exists (not the file, we'll create that if needed)
57+
cursorDir := filepath.Dir(configPath)
58+
if _, err := os.Stat(cursorDir); err != nil {
59+
return fmt.Errorf("cursor directory not found at: %s\n\nPlease install Cursor from: https://cursor.sh", cursorDir)
60+
}
61+
62+
// Read existing config
63+
var config cursorConfig
64+
data, err := os.ReadFile(configPath)
65+
if err != nil {
66+
// If file doesn't exist or can't be read, start with empty config
67+
config = cursorConfig{
68+
McpServers: make(map[string]mcpServer),
69+
}
70+
} else {
71+
if err := json.Unmarshal(data, &config); err != nil {
72+
return fmt.Errorf("failed to parse Cursor config: %w", err)
73+
}
74+
if config.McpServers == nil {
75+
config.McpServers = make(map[string]mcpServer)
76+
}
77+
}
78+
79+
// Add or update the Databricks MCP server entry
80+
config.McpServers["databricks"] = mcpServer{
81+
Command: "databricks",
82+
Args: []string{"experimental", "aitools", "server"},
83+
}
84+
85+
// Write back to file with pretty printing
86+
updatedData, err := json.MarshalIndent(config, "", " ")
87+
if err != nil {
88+
return fmt.Errorf("failed to marshal Cursor config: %w", err)
89+
}
90+
91+
if err := os.WriteFile(configPath, updatedData, 0o644); err != nil {
92+
return fmt.Errorf("failed to write Cursor config: %w", err)
93+
}
94+
95+
return nil
96+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package agents
2+
3+
import (
4+
"context"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
)
8+
9+
// ShowCustomInstructions displays instructions for manually installing the MCP server.
10+
func ShowCustomInstructions(ctx context.Context) error {
11+
instructions := `
12+
To install the Databricks CLI MCP server in your coding agent:
13+
14+
1. Add a new MCP server to your coding agent's configuration
15+
2. Set the command to: databricks experimental aitools server
16+
3. No environment variables or additional configuration needed
17+
18+
Example MCP server configuration:
19+
{
20+
"mcpServers": {
21+
"databricks": {
22+
"command": "databricks",
23+
"args": ["experimental", "aitools", "server"]
24+
}
25+
}
26+
}
27+
`
28+
cmdio.LogString(ctx, instructions)
29+
30+
_, err := cmdio.Ask(ctx, "Press Enter to continue", "")
31+
if err != nil {
32+
return err
33+
}
34+
cmdio.LogString(ctx, "")
35+
return nil
36+
}

experimental/aitools/aitools.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package aitools
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func New() *cobra.Command {
8+
cmd := &cobra.Command{
9+
Use: "aitools",
10+
Short: "Manage AI agent skills and MCP server for coding agents",
11+
Hidden: true,
12+
Long: `Manage Databricks AI agent skills and MCP server.
13+
14+
This provides AI agents like Claude Code and Cursor with capabilities to interact
15+
with Databricks, create projects, deploy bundles, run jobs, etc.
16+
17+
╔════════════════════════════════════════════════════════════════╗
18+
║ ⚠️ EXPERIMENTAL: This command may change in future versions ║
19+
╚════════════════════════════════════════════════════════════════╝
20+
21+
Common workflows:
22+
databricks experimental aitools install # Install in Claude Code or Cursor
23+
databricks experimental aitools server # Start server (used by agents)
24+
25+
Online documentation: https://docs.databricks.com/dev-tools/cli/aitools.html`,
26+
}
27+
28+
cmd.AddCommand(newInstallCmd())
29+
cmd.AddCommand(newServerCmd())
30+
cmd.AddCommand(newUninstallCmd())
31+
cmd.AddCommand(newToolCmd())
32+
33+
return cmd
34+
}

experimental/aitools/auth/check.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"os"
8+
"sync"
9+
10+
"github.com/databricks/cli/experimental/aitools/tools/prompts"
11+
"github.com/databricks/databricks-sdk-go"
12+
"github.com/databricks/databricks-sdk-go/apierr"
13+
"github.com/databricks/databricks-sdk-go/config"
14+
"github.com/databricks/databricks-sdk-go/service/jobs"
15+
)
16+
17+
var (
18+
authCheckOnce sync.Once
19+
authCheckResult error
20+
)
21+
22+
// CheckAuthentication checks if the user is authenticated to a Databricks workspace.
23+
// It caches the result so the check only runs once per process.
24+
func CheckAuthentication(ctx context.Context) error {
25+
authCheckOnce.Do(func() {
26+
authCheckResult = checkAuth(ctx)
27+
})
28+
return authCheckResult
29+
}
30+
31+
func checkAuth(ctx context.Context) error {
32+
if os.Getenv("DATABRICKS_MCP_SKIP_AUTH_CHECK") == "1" {
33+
return nil
34+
}
35+
36+
w, err := databricks.NewWorkspaceClient()
37+
if err != nil {
38+
return wrapAuthError(err)
39+
}
40+
41+
// Use Jobs API for auth check (fast). Expected: 404 (authenticated), 401/403 (not authenticated).
42+
_, err = w.Jobs.Get(ctx, jobs.GetJobRequest{JobId: 999999999})
43+
if err == nil {
44+
return nil
45+
}
46+
47+
var apiErr *apierr.APIError
48+
if errors.As(err, &apiErr) {
49+
switch apiErr.StatusCode {
50+
case http.StatusNotFound:
51+
return nil
52+
case http.StatusUnauthorized, http.StatusForbidden:
53+
return errors.New(prompts.MustExecuteTemplate("auth_error.tmpl", nil))
54+
default:
55+
return nil
56+
}
57+
}
58+
59+
return wrapAuthError(err)
60+
}
61+
62+
func wrapAuthError(err error) error {
63+
if errors.Is(err, config.ErrCannotConfigureDefault) {
64+
return errors.New(prompts.MustExecuteTemplate("auth_error.tmpl", nil))
65+
}
66+
return err
67+
}

0 commit comments

Comments
 (0)