You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/06/04 17:39:36 UTC

(superset) 03/03: fix(mcp): ensure filter_state in response when permalink_key is present

This is an automated email from the ASF dual-hosted git repository.

aminghadersohi pushed a commit to branch aminghadersohi/fix/mcp-info-tools-select-columns
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 769aa7e695ebc7de0da41b1406b6585b5dbe0b3a
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Thu Jun 4 17:39:06 2026 +0000

    fix(mcp): ensure filter_state in response when permalink_key is present
    
    When `permalink_key` is supplied without explicit `select_columns`,
    guard that `filter_state` is always included in the serialization
    context. Adds `test_get_dashboard_info_permalink_key_includes_filter_state`
    to cover the fix and adds `TestGetDatasetInfoRequestValidators` for
    `GetDatasetInfoRequest.column_fields` validator coverage (JSON string,
    empty list, None fallback).
---
 .../dashboard/tool/get_dashboard_info.py           | 14 ++++-
 .../dashboard/tool/test_dashboard_tools.py         | 72 ++++++++++++++++++++++
 .../mcp_service/dataset/tool/test_dataset_tools.py | 41 ++++++++++++
 3 files changed, 126 insertions(+), 1 deletion(-)

diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_info.py b/superset/mcp_service/dashboard/tool/get_dashboard_info.py
index d47e4caa9ac..f294477e507 100644
--- a/superset/mcp_service/dashboard/tool/get_dashboard_info.py
+++ b/superset/mcp_service/dashboard/tool/get_dashboard_info.py
@@ -39,6 +39,7 @@ from superset.mcp_service.dashboard.schemas import (
     dashboard_serializer,
     DashboardError,
     DashboardInfo,
+    DEFAULT_GET_DASHBOARD_INFO_COLUMNS,
     GetDashboardInfoRequest,
     redact_filter_state_data_model_metadata,
 )
@@ -248,9 +249,20 @@ async def get_dashboard_info(
                     result.is_permalink_state,
                 )
             )
+            # When permalink_key is supplied and the caller did not explicitly
+            # override select_columns, ensure filter_state is present so the
+            # caller gets the data they came for.
+            effective_select_columns = list(request.select_columns)
+            if (
+                request.permalink_key
+                and effective_select_columns == list(DEFAULT_GET_DASHBOARD_INFO_COLUMNS)
+                and "filter_state" not in effective_select_columns
+            ):
+                effective_select_columns.append("filter_state")
+
             return result.model_dump(
                 mode="json",
-                context={"select_columns": request.select_columns},
+                context={"select_columns": effective_select_columns},
             )
         else:
             await ctx.warning(
diff --git a/tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py b/tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
index a2b4e1e5f98..3cabe42f471 100644
--- a/tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
+++ b/tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
@@ -489,6 +489,78 @@ async def test_get_dashboard_info_permalink_does_not_double_sanitize(
     assert result.data["filter_state"]["activeTabs"][0] == _wrapped("TAB-1")
 
 
+@patch("superset.daos.dashboard.DashboardDAO.find_by_id")
+@pytest.mark.asyncio
+async def test_get_dashboard_info_permalink_key_includes_filter_state(
+    mock_info, mcp_server
+):
+    """When permalink_key is provided without explicit select_columns, filter_state
+    must be present in the response."""
+    dashboard = Mock()
+    dashboard.id = 42
+    dashboard.dashboard_title = "Sales Dashboard"
+    dashboard.slug = "sales"
+    dashboard.description = None
+    dashboard.css = None
+    dashboard.certified_by = None
+    dashboard.certification_details = None
+    dashboard.json_metadata = None
+    dashboard.published = True
+    dashboard.is_managed_externally = False
+    dashboard.external_url = None
+    dashboard.created_on = None
+    dashboard.changed_on = None
+    dashboard.created_by = None
+    dashboard.changed_by = None
+    dashboard.uuid = "dashboard-uuid-42"
+    dashboard.url = "/dashboard/42"
+    dashboard.thumbnail_url = None
+    dashboard.created_on_humanized = None
+    dashboard.changed_on_humanized = None
+    dashboard.slices = []
+    dashboard.owners = []
+    dashboard.tags = []
+    dashboard.roles = []
+    dashboard.charts = []
+    mock_info.return_value = dashboard
+
+    permalink_value = {
+        "dashboardId": "42",
+        "state": {
+            "dataMask": {},
+            "activeTabs": ["TAB-A"],
+        },
+    }
+
+    with (
+        patch(
+            "superset.mcp_service.dashboard.schemas.user_can_view_data_model_metadata",
+            return_value=True,
+        ),
+        patch.object(
+            get_dashboard_info_module,
+            "user_can_view_data_model_metadata",
+            return_value=True,
+        ),
+        patch.object(
+            get_dashboard_info_module,
+            "_get_permalink_state",
+            return_value=permalink_value,
+        ),
+    ):
+        async with Client(mcp_server) as client:
+            # No explicit select_columns — tool should use its default which
+            # includes filter_state when permalink_key is supplied.
+            result = await client.call_tool(
+                "get_dashboard_info",
+                {"request": {"identifier": 42, "permalink_key": "some-key"}},
+            )
+
+    assert "filter_state" in result.data
+    assert result.data["is_permalink_state"] is True
+    assert result.data["permalink_key"] == "some-key"
+
+
 def test_refresh_request_user_for_permalink_access(
     app,
 ):
diff --git a/tests/unit_tests/mcp_service/dataset/tool/test_dataset_tools.py b/tests/unit_tests/mcp_service/dataset/tool/test_dataset_tools.py
index adc6e73ad03..6b8f5d02087 100644
--- a/tests/unit_tests/mcp_service/dataset/tool/test_dataset_tools.py
+++ b/tests/unit_tests/mcp_service/dataset/tool/test_dataset_tools.py
@@ -1641,6 +1641,47 @@ class TestDatasetDefaultColumnFiltering:
                 )
 
 
+class TestGetDatasetInfoRequestValidators:
+    """Unit tests for GetDatasetInfoRequest field validators."""
+
+    def test_column_fields_json_string_parses_to_list(self):
+        """JSON string input for column_fields is decoded into a list."""
+        from superset.mcp_service.dataset.schemas import GetDatasetInfoRequest
+
+        request = GetDatasetInfoRequest(
+            identifier=1,
+            column_fields='["column_name","type"]',
+        )
+        assert request.column_fields == ["column_name", "type"]
+
+    def test_column_fields_empty_list_stays_empty(self):
+        """An explicit empty list for column_fields is preserved as-is."""
+        from superset.mcp_service.dataset.schemas import GetDatasetInfoRequest
+
+        request = GetDatasetInfoRequest(identifier=1, column_fields=[])
+        assert request.column_fields == []
+
+    def test_column_fields_none_falls_back_to_default(self):
+        """When column_fields is None (not provided), the default columns are used."""
+        from superset.mcp_service.dataset.schemas import (
+            DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS,
+            GetDatasetInfoRequest,
+        )
+
+        request = GetDatasetInfoRequest(identifier=1, column_fields=None)
+        assert request.column_fields == list(DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS)
+
+    def test_column_fields_default_when_omitted(self):
+        """When column_fields is omitted entirely, the default columns are used."""
+        from superset.mcp_service.dataset.schemas import (
+            DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS,
+            GetDatasetInfoRequest,
+        )
+
+        request = GetDatasetInfoRequest(identifier=1)
+        assert request.column_fields == list(DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS)
+
+
 class TestDatasetSortableColumns:
     """Test sortable columns configuration for dataset tools."""