Skip to content

Commit 5f9fc31

Browse files
feat(mcp): add get_chart_type_schema tool for on-demand schema discovery (#39142)
1 parent 8e811de commit 5f9fc31

File tree

4 files changed

+263
-0
lines changed

4 files changed

+263
-0
lines changed

superset/mcp_service/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ def create_mcp_app(
430430
get_chart_data,
431431
get_chart_info,
432432
get_chart_preview,
433+
get_chart_type_schema,
433434
list_charts,
434435
update_chart,
435436
update_chart_preview,

superset/mcp_service/chart/tool/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .get_chart_data import get_chart_data
2020
from .get_chart_info import get_chart_info
2121
from .get_chart_preview import get_chart_preview
22+
from .get_chart_type_schema import get_chart_type_schema
2223
from .list_charts import list_charts
2324
from .update_chart import update_chart
2425
from .update_chart_preview import update_chart_preview
@@ -31,4 +32,5 @@
3132
"update_chart_preview",
3233
"get_chart_preview",
3334
"get_chart_data",
35+
"get_chart_type_schema",
3436
]
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""
19+
MCP tool: get_chart_type_schema
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import logging
25+
from typing import Any, Dict
26+
27+
from pydantic import TypeAdapter
28+
from superset_core.mcp.decorators import tool, ToolAnnotations
29+
30+
from superset.mcp_service.chart.schemas import (
31+
BigNumberChartConfig,
32+
HandlebarsChartConfig,
33+
MixedTimeseriesChartConfig,
34+
PieChartConfig,
35+
PivotTableChartConfig,
36+
TableChartConfig,
37+
XYChartConfig,
38+
)
39+
40+
logger = logging.getLogger(__name__)
41+
42+
# Module-level TypeAdapters — one per chart type, compiled once.
43+
_CHART_TYPE_ADAPTERS: Dict[str, TypeAdapter[Any]] = {
44+
"xy": TypeAdapter(XYChartConfig),
45+
"table": TypeAdapter(TableChartConfig),
46+
"pie": TypeAdapter(PieChartConfig),
47+
"pivot_table": TypeAdapter(PivotTableChartConfig),
48+
"mixed_timeseries": TypeAdapter(MixedTimeseriesChartConfig),
49+
"handlebars": TypeAdapter(HandlebarsChartConfig),
50+
"big_number": TypeAdapter(BigNumberChartConfig),
51+
}
52+
53+
VALID_CHART_TYPES = sorted(_CHART_TYPE_ADAPTERS.keys())
54+
55+
# Per-type examples — lightweight inline examples for each chart type.
56+
_CHART_EXAMPLES: Dict[str, list[Dict[str, Any]]] = {
57+
"xy": [
58+
{
59+
"chart_type": "xy",
60+
"kind": "line",
61+
"x": {"name": "order_date"},
62+
"y": [{"name": "revenue", "aggregate": "SUM"}],
63+
"time_grain": "P1D",
64+
},
65+
{
66+
"chart_type": "xy",
67+
"kind": "bar",
68+
"x": {"name": "category"},
69+
"y": [{"name": "sales", "aggregate": "SUM"}],
70+
},
71+
],
72+
"table": [
73+
{
74+
"chart_type": "table",
75+
"columns": [
76+
{"name": "customer_name"},
77+
{"name": "revenue", "aggregate": "SUM"},
78+
],
79+
},
80+
],
81+
"pie": [
82+
{
83+
"chart_type": "pie",
84+
"dimension": {"name": "region"},
85+
"metric": {"name": "revenue", "aggregate": "SUM"},
86+
},
87+
],
88+
"pivot_table": [
89+
{
90+
"chart_type": "pivot_table",
91+
"rows": [{"name": "region"}],
92+
"metrics": [{"name": "revenue", "aggregate": "SUM"}],
93+
"columns": [{"name": "quarter"}],
94+
},
95+
],
96+
"mixed_timeseries": [
97+
{
98+
"chart_type": "mixed_timeseries",
99+
"x": {"name": "order_date"},
100+
"y": [{"name": "revenue", "aggregate": "SUM"}],
101+
"y_secondary": [{"name": "orders", "aggregate": "COUNT"}],
102+
"time_grain": "P1M",
103+
},
104+
],
105+
"handlebars": [
106+
{
107+
"chart_type": "handlebars",
108+
"query_mode": "raw",
109+
"columns": [{"name": "customer_name"}, {"name": "email"}],
110+
"handlebars_template": "{{#each data}}<p>{{customer_name}}</p>{{/each}}",
111+
},
112+
],
113+
"big_number": [
114+
{
115+
"chart_type": "big_number",
116+
"metric": {"name": "revenue", "aggregate": "SUM"},
117+
},
118+
],
119+
}
120+
121+
122+
def _get_chart_type_schema_impl(
123+
chart_type: str,
124+
include_examples: bool = True,
125+
) -> Dict[str, Any]:
126+
"""Pure logic for chart type schema lookup — no auth, no decorators."""
127+
adapter = _CHART_TYPE_ADAPTERS.get(chart_type)
128+
if adapter is None:
129+
return {
130+
"error": f"Unknown chart_type: {chart_type!r}",
131+
"valid_chart_types": VALID_CHART_TYPES,
132+
"hint": (
133+
"Use one of the valid chart_type values listed above. "
134+
"Call this tool again with a valid chart_type to see "
135+
"its schema and examples."
136+
),
137+
}
138+
139+
schema = adapter.json_schema()
140+
result: Dict[str, Any] = {
141+
"chart_type": chart_type,
142+
"schema": schema,
143+
}
144+
145+
if include_examples:
146+
result["examples"] = _CHART_EXAMPLES.get(chart_type, [])
147+
148+
return result
149+
150+
151+
@tool(
152+
tags=["discovery"],
153+
annotations=ToolAnnotations(
154+
title="Get chart type schema",
155+
readOnlyHint=True,
156+
destructiveHint=False,
157+
),
158+
)
159+
def get_chart_type_schema(
160+
chart_type: str,
161+
include_examples: bool = True,
162+
) -> Dict[str, Any]:
163+
"""Get the full JSON Schema and examples for a specific chart type.
164+
165+
Use this tool to discover the exact fields, types, and constraints
166+
for a chart configuration before calling generate_chart or update_chart.
167+
168+
Valid chart_type values: xy, table, pie, pivot_table,
169+
mixed_timeseries, handlebars, big_number.
170+
171+
Returns the JSON Schema for the requested chart type, optionally
172+
with working examples.
173+
"""
174+
return _get_chart_type_schema_impl(chart_type, include_examples)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Tests for get_chart_type_schema tool logic."""
19+
20+
import pytest
21+
22+
from superset.mcp_service.chart.tool.get_chart_type_schema import (
23+
_CHART_EXAMPLES,
24+
_get_chart_type_schema_impl as _call_schema,
25+
VALID_CHART_TYPES,
26+
)
27+
28+
29+
class TestGetChartTypeSchema:
30+
@pytest.mark.parametrize("chart_type", VALID_CHART_TYPES)
31+
def test_valid_chart_type_returns_schema(self, chart_type: str) -> None:
32+
result = _call_schema(chart_type)
33+
assert "schema" in result
34+
assert result["chart_type"] == chart_type
35+
assert isinstance(result["schema"], dict)
36+
assert "properties" in result["schema"]
37+
assert "examples" in result
38+
39+
def test_xy_schema_has_expected_fields(self) -> None:
40+
result = _call_schema("xy")
41+
props = result["schema"]["properties"]
42+
assert "x" in props
43+
assert "y" in props
44+
assert "kind" in props
45+
46+
def test_table_schema_has_columns(self) -> None:
47+
result = _call_schema("table")
48+
props = result["schema"]["properties"]
49+
assert "columns" in props
50+
51+
def test_pie_schema_has_dimension_metric(self) -> None:
52+
result = _call_schema("pie")
53+
props = result["schema"]["properties"]
54+
assert "dimension" in props
55+
assert "metric" in props
56+
57+
def test_big_number_schema_has_metric(self) -> None:
58+
result = _call_schema("big_number")
59+
props = result["schema"]["properties"]
60+
assert "metric" in props
61+
62+
def test_include_examples_false_omits_examples(self) -> None:
63+
result = _call_schema("xy", include_examples=False)
64+
assert "schema" in result
65+
assert "examples" not in result
66+
67+
def test_invalid_chart_type_returns_error(self) -> None:
68+
result = _call_schema("nonexistent")
69+
assert "error" in result
70+
assert "valid_chart_types" in result
71+
assert result["valid_chart_types"] == VALID_CHART_TYPES
72+
73+
def test_examples_match_chart_type(self) -> None:
74+
result = _call_schema("pie")
75+
for example in result["examples"]:
76+
assert example["chart_type"] == "pie"
77+
78+
def test_valid_chart_types_constant(self) -> None:
79+
assert len(VALID_CHART_TYPES) == 7
80+
assert "xy" in VALID_CHART_TYPES
81+
assert "table" in VALID_CHART_TYPES
82+
83+
def test_all_chart_types_have_examples(self) -> None:
84+
for chart_type in VALID_CHART_TYPES:
85+
assert chart_type in _CHART_EXAMPLES
86+
assert len(_CHART_EXAMPLES[chart_type]) >= 1

0 commit comments

Comments
 (0)