From 4c9dc89f1bed8da5532320eea2b537b5981e09ef Mon Sep 17 00:00:00 2001 From: onurtashan Date: Thu, 18 Jun 2026 11:47:54 +0300 Subject: [PATCH 1/4] fix(mcp): include embedded_uuid in get_dashboard_info response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds embedded_uuid to DashboardInfo so agents can retrieve the embedded UUID without a separate API call. The embedded UUID is required for guest token generation (resources[].id) and differs from the dashboard UUID — using the wrong one causes 403 errors in has_guest_access(). --- superset/mcp_service/dashboard/schemas.py | 15 +++ .../dashboard/tool/get_dashboard_info.py | 3 +- .../dashboard/tool/test_dashboard_tools.py | 94 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py index 73a57a6e292e..04e4381391f4 100644 --- a/superset/mcp_service/dashboard/schemas.py +++ b/superset/mcp_service/dashboard/schemas.py @@ -425,6 +425,18 @@ class DashboardInfo(BaseModel): created_on: str | datetime | None = None changed_on: str | datetime | None = None uuid: str | None = None + embedded_uuid: str | None = Field( + None, + description=( + "Embedded UUID for this dashboard. This is the UUID required when " + "generating guest tokens for embedded dashboards " + "(resources[].id in the guest token payload). " + "Only present when the dashboard has been configured for embedding " + "via the Embed Dashboard UI. Distinct from `uuid` (the internal " + "dashboard UUID) — using the wrong one causes 403 errors in guest " + "token validation." + ), + ) url: str | None = None created_on_humanized: str | None = None changed_on_humanized: str | None = None @@ -1137,6 +1149,9 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo: created_on=dashboard.created_on, changed_on=dashboard.changed_on, uuid=str(dashboard.uuid) if dashboard.uuid else None, + embedded_uuid=str(dashboard.embedded[0].uuid) + if dashboard.embedded + else None, url=absolute_url, created_on_humanized=dashboard.created_on_humanized, changed_on_humanized=dashboard.changed_on_humanized, diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_info.py b/superset/mcp_service/dashboard/tool/get_dashboard_info.py index f7f39e27228b..9a414dae06c3 100644 --- a/superset/mcp_service/dashboard/tool/get_dashboard_info.py +++ b/superset/mcp_service/dashboard/tool/get_dashboard_info.py @@ -155,10 +155,11 @@ async def get_dashboard_info( from superset.models.dashboard import Dashboard from superset.models.slice import Slice - # Eager load slices and tags to avoid N+1 queries during serialization. + # Eager load slices, tags, and embedded to avoid N+1 queries. eager_options = [ subqueryload(Dashboard.slices).subqueryload(Slice.tags), subqueryload(Dashboard.tags), + subqueryload(Dashboard.embedded), ] with event_logger.log_context(action="mcp.get_dashboard_info.lookup"): 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 3cabe42f471e..a742f743be21 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 @@ -96,6 +96,7 @@ async def test_list_dashboards_basic(mock_list, mcp_server): dashboard.uuid = "test-dashboard-uuid-1" dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -162,6 +163,7 @@ async def test_list_dashboards_with_filters(mock_list, mcp_server): dashboard.uuid = None dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -257,6 +259,7 @@ async def test_list_dashboards_with_search(mock_list, mcp_server): dashboard.uuid = None dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -351,6 +354,7 @@ async def test_get_dashboard_info_success( dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -429,6 +433,7 @@ async def test_get_dashboard_info_permalink_does_not_double_sanitize( dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] mock_info.return_value = dashboard permalink_value = { @@ -838,6 +843,7 @@ async def test_get_dashboard_info_restricted_user_redacts_data_model_metadata( dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] mock_info.return_value = dashboard @@ -890,6 +896,7 @@ async def test_get_dashboard_info_restricted_user_redacts_permalink_filter_state dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] mock_info.return_value = dashboard @@ -1012,6 +1019,88 @@ async def test_list_dashboards_omits_requested_user_directory_fields( assert field not in data["columns_available"] +@patch("superset.mcp_service.mcp_core.ModelGetInfoCore._find_object") +@pytest.mark.asyncio +async def test_get_dashboard_info_includes_embedded_uuid(mock_find_object, mcp_server): + """Test that get_dashboard_info returns embedded_uuid when set.""" + from superset.models.embedded_dashboard import EmbeddedDashboard + + dashboard = Mock() + dashboard.id = 1 + dashboard.dashboard_title = "Embedded Dashboard" + dashboard.slug = "" + dashboard.description = None + dashboard.css = None + dashboard.certified_by = None + dashboard.certification_details = None + dashboard.json_metadata = "{}" + dashboard.published = True + dashboard.is_managed_externally = False + dashboard.external_url = None + dashboard.created_on = None + dashboard.changed_on = None + dashboard.created_on_humanized = None + dashboard.changed_on_humanized = None + dashboard.uuid = "94b826a5-dbd5-473d-ab58-1af676ee07e4" + dashboard.url = "/dashboard/1" + dashboard.slices = [] + dashboard.owners = [] + dashboard.tags = [] + dashboard.roles = [] + dashboard.embedded = [] + + embedded = Mock(spec=EmbeddedDashboard) + embedded.uuid = "37c56048-d3f1-452d-b3ae-0879802dcb1f" + dashboard.embedded = [embedded] + + mock_find_object.return_value = dashboard + async with Client(mcp_server) as client: + result = await client.call_tool( + "get_dashboard_info", {"request": {"identifier": 1}} + ) + assert result.data["uuid"] == "94b826a5-dbd5-473d-ab58-1af676ee07e4" + assert result.data["embedded_uuid"] == "37c56048-d3f1-452d-b3ae-0879802dcb1f" + + +@patch("superset.mcp_service.mcp_core.ModelGetInfoCore._find_object") +@pytest.mark.asyncio +async def test_get_dashboard_info_embedded_uuid_none_when_not_embedded( + mock_find_object, mcp_server +): + """Test that embedded_uuid is None when the dashboard has not been configured + for embedding.""" + dashboard = Mock() + dashboard.id = 2 + dashboard.dashboard_title = "Non-Embedded Dashboard" + dashboard.slug = "" + dashboard.description = None + dashboard.css = None + dashboard.certified_by = None + dashboard.certification_details = None + dashboard.json_metadata = "{}" + dashboard.published = True + dashboard.is_managed_externally = False + dashboard.external_url = None + dashboard.created_on = None + dashboard.changed_on = None + dashboard.created_on_humanized = None + dashboard.changed_on_humanized = None + dashboard.uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + dashboard.url = "/dashboard/2" + dashboard.slices = [] + dashboard.owners = [] + dashboard.tags = [] + dashboard.roles = [] + dashboard.embedded = [] + + mock_find_object.return_value = dashboard + async with Client(mcp_server) as client: + result = await client.call_tool( + "get_dashboard_info", {"request": {"identifier": 2}} + ) + assert result.data.get("embedded_uuid") is None + + # TODO (Phase 3+): Add tests for get_dashboard_available_filters tool @@ -1044,6 +1133,7 @@ async def test_get_dashboard_info_by_uuid(mock_find_object, mcp_server): dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] mock_find_object.return_value = dashboard async with Client(mcp_server) as client: @@ -1083,6 +1173,7 @@ async def test_get_dashboard_info_by_slug(mock_find_object, mcp_server): dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] mock_find_object.return_value = dashboard async with Client(mcp_server) as client: @@ -1122,6 +1213,7 @@ async def test_list_dashboards_custom_uuid_slug_columns(mock_list, mcp_server): dashboard.external_url = None dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -1203,6 +1295,7 @@ async def test_list_dashboards_sanitizes_dashboard_descriptions_and_filter_text( dashboard.external_url = None dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] dashboard._mapping = { "id": dashboard.id, @@ -1343,6 +1436,7 @@ async def test_default_columns_in_response(self, mock_list, mcp_server): dashboard.external_url = None dashboard.thumbnail_url = None dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] mock_list.return_value = ([dashboard], 1) From 643ed59dbe5c12939ea798dcfca42b92181fd3d6 Mon Sep 17 00:00:00 2001 From: onurtashan Date: Thu, 18 Jun 2026 14:37:25 +0300 Subject: [PATCH 2/4] fix(mcp): fix indentation in test_default_columns_in_response Co-Authored-By: Claude Sonnet 4.6 --- .../mcp_service/dashboard/tool/test_dashboard_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a742f743be21..6223d1eb5ad9 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 @@ -1436,7 +1436,7 @@ async def test_default_columns_in_response(self, mock_list, mcp_server): dashboard.external_url = None dashboard.thumbnail_url = None dashboard.roles = [] - dashboard.embedded = [] + dashboard.embedded = [] dashboard.charts = [] mock_list.return_value = ([dashboard], 1) From 5bf58493649297c5492d7f13a4714a8bc286311d Mon Sep 17 00:00:00 2001 From: onurtashan Date: Thu, 18 Jun 2026 15:30:26 +0300 Subject: [PATCH 3/4] fix(mcp): add missing dashboard.embedded=[] to get_dashboard_info tests Two tests were missing `dashboard.embedded = []` in their mock setup, causing `dashboard_serializer` to access `dashboard.embedded[0]` on an unset Mock attribute (truthy but not subscriptable). Co-Authored-By: Claude Sonnet 4.6 --- .../mcp_service/dashboard/tool/test_dashboard_tools.py | 2 ++ 1 file changed, 2 insertions(+) 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 6223d1eb5ad9..b6611b0c2c16 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 @@ -526,6 +526,7 @@ async def test_get_dashboard_info_permalink_key_includes_filter_state( dashboard.owners = [] dashboard.tags = [] dashboard.roles = [] + dashboard.embedded = [] dashboard.charts = [] mock_info.return_value = dashboard @@ -772,6 +773,7 @@ async def test_get_dashboard_info_does_not_expose_access_list_or_roles( dashboard.owners = [owner] dashboard.tags = [] dashboard.roles = [dashboard_role] + dashboard.embedded = [] mock_info.return_value = dashboard From 8badd8cebdce50d517e31ffbff95a69915c86f18 Mon Sep 17 00:00:00 2001 From: onurtashan Date: Thu, 18 Jun 2026 16:04:02 +0300 Subject: [PATCH 4/4] fix: add embedded_uuid to DEFAULT_GET_DASHBOARD_INFO_COLUMNS embedded_uuid was populated in dashboard_serializer and defined as a field on DashboardInfo, but missing from DEFAULT_GET_DASHBOARD_INFO_COLUMNS. Since get_dashboard_info calls model_dump with select_columns defaulting to DEFAULT_GET_DASHBOARD_INFO_COLUMNS, the field was always filtered out of the returned dict even when the dashboard had an embedded UUID. --- superset/mcp_service/dashboard/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py index 04e4381391f4..4e198c353569 100644 --- a/superset/mcp_service/dashboard/schemas.py +++ b/superset/mcp_service/dashboard/schemas.py @@ -301,6 +301,7 @@ def validate_search_and_filters(self) -> "ListDashboardsRequest": "created_on", "changed_on", "uuid", + "embedded_uuid", "url", "created_on_humanized", "changed_on_humanized",