diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py index 81ddd56a540c..7de77da1fe0a 100644 --- a/superset/mcp_service/chart/schemas.py +++ b/superset/mcp_service/chart/schemas.py @@ -1703,7 +1703,14 @@ def validate_save_or_preview(self) -> "GenerateChartRequest": class GenerateExploreLinkRequest(FormDataCacheControl): dataset_id: int | str = Field(..., description="Dataset identifier (ID, UUID)") - config: ChartConfig = Field(..., description="Chart configuration") + config: ChartConfig | None = Field( + None, + description=( + "Chart configuration. Optional; omit to get a default " + "explore URL that opens the dataset in Superset without a " + "preconfigured chart." + ), + ) class UpdateChartRequest(QueryCacheControl): diff --git a/superset/mcp_service/explore/tool/generate_explore_link.py b/superset/mcp_service/explore/tool/generate_explore_link.py index 6e29158b6e6c..68ed36c037a8 100644 --- a/superset/mcp_service/explore/tool/generate_explore_link.py +++ b/superset/mcp_service/explore/tool/generate_explore_link.py @@ -65,10 +65,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 @@ -83,6 +85,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 @@ -93,9 +100,10 @@ async def generate_explore_link( Returns explore URL for immediate use. """ + chart_type = request.config.chart_type if request.config else "none" await ctx.info( "Generating explore link for dataset_id=%s, chart_type=%s" - % (request.dataset_id, request.config.chart_type) + % (request.dataset_id, chart_type) ) await ctx.debug( "Configuration details: use_cache=%s, force_refresh=%s, cache_form_data=%s" @@ -103,9 +111,6 @@ async def generate_explore_link( ) try: - # config is already a typed ChartConfig (validated by Pydantic) - 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 @@ -157,8 +162,32 @@ 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, + "chart_type_label": 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"): + # config is already a typed ChartConfig (validated by Pydantic) + config = request.config + # Normalize column names to match canonical dataset column names # This fixes case sensitivity issues (e.g., 'order_date' vs 'OrderDate') try: @@ -256,7 +285,7 @@ async def generate_explore_link( "Explore link generation failed for dataset_id=%s, chart_type=%s: %s: %s" % ( request.dataset_id, - request.config.chart_type, + chart_type, type(e).__name__, str(e), ) diff --git a/tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py b/tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py index 5a22d2dc6c30..a4dc86f86a37 100644 --- a/tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py +++ b/tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py @@ -810,6 +810,52 @@ async def test_generate_explore_link_nonexistent_dataset( 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 + assert result.data["chart_type_label"] 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 result.data["chart_type_label"] 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(