Skip to content
16 changes: 16 additions & 0 deletions superset/mcp_service/dashboard/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -425,6 +426,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
Expand Down Expand Up @@ -1137,6 +1150,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,
Expand Down
3 changes: 2 additions & 1 deletion superset/mcp_service/dashboard/tool/get_dashboard_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add explicit type annotations for all parameters and the return type in this new async test function to comply with the project typing rule for newly introduced code. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a newly added Python async function in a modified file, and its parameters are untyped with no return annotation. That directly violates the rule requiring new Python code to be fully typed.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
**Line:** 1026:1026
**Comment:**
	*Custom Rule: Add explicit type annotations for all parameters and the return type in this new async test function to comply with the project typing rule for newly introduced code.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

"""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
):
Comment on lines +1069 to +1071

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add parameter and return type annotations to this newly added async test function signature so the new code is fully typed. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This newly added async test function lacks parameter type hints and a return type annotation. Because it is introduced in the changed code, it matches the typing rule violation exactly.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
**Line:** 1069:1071
**Comment:**
	*Custom Rule: Add parameter and return type annotations to this newly added async test function signature so the new code is fully typed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

"""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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1343,6 +1438,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)

Expand Down
Loading