You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/27 01:00:16 UTC

(superset) branch master updated: fix(mcp): include embedded_uuid in get_dashboard_info response (#41195)

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

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 1280eaee18a fix(mcp): include embedded_uuid in get_dashboard_info response (#41195)
1280eaee18a is described below

commit 1280eaee18a57b70c718796e85beb416830b7971
Author: Onur Taşhan <on...@gmail.com>
AuthorDate: Sat Jun 27 04:00:10 2026 +0300

    fix(mcp): include embedded_uuid in get_dashboard_info response (#41195)
    
    Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
---
 superset/mcp_service/dashboard/schemas.py          | 16 ++++
 .../dashboard/tool/get_dashboard_info.py           |  3 +-
 .../dashboard/tool/test_dashboard_tools.py         | 96 ++++++++++++++++++++++
 3 files changed, 114 insertions(+), 1 deletion(-)

diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py
index f45684abc37..1c2d16190f0 100644
--- a/superset/mcp_service/dashboard/schemas.py
+++ b/superset/mcp_service/dashboard/schemas.py
@@ -303,6 +303,7 @@ DEFAULT_GET_DASHBOARD_INFO_COLUMNS: List[str] = [
     "created_on",
     "changed_on",
     "uuid",
+    "embedded_uuid",
     "url",
     "created_on_humanized",
     "changed_on_humanized",
@@ -427,6 +428,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
@@ -1352,6 +1365,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 f7f39e27228..9a414dae06c 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 3cabe42f471..b6611b0c2c1 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 = {
@@ -521,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
 
@@ -767,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
 
@@ -838,6 +845,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 +898,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 +1021,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 +1135,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 +1175,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 +1215,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 +1297,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 +1438,7 @@ class TestDashboardDefaultColumnFiltering:
         dashboard.external_url = None
         dashboard.thumbnail_url = None
         dashboard.roles = []
+        dashboard.embedded = []
         dashboard.charts = []
         mock_list.return_value = ([dashboard], 1)