mirror of
https://github.com/apache/superset.git
synced 2026-04-28 20:44:24 +00:00
Compare commits
1 Commits
fix/postgr
...
feat/mcp-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d4e9c0be |
@@ -1378,11 +1378,20 @@ class GenerateChartRequest(QueryCacheControl):
|
||||
|
||||
class GenerateExploreLinkRequest(FormDataCacheControl):
|
||||
dataset_id: int | str = Field(..., description="Dataset identifier (ID, UUID)")
|
||||
config: Dict[str, Any] = Field(..., description=_CHART_CONFIG_DESCRIPTION)
|
||||
config: Dict[str, Any] | None = Field(
|
||||
None,
|
||||
description=(
|
||||
f"{_CHART_CONFIG_DESCRIPTION} Optional; omit to get a default "
|
||||
"explore URL that opens the dataset in Superset without a "
|
||||
"preconfigured chart."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("config", mode="before")
|
||||
@classmethod
|
||||
def coerce_config(cls, v: Any) -> Dict[str, Any]:
|
||||
def coerce_config(cls, v: Any) -> Dict[str, Any] | None:
|
||||
if v is None:
|
||||
return None
|
||||
return _coerce_config_to_dict(v)
|
||||
|
||||
|
||||
|
||||
@@ -60,10 +60,12 @@ async def generate_explore_link(
|
||||
- "Visualize [data]"
|
||||
- General data exploration
|
||||
- When user wants to SEE data visually
|
||||
- Opening a dataset in Explore without a preconfigured chart (omit config)
|
||||
|
||||
IMPORTANT:
|
||||
- Use numeric dataset ID or UUID (NOT schema.table_name format)
|
||||
- MUST include chart_type in config (either 'xy' or 'table')
|
||||
- When config is provided, MUST include chart_type (e.g. 'xy' or 'table')
|
||||
- Omit config entirely to return a default explore URL for the dataset
|
||||
|
||||
Example usage:
|
||||
```json
|
||||
@@ -78,6 +80,11 @@ async def generate_explore_link(
|
||||
}
|
||||
```
|
||||
|
||||
Or with no config to simply open the dataset in Explore:
|
||||
```json
|
||||
{"dataset_id": 123}
|
||||
```
|
||||
|
||||
Better UX because:
|
||||
- Users can interact with chart before saving
|
||||
- Easy to modify parameters instantly
|
||||
@@ -88,9 +95,12 @@ async def generate_explore_link(
|
||||
|
||||
Returns explore URL for immediate use.
|
||||
"""
|
||||
chart_type = (
|
||||
request.config.get("chart_type", "unknown") if request.config else "none"
|
||||
)
|
||||
await ctx.info(
|
||||
"Generating explore link for dataset_id=%s, chart_type=%s"
|
||||
% (request.dataset_id, request.config.get("chart_type", "unknown"))
|
||||
% (request.dataset_id, chart_type)
|
||||
)
|
||||
await ctx.debug(
|
||||
"Configuration details: use_cache=%s, force_refresh=%s, cache_form_data=%s"
|
||||
@@ -98,9 +108,6 @@ async def generate_explore_link(
|
||||
)
|
||||
|
||||
try:
|
||||
# Parse the raw config dict into a typed ChartConfig
|
||||
config = parse_chart_config(request.config)
|
||||
|
||||
await ctx.report_progress(1, 4, "Validating dataset exists")
|
||||
with event_logger.log_context(action="mcp.generate_explore_link.dataset_check"):
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
@@ -132,8 +139,31 @@ async def generate_explore_link(
|
||||
),
|
||||
}
|
||||
|
||||
# When no config is provided, return a default explore URL that opens
|
||||
# the dataset in Superset without a preconfigured chart.
|
||||
if request.config is None:
|
||||
await ctx.report_progress(4, 4, "URL generation complete")
|
||||
from superset.mcp_service.utils.url_utils import get_superset_base_url
|
||||
|
||||
base_url = get_superset_base_url()
|
||||
default_url = (
|
||||
f"{base_url}/explore/?datasource_type=table&datasource_id={dataset.id}"
|
||||
)
|
||||
await ctx.info(
|
||||
"Default explore link generated: dataset_id=%s" % (request.dataset_id,)
|
||||
)
|
||||
return {
|
||||
"url": default_url,
|
||||
"form_data": {},
|
||||
"form_data_key": None,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
await ctx.report_progress(2, 4, "Converting configuration to form data")
|
||||
with event_logger.log_context(action="mcp.generate_explore_link.form_data"):
|
||||
# Parse the raw config dict into a typed ChartConfig
|
||||
config = parse_chart_config(request.config)
|
||||
|
||||
# Normalize column names to match canonical dataset column names
|
||||
# This fixes case sensitivity issues (e.g., 'order_date' vs 'OrderDate')
|
||||
try:
|
||||
@@ -203,7 +233,7 @@ async def generate_explore_link(
|
||||
"Explore link generation failed for dataset_id=%s, chart_type=%s: %s: %s"
|
||||
% (
|
||||
request.dataset_id,
|
||||
request.config.get("chart_type", "unknown"),
|
||||
chart_type,
|
||||
type(e).__name__,
|
||||
str(e),
|
||||
)
|
||||
|
||||
@@ -746,6 +746,50 @@ class TestGenerateExploreLink:
|
||||
assert "Dataset not found: 99999" in result.data["error"]
|
||||
assert "list_datasets" in result.data["error"]
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_without_config(
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Omitting config returns a default dataset explore URL."""
|
||||
mock_find_dataset.return_value = _mock_dataset(id=42)
|
||||
|
||||
request = GenerateExploreLinkRequest(dataset_id="42")
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?datasource_type=table"
|
||||
"&datasource_id=42"
|
||||
)
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_without_config_missing_dataset(
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Omitting config still surfaces a dataset-not-found error."""
|
||||
mock_find_dataset.return_value = None
|
||||
|
||||
request = GenerateExploreLinkRequest(dataset_id="99999")
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert "Dataset not found: 99999" in result.data["error"]
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_nonexistent_uuid_dataset(
|
||||
|
||||
Reference in New Issue
Block a user