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)