Skip to content

Commit 6ab1806

Browse files
authored
apps MCP: restructure context management (#4073)
## Changes Restructure the MCP context management into three layers to establish separation of concerns. Prompts are not changed here, subject for a separate change. ## Why ### Why separate context layers? The previous implementation mixed different types of guidance in a single template — app-specific validation rules sat alongside TypeScript SDK patterns, making it hard to support new target types (jobs, pipelines) or new templates (Python apps). By separating into L0 (tool descriptions), L1 (universal workflow), L2 (target-specific), and L3 (template-specific), each layer can evolve independently. Adding a new target type now means creating one target_<type>.tmpl file rather than modifying shared templates. ### Why replace first-call middleware with explicit databricks_discover? The middleware approach injected context into whatever tool happened to run first — fragile and non-recoverable if the agent missed it. The explicit databricks_discover tool lets agents request context when they need it, re-call it when switching directories, and get project-aware guidance (detecting apps/jobs from databricks.yml). The tool description itself serves as L0 context, instructing agents to call it first during planning without any middleware magic.
1 parent f5c7f47 commit 6ab1806

File tree

16 files changed

+526
-274
lines changed

16 files changed

+526
-274
lines changed

experimental/apps-mcp/cmd/init_template.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,12 @@ After initialization:
336336
cmdio.LogString(ctx, fileTree)
337337
}
338338

339-
// Try to read and display CLAUDE.md if present
340-
readClaudeMd(ctx, configFile)
339+
// Inject L2 (target-specific guidance for apps)
340+
targetApps := prompts.MustExecuteTemplate("target_apps.tmpl", map[string]any{})
341+
cmdio.LogString(ctx, targetApps)
341342

342-
// Re-inject app-specific guidance
343-
appsContent := prompts.MustExecuteTemplate("apps.tmpl", map[string]any{})
344-
cmdio.LogString(ctx, appsContent)
343+
// Inject L3 (template-specific guidance from CLAUDE.md)
344+
readClaudeMd(ctx, configFile)
345345

346346
return nil
347347
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!-- DO NOT MODIFY: This documentation defines the context management architecture for Databricks MCP -->
2+
# Context Management for Databricks MCP
3+
4+
## Goals
5+
6+
- Universal MCP for any coding agent (Claude, Cursor, etc.)
7+
- Support multiple target types: apps, jobs, pipelines
8+
- Support multiple templates per target type
9+
- Clean separation of context layers
10+
- Detect existing project context automatically
11+
12+
## Context Layers
13+
14+
| Layer | Content | When Injected |
15+
|-------|---------|---------------|
16+
| **L0: Tools** | Databricks MCP tool names and descriptions | Always (MCP protocol) |
17+
| **L1: Flow** | Universal workflow, available tools, CLI patterns | Always (via `databricks_discover`) |
18+
| **L2: Target** | Target-specific: validation, deployment, constraints | When target type detected or after `init-template` |
19+
| **L3: Template** | SDK/language-specific: file structure, commands, patterns | After `init-template`. For existing projects, agent reads CLAUDE.md. |
20+
21+
L0 is implicit - tool descriptions guide agent behavior before any tool is called (e.g., `databricks_discover` description tells agent to call it first during planning).
22+
23+
### Examples
24+
25+
**L1 (universal):** "validate before deploying", "use invoke_databricks_cli for all commands"
26+
27+
**L2 (apps):** app naming constraints, deployment consent requirement, app-specific validation
28+
29+
**L3 (appkit-typescript):** npm scripts, tRPC patterns, useAnalyticsQuery usage, TypeScript import rules
30+
31+
## Flows
32+
33+
### New Project
34+
35+
```
36+
Agent MCP
37+
│ │
38+
├─► databricks_discover │
39+
│ {working_directory: "."} │
40+
│ ├─► Run detectors (nothing found)
41+
│ ├─► Return L1 only
42+
│◄─────────────────────────────┤
43+
│ │
44+
├─► invoke_databricks_cli │
45+
│ ["...", "init-template", ...]
46+
│ ├─► Scaffold project
47+
│ ├─► Return L2[apps] + L3
48+
│◄─────────────────────────────┤
49+
│ │
50+
├─► (agent now has L1 + L2 + L3)
51+
```
52+
53+
### Existing Project
54+
55+
```
56+
Agent MCP
57+
│ │
58+
├─► databricks_discover │
59+
│ {working_directory: "./my-app"}
60+
│ ├─► BundleDetector: found apps + jobs
61+
│ ├─► Return L1 + L2[apps] + L2[jobs]
62+
│◄─────────────────────────────┤
63+
│ │
64+
├─► Read CLAUDE.md naturally │
65+
│ (agent learns L3 itself) │
66+
```
67+
68+
### Combined Bundles
69+
70+
When `databricks.yml` contains multiple resource types (e.g., app + job), all relevant L2 layers are injected together.
71+
72+
## Extensibility
73+
74+
New target types can be added by:
75+
1. Creating `target_<type>.tmpl` in `lib/prompts/`
76+
2. Adding detection logic to recognize the target type from `databricks.yml`
77+
78+
New templates can be added by:
79+
1. Creating template directory with CLAUDE.md (L3 guidance)
80+
2. Adding detection logic to recognize the template from project files
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/databricks/cli/bundle"
9+
"github.com/databricks/cli/bundle/phases"
10+
"github.com/databricks/cli/libs/logdiag"
11+
)
12+
13+
// BundleDetector detects Databricks bundle configuration.
14+
type BundleDetector struct{}
15+
16+
// Detect loads databricks.yml with all includes and extracts target types.
17+
func (d *BundleDetector) Detect(ctx context.Context, workDir string, detected *DetectedContext) error {
18+
bundlePath := filepath.Join(workDir, "databricks.yml")
19+
if _, err := os.Stat(bundlePath); err != nil {
20+
// no bundle file - not an error, just not a bundle project
21+
return nil
22+
}
23+
24+
// use full bundle loading to get all resources including from includes
25+
ctx = logdiag.InitContext(ctx)
26+
b, err := bundle.Load(ctx, workDir)
27+
if err != nil || b == nil {
28+
return nil
29+
}
30+
31+
phases.Load(ctx, b)
32+
if logdiag.HasError(ctx) {
33+
return nil
34+
}
35+
36+
detected.InProject = true
37+
detected.BundleInfo = &BundleInfo{
38+
Name: b.Config.Bundle.Name,
39+
RootDir: workDir,
40+
}
41+
42+
// extract target types from fully loaded resources
43+
if len(b.Config.Resources.Apps) > 0 {
44+
detected.TargetTypes = append(detected.TargetTypes, "apps")
45+
}
46+
if len(b.Config.Resources.Jobs) > 0 {
47+
detected.TargetTypes = append(detected.TargetTypes, "jobs")
48+
}
49+
if len(b.Config.Resources.Pipelines) > 0 {
50+
detected.TargetTypes = append(detected.TargetTypes, "pipelines")
51+
}
52+
53+
return nil
54+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Package detector provides project context detection for Databricks MCP.
2+
package detector
3+
4+
import (
5+
"context"
6+
)
7+
8+
// BundleInfo contains information about a detected Databricks bundle.
9+
type BundleInfo struct {
10+
Name string
11+
Target string
12+
RootDir string
13+
}
14+
15+
// DetectedContext represents the detected project context.
16+
type DetectedContext struct {
17+
InProject bool
18+
TargetTypes []string // ["apps", "jobs"] - supports combined bundles
19+
Template string // "appkit-typescript", "python", etc.
20+
BundleInfo *BundleInfo
21+
Metadata map[string]string
22+
}
23+
24+
// Detector detects project context from a working directory.
25+
type Detector interface {
26+
// Detect examines the working directory and updates the context.
27+
Detect(ctx context.Context, workDir string, detected *DetectedContext) error
28+
}
29+
30+
// Registry manages a collection of detectors.
31+
type Registry struct {
32+
detectors []Detector
33+
}
34+
35+
// NewRegistry creates a new detector registry with default detectors.
36+
func NewRegistry() *Registry {
37+
return &Registry{
38+
detectors: []Detector{
39+
&BundleDetector{},
40+
&TemplateDetector{},
41+
},
42+
}
43+
}
44+
45+
// Detect runs all detectors and returns the combined context.
46+
func (r *Registry) Detect(ctx context.Context, workDir string) *DetectedContext {
47+
detected := &DetectedContext{
48+
Metadata: make(map[string]string),
49+
}
50+
51+
for _, d := range r.detectors {
52+
// ignore errors - detectors should be resilient
53+
_ = d.Detect(ctx, workDir, detected)
54+
}
55+
56+
return detected
57+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package detector_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/databricks/cli/experimental/apps-mcp/lib/detector"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestDetectorRegistry_EmptyDir(t *testing.T) {
15+
dir := t.TempDir()
16+
ctx := context.Background()
17+
18+
registry := detector.NewRegistry()
19+
detected := registry.Detect(ctx, dir)
20+
21+
assert.False(t, detected.InProject)
22+
assert.Empty(t, detected.TargetTypes)
23+
assert.Empty(t, detected.Template)
24+
}
25+
26+
func TestDetectorRegistry_BundleWithApps(t *testing.T) {
27+
dir := t.TempDir()
28+
ctx := context.Background()
29+
30+
bundleYml := `bundle:
31+
name: my-app
32+
resources:
33+
apps:
34+
my_app: {}
35+
`
36+
require.NoError(t, os.WriteFile(filepath.Join(dir, "databricks.yml"), []byte(bundleYml), 0o644))
37+
38+
registry := detector.NewRegistry()
39+
detected := registry.Detect(ctx, dir)
40+
41+
assert.True(t, detected.InProject)
42+
assert.Equal(t, []string{"apps"}, detected.TargetTypes)
43+
assert.Equal(t, "my-app", detected.BundleInfo.Name)
44+
}
45+
46+
func TestDetectorRegistry_BundleWithJobs(t *testing.T) {
47+
dir := t.TempDir()
48+
ctx := context.Background()
49+
50+
bundleYml := `bundle:
51+
name: my-job
52+
resources:
53+
jobs:
54+
daily_job: {}
55+
`
56+
require.NoError(t, os.WriteFile(filepath.Join(dir, "databricks.yml"), []byte(bundleYml), 0o644))
57+
58+
registry := detector.NewRegistry()
59+
detected := registry.Detect(ctx, dir)
60+
61+
assert.True(t, detected.InProject)
62+
assert.Equal(t, []string{"jobs"}, detected.TargetTypes)
63+
assert.Equal(t, "my-job", detected.BundleInfo.Name)
64+
}
65+
66+
func TestDetectorRegistry_CombinedBundle(t *testing.T) {
67+
dir := t.TempDir()
68+
ctx := context.Background()
69+
70+
bundleYml := `bundle:
71+
name: my-project
72+
resources:
73+
apps:
74+
my_app: {}
75+
jobs:
76+
daily_job: {}
77+
`
78+
require.NoError(t, os.WriteFile(filepath.Join(dir, "databricks.yml"), []byte(bundleYml), 0o644))
79+
80+
registry := detector.NewRegistry()
81+
detected := registry.Detect(ctx, dir)
82+
83+
assert.True(t, detected.InProject)
84+
assert.Contains(t, detected.TargetTypes, "apps")
85+
assert.Contains(t, detected.TargetTypes, "jobs")
86+
assert.Equal(t, "my-project", detected.BundleInfo.Name)
87+
}
88+
89+
func TestDetectorRegistry_AppkitTemplate(t *testing.T) {
90+
dir := t.TempDir()
91+
ctx := context.Background()
92+
93+
// bundle + package.json with appkit marker
94+
bundleYml := `bundle:
95+
name: my-app
96+
resources:
97+
apps:
98+
my_app: {}
99+
`
100+
packageJson := `{
101+
"name": "my-app",
102+
"dependencies": {
103+
"@databricks/sql": "^1.0.0"
104+
}
105+
}`
106+
require.NoError(t, os.WriteFile(filepath.Join(dir, "databricks.yml"), []byte(bundleYml), 0o644))
107+
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJson), 0o644))
108+
109+
registry := detector.NewRegistry()
110+
detected := registry.Detect(ctx, dir)
111+
112+
assert.True(t, detected.InProject)
113+
assert.Equal(t, []string{"apps"}, detected.TargetTypes)
114+
assert.Equal(t, "appkit-typescript", detected.Template)
115+
}

0 commit comments

Comments
 (0)