Skip to content

Commit 17aa160

Browse files
authored
Restructure AI/BI dashboard skill with progressive disclosure (#322) (#362)
* Split databricks-aibi-dashboards skill * Fix: replace ambiguous tool docstring with concrete widget JSON examples
1 parent 2ca440b commit 17aa160

File tree

6 files changed

+877
-842
lines changed

6 files changed

+877
-842
lines changed

databricks-mcp-server/databricks_mcp_server/tools/aibi_dashboards.py

Lines changed: 111 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -48,95 +48,117 @@ def create_or_update_dashboard(
4848
) -> Dict[str, Any]:
4949
"""Create or update an AI/BI dashboard from JSON content.
5050
51-
CRITICAL PRE-REQUISITES (DO NOT SKIP):
52-
Before calling this tool, you MUST:
51+
CRITICAL: Before calling this tool, you MUST:
5352
1. Call get_table_details() to get table schemas
54-
2. Call execute_sql() to TEST EVERY dataset query - if any fail, fix them first!
55-
3. Verify query results have expected columns and data types
56-
57-
If you skip validation, widgets WILL show "Invalid widget definition" errors!
58-
59-
DASHBOARD JSON REQUIREMENTS:
60-
61-
Dataset Architecture:
62-
- One dataset per domain (orders, customers, products)
63-
- Exactly ONE SQL query per dataset (no semicolon-separated queries)
64-
- Use fully-qualified table names: catalog.schema.table_name
65-
- All widget fieldNames must match dataset column names exactly
66-
67-
CRITICAL VERSION REQUIREMENTS:
68-
- counter: version 2
69-
- table: version 2
70-
- filter-multi-select, filter-single-select, filter-date-range-picker: version 2
71-
- bar, line, pie: version 3
72-
- text: NO spec block (use multilineTextboxSpec directly on widget)
73-
74-
CRITICAL FIELD NAME MATCHING:
75-
The "name" in query.fields MUST exactly match "fieldName" in encodings!
76-
- CORRECT: fields=[{"name": "sum(spend)", "expression": "SUM(`spend`)"}]
77-
encodings={"value": {"fieldName": "sum(spend)", ...}}
78-
- WRONG: fields=[{"name": "spend", "expression": "SUM(`spend`)"}]
79-
encodings={"value": {"fieldName": "sum(spend)", ...}} # ERROR!
80-
81-
Widget Field Expressions (ONLY these are allowed):
82-
- Aggregates: SUM(`col`), AVG(`col`), COUNT(`col`), COUNT(DISTINCT `col`), MIN(`col`), MAX(`col`)
83-
- Date truncation: DATE_TRUNC("DAY", `date`), DATE_TRUNC("WEEK", `date`), DATE_TRUNC("MONTH", `date`)
84-
- Simple reference: `column_name`
85-
- NO CAST, no complex SQL - put logic in dataset query instead
86-
87-
Layout (6-column grid, NO GAPS):
88-
- Each row must total width=6 exactly
89-
- Counter/KPI: width=2, height=3-4 (NEVER height=2)
90-
- Charts: width=3, height=5-6
91-
- Tables: width=6, height=5-8
92-
- Text headers: width=6, height=1 (use SEPARATE widgets for title and subtitle)
93-
94-
Widget Naming:
95-
- widget.name: alphanumeric + hyphens + underscores ONLY (no spaces/parentheses/colons)
96-
- frame.title: human-readable name (any characters)
97-
- widget.queries[0].name: always "main_query"
98-
99-
Text Widgets:
100-
- Do NOT use a spec block - use multilineTextboxSpec directly on widget
101-
- Multiple items in lines[] are CONCATENATED, not separate lines
102-
- Use separate text widgets for title and subtitle at different y positions
103-
104-
Counter Widgets (version 2):
105-
Pattern 1 - Pre-aggregated (1 row, no filters):
106-
- Dataset returns exactly 1 row
107-
- Use "disaggregated": true and simple field reference
108-
Pattern 2 - Aggregating (multi-row, supports filters):
109-
- Dataset returns multiple rows (grouped by filter dimension)
110-
- Use "disaggregated": false and aggregation expression
111-
- Field name must match: {"name": "sum(spend)", "expression": "SUM(`spend`)"}
112-
- Percent values must be 0-1 (not 0-100)
113-
114-
Table Widgets (version 2):
115-
- Column objects only need fieldName and displayName - no other properties!
116-
- Use "disaggregated": true for raw rows
117-
118-
Charts - line/bar/pie (version 3):
119-
- Use "disaggregated": true with pre-aggregated data
120-
- scale.type: "temporal" (dates), "quantitative" (numbers), "categorical" (strings)
121-
- Limit color/grouping dimensions to 3-8 distinct values
122-
123-
SQL Patterns (Spark SQL):
124-
- Date math: date_sub(current_date(), N), add_months(current_date(), -N)
125-
- AVOID INTERVAL syntax - use functions instead
126-
127-
Filters (CRITICAL - Global vs Page-Level):
128-
- Valid filter widgetTypes: "filter-multi-select", "filter-single-select", "filter-date-range-picker"
129-
- Filter widgets use spec.version: 2 (NOT 3 like charts)
130-
- Filter encodings require "queryName" to bind to dataset queries
131-
- Use "disaggregated": false for filter queries
132-
- DO NOT use widgetType: "filter" - this is INVALID and will cause errors
133-
- DO NOT use associative_filter_predicate_group - causes SQL errors
134-
- ALWAYS include "frame": {"showTitle": true, "title": "..."} for filter widgets
135-
136-
Global Filters vs Page-Level Filters:
137-
- GLOBAL: Place on page with "pageType": "PAGE_TYPE_GLOBAL_FILTERS" - affects ALL pages
138-
- PAGE-LEVEL: Place on regular "PAGE_TYPE_CANVAS" page - affects ONLY that page
139-
- A filter only affects datasets containing the filter field column
53+
2. Call execute_sql() to TEST EVERY dataset query
54+
If you skip validation, widgets WILL show errors!
55+
56+
WIDGET STRUCTURE (CRITICAL - follow this exactly):
57+
Each widget in a page layout has `queries` as a TOP-LEVEL SIBLING of `spec`.
58+
Do NOT put queries inside spec. Do NOT use `named_queries`.
59+
60+
Correct counter widget:
61+
{
62+
"widget": {
63+
"name": "total-trips",
64+
"queries": [
65+
{
66+
"name": "main_query",
67+
"query": {
68+
"datasetName": "summary",
69+
"fields": [{"name": "sum(trips)", "expression": "SUM(`trips`)"}],
70+
"disaggregated": false
71+
}
72+
}
73+
],
74+
"spec": {
75+
"version": 2,
76+
"widgetType": "counter",
77+
"encodings": {
78+
"value": {"fieldName": "sum(trips)", "displayName": "Total Trips"}
79+
},
80+
"frame": {"showTitle": true, "title": "Total Trips"}
81+
}
82+
},
83+
"position": {"x": 0, "y": 0, "width": 2, "height": 3}
84+
}
85+
86+
Correct bar chart widget:
87+
{
88+
"widget": {
89+
"name": "trips-by-zip",
90+
"queries": [
91+
{
92+
"name": "main_query",
93+
"query": {
94+
"datasetName": "by_zip",
95+
"fields": [
96+
{"name": "pickup_zip", "expression": "`pickup_zip`"},
97+
{"name": "trip_count", "expression": "`trip_count`"}
98+
],
99+
"disaggregated": true
100+
}
101+
}
102+
],
103+
"spec": {
104+
"version": 3,
105+
"widgetType": "bar",
106+
"encodings": {
107+
"x": {"fieldName": "pickup_zip", "scale": {"type": "categorical"}, "displayName": "ZIP"},
108+
"y": {"fieldName": "trip_count", "scale": {"type": "quantitative"}, "displayName": "Trips"}
109+
},
110+
"frame": {"showTitle": true, "title": "Trips by ZIP"}
111+
}
112+
},
113+
"position": {"x": 0, "y": 3, "width": 6, "height": 5}
114+
}
115+
116+
Correct filter widget:
117+
{
118+
"widget": {
119+
"name": "filter-region",
120+
"queries": [
121+
{
122+
"name": "main_query",
123+
"query": {
124+
"datasetName": "sales",
125+
"fields": [{"name": "region", "expression": "`region`"}],
126+
"disaggregated": false
127+
}
128+
}
129+
],
130+
"spec": {
131+
"version": 2,
132+
"widgetType": "filter-multi-select",
133+
"encodings": {
134+
"fields": [{"fieldName": "region", "queryName": "main_query", "displayName": "Region"}]
135+
},
136+
"frame": {"showTitle": true, "title": "Region"}
137+
}
138+
},
139+
"position": {"x": 0, "y": 0, "width": 2, "height": 2}
140+
}
141+
142+
Text widget (NO spec block):
143+
{
144+
"widget": {
145+
"name": "title",
146+
"textbox_spec": "## Dashboard Title"
147+
},
148+
"position": {"x": 0, "y": 0, "width": 6, "height": 1}
149+
}
150+
151+
KEY RULES:
152+
- queries[].query.datasetName (camelCase, not dataSetName)
153+
- queries[].query.fields[].name MUST exactly match encodings fieldName
154+
- Versions: counter=2, table=2, filters=2, bar/line/pie=3
155+
- Layout: 6-column grid, each row must sum to width=6
156+
- Filter widgetType must be "filter-multi-select", "filter-single-select",
157+
or "filter-date-range-picker" (NOT "filter")
158+
- Global filters: page with "pageType": "PAGE_TYPE_GLOBAL_FILTERS"
159+
- Page-level filters: on regular "PAGE_TYPE_CANVAS" page
160+
161+
See the databricks-aibi-dashboards skill for full reference.
140162
141163
Args:
142164
display_name: Dashboard display name
@@ -146,14 +168,7 @@ def create_or_update_dashboard(
146168
publish: Whether to publish after creation (default: True)
147169
148170
Returns:
149-
Dictionary with:
150-
- success: Whether operation succeeded
151-
- status: 'created' or 'updated'
152-
- dashboard_id: Dashboard ID
153-
- path: Full workspace path
154-
- url: Dashboard URL
155-
- published: Whether dashboard was published
156-
- error: Error message if failed
171+
Dictionary with success, status, dashboard_id, path, url, published, error.
157172
"""
158173
# MCP deserializes JSON params, so serialized_dashboard may arrive as a dict
159174
if isinstance(serialized_dashboard, dict):

0 commit comments

Comments
 (0)