Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions superset/mcp_service/common/schema_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ def _get_sqlalchemy_type_name(col_type: Any) -> str:
"dashboard_title": "Dashboard display title",
"slug": "URL-friendly identifier for the dashboard",
"published": "Whether the dashboard is published and visible",
"position_json": "JSON layout of dashboard components",
"json_metadata": "JSON metadata including filters and settings",
"css": "Custom CSS for the dashboard",
"native_filters": "Native filter configuration (name, type, targets)",
"cross_filters_enabled": "Whether cross-filtering between charts is enabled",
"theme_id": "Theme ID for dashboard styling",
}

Expand Down
216 changes: 205 additions & 11 deletions superset/mcp_service/dashboard/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
dashboard_title="Sales Dashboard",
published=True,
owners=[UserInfo(id=1, username="admin")],
charts=[ChartInfo(id=1, slice_name="Sales Chart")]
charts=[DashboardChartSummary(id=1, slice_name="Sales Chart")]
)

# For dashboard list responses
Expand Down Expand Up @@ -65,6 +65,7 @@

from __future__ import annotations

import logging
from datetime import datetime
from typing import Annotated, Any, Dict, List, Literal, TYPE_CHECKING

Expand All @@ -83,7 +84,6 @@
from superset.models.dashboard import Dashboard

from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.mcp_service.chart.schemas import ChartInfo, serialize_chart_object
from superset.mcp_service.common.cache_schemas import MetadataCacheControl
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from superset.mcp_service.system.schemas import (
Expand All @@ -97,6 +97,7 @@
_remove_dangerous_unicode,
_strip_html_tags,
)
from superset.utils.json import loads as json_loads


class DashboardError(BaseModel):
Expand Down Expand Up @@ -298,6 +299,43 @@ class GetDashboardInfoRequest(MetadataCacheControl):
)


logger = logging.getLogger(__name__)


class NativeFilterSummary(BaseModel):
"""Lightweight summary of a native filter for LLM consumption.

Extracts only the fields needed to understand what filters are
available on a dashboard: name, type, and which columns they target.
"""

id: str | None = Field(None, description="Filter ID")
name: str | None = Field(None, description="Filter display name")
filter_type: str | None = Field(
None, description="Filter type (e.g. filter_select, filter_range)"
)
targets: List[Dict[str, Any]] = Field(
default_factory=list,
description="Filter targets (column name and dataset ID)",
)


class DashboardChartSummary(BaseModel):
"""Lightweight chart representation for dashboard context.

Contains only the fields needed for LLMs to understand which charts
are on a dashboard, omitting verbose fields like form_data, tags,
owners, and timestamps that bloat the response.
"""

id: int | None = Field(None, description="Chart ID")
slice_name: str | None = Field(None, description="Chart name")
viz_type: str | None = Field(None, description="Visualization type")
datasource_name: str | None = Field(None, description="Datasource name")
url: str | None = Field(None, description="Chart explore page URL")
description: str | None = Field(None, description="Chart description")


class DashboardInfo(BaseModel):
id: int | None = None
dashboard_title: str | None = None
Expand All @@ -306,8 +344,6 @@ class DashboardInfo(BaseModel):
css: str | None = None
certified_by: str | None = None
certification_details: str | None = None
json_metadata: str | None = None
position_json: str | None = None
published: bool | None = None
is_managed_externally: bool | None = None
external_url: str | None = None
Expand All @@ -323,7 +359,32 @@ class DashboardInfo(BaseModel):
owners: List[UserInfo] = Field(default_factory=list)
tags: List[TagInfo] = Field(default_factory=list)
roles: List[RoleInfo] = Field(default_factory=list)
charts: List[ChartInfo] = Field(default_factory=list)
charts: List[DashboardChartSummary] = Field(default_factory=list)

# Structured filter information extracted from json_metadata
native_filters: List[NativeFilterSummary] = Field(
default_factory=list,
description=(
"Native filters configured on this dashboard. Extracted from "
"json_metadata for LLM consumption — includes filter name, type, "
"and target columns."
),
)
cross_filters_enabled: bool | None = Field(
None,
description="Whether cross-filtering between charts is enabled.",
)

# Omission metadata — tells the agent what was stripped and why
omitted_fields: Dict[str, str] = Field(
default_factory=dict,
description=(
"Fields omitted from this response to reduce size. Keys are field "
"names, values describe what was omitted and how to access the full "
"data. Useful filter information has been extracted into "
"native_filters and cross_filters_enabled above."
),
)

# Fields for permalink/filter state support
permalink_key: str | None = Field(
Expand Down Expand Up @@ -473,6 +534,121 @@ class GenerateDashboardResponse(BaseModel):
error: str | None = Field(None, description="Error message, if creation failed")


def _parse_json_metadata(json_metadata_str: str | None) -> Dict[str, Any] | None:
"""Parse json_metadata string into a dict, returning None on any failure.

Handles None/empty input, invalid JSON, and non-dict JSON values
(e.g. ``"[]"``, ``"123"``) by returning None instead of raising.
"""
if not json_metadata_str:
return None
try:
metadata = json_loads(json_metadata_str)
except (ValueError, TypeError):
return None
if not isinstance(metadata, dict):
return None
return metadata


def _extract_native_filters(json_metadata_str: str | None) -> List[NativeFilterSummary]:
"""Extract native filter summaries from raw json_metadata string.

Parses the json_metadata JSON blob and pulls out only the filter
name, type, and targets — dropping verbose fields like controlValues,
defaultDataMask, scope, and cascadeParentIds.
"""
metadata = _parse_json_metadata(json_metadata_str)
if metadata is None:
return []

native_filters = metadata.get("native_filter_configuration", [])
if not isinstance(native_filters, list):
return []

summaries: List[NativeFilterSummary] = []
for f in native_filters:
if not isinstance(f, dict):
continue
raw_targets = f.get("targets", [])
if not isinstance(raw_targets, list):
raw_targets = []
targets = [t for t in raw_targets if isinstance(t, dict)]
summaries.append(
NativeFilterSummary(
id=f.get("id"),
name=f.get("name"),
Comment thread
aminghadersohi marked this conversation as resolved.
filter_type=f.get("filterType"),
targets=targets,
)
)
return summaries


def _extract_cross_filters_enabled(json_metadata_str: str | None) -> bool | None:
"""Extract the cross_filters_enabled flag from json_metadata."""
metadata = _parse_json_metadata(json_metadata_str)
if metadata is None:
return None
cross_filters = metadata.get("cross_filters_enabled")
if isinstance(cross_filters, bool):
return cross_filters
return None


def _build_omitted_fields(
json_metadata_str: str | None, position_json_str: str | None
) -> Dict[str, str]:
"""Build omission metadata describing which fields were stripped and why.

Uses the shared OmittedFieldsBuilder utility so the pattern is consistent
across all MCP tool serializers.
"""
from superset.mcp_service.utils.response_utils import OmittedFieldsBuilder

return (
OmittedFieldsBuilder()
.add_raw_field(
"position_json",
raw_value=position_json_str,
reason=(
"Internal layout tree with component positions/hierarchy. "
"Not useful for analysis or LLM context."
),
)
.add_extracted_field(
"json_metadata",
raw_value=json_metadata_str,
reason=(
"native_filters and cross_filters_enabled extracted into "
"dedicated fields above."
),
)
.build()
)


def serialize_chart_summary(chart: Any) -> DashboardChartSummary | None:
"""Serialize a chart to a lightweight summary for dashboard context."""
if not chart:
return None
from superset.mcp_service.utils.url_utils import get_superset_base_url

chart_id = getattr(chart, "id", None)
chart_url = None
if chart_id is not None:
chart_url = f"{get_superset_base_url()}/explore/?slice_id={chart_id}"

return DashboardChartSummary(
id=chart_id,
slice_name=getattr(chart, "slice_name", None),
viz_type=getattr(chart, "viz_type", None),
datasource_name=getattr(chart, "datasource_name", None),
url=chart_url,
description=getattr(chart, "description", None),
)


def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
from superset.mcp_service.utils.url_utils import get_superset_base_url

Expand All @@ -488,8 +664,6 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
css=dashboard.css,
certified_by=dashboard.certified_by,
certification_details=dashboard.certification_details,
json_metadata=dashboard.json_metadata,
position_json=dashboard.position_json,
published=dashboard.published,
is_managed_externally=dashboard.is_managed_externally,
external_url=dashboard.external_url,
Expand All @@ -506,6 +680,16 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
created_on_humanized=dashboard.created_on_humanized,
changed_on_humanized=dashboard.changed_on_humanized,
chart_count=len(dashboard.slices) if dashboard.slices else 0,
native_filters=_extract_native_filters(
getattr(dashboard, "json_metadata", None)
),
cross_filters_enabled=_extract_cross_filters_enabled(
getattr(dashboard, "json_metadata", None)
),
omitted_fields=_build_omitted_fields(
getattr(dashboard, "json_metadata", None),
getattr(dashboard, "position_json", None),
),
Comment thread
aminghadersohi marked this conversation as resolved.
owners=[
info
for owner in dashboard.owners
Expand All @@ -524,7 +708,11 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
]
if dashboard.roles
else [],
charts=[serialize_chart_object(chart) for chart in dashboard.slices]
charts=[
summary
for chart in dashboard.slices
if (summary := serialize_chart_summary(chart)) is not None
]
if dashboard.slices
else [],
)
Expand All @@ -551,6 +739,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
f"{get_superset_base_url()}/superset/dashboard/{slug or dashboard_id}/"
)

json_metadata_str = getattr(dashboard, "json_metadata", None)
position_json_str = getattr(dashboard, "position_json", None)

return DashboardInfo(
id=dashboard_id,
dashboard_title=getattr(dashboard, "dashboard_title", None),
Expand All @@ -571,8 +762,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
css=getattr(dashboard, "css", None),
certified_by=getattr(dashboard, "certified_by", None),
certification_details=getattr(dashboard, "certification_details", None),
json_metadata=getattr(dashboard, "json_metadata", None),
position_json=getattr(dashboard, "position_json", None),
native_filters=_extract_native_filters(json_metadata_str),
cross_filters_enabled=_extract_cross_filters_enabled(json_metadata_str),
omitted_fields=_build_omitted_fields(json_metadata_str, position_json_str),
is_managed_externally=getattr(dashboard, "is_managed_externally", None),
external_url=getattr(dashboard, "external_url", None),
uuid=str(getattr(dashboard, "uuid", ""))
Expand All @@ -599,7 +791,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
if getattr(dashboard, "roles", None)
else [],
charts=[
serialize_chart_object(chart) for chart in getattr(dashboard, "slices", [])
summary
for chart in getattr(dashboard, "slices", [])
if (summary := serialize_chart_summary(chart)) is not None
]
if getattr(dashboard, "slices", None)
else [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

from superset.commands.exceptions import CommandException
from superset.extensions import event_logger
from superset.mcp_service.chart.schemas import serialize_chart_object
from superset.mcp_service.dashboard.constants import (
generate_id,
GRID_COLUMN_COUNT,
Expand All @@ -41,6 +40,7 @@
AddChartToDashboardRequest,
AddChartToDashboardResponse,
DashboardInfo,
serialize_chart_summary,
)
from superset.mcp_service.utils.url_utils import get_superset_base_url
from superset.utils import json
Expand Down Expand Up @@ -525,7 +525,7 @@ def add_chart_to_existing_dashboard(
charts=[
obj
for chart in getattr(updated_dashboard, "slices", [])
if (obj := serialize_chart_object(chart)) is not None
if (obj := serialize_chart_summary(chart)) is not None
],
)

Expand Down
4 changes: 2 additions & 2 deletions superset/mcp_service/dashboard/tool/generate_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.chart.schemas import serialize_chart_object
from superset.mcp_service.dashboard.constants import (
generate_id,
GRID_COLUMN_COUNT,
Expand Down Expand Up @@ -395,6 +394,7 @@ def generate_dashboard( # noqa: C901

# Convert to our response format
from superset.mcp_service.dashboard.schemas import (
serialize_chart_summary,
serialize_tag_object,
serialize_user_object,
)
Comment thread
aminghadersohi marked this conversation as resolved.
Expand Down Expand Up @@ -426,7 +426,7 @@ def generate_dashboard( # noqa: C901
charts=[
obj
for chart in getattr(dashboard, "slices", [])
if (obj := serialize_chart_object(chart)) is not None
if (obj := serialize_chart_summary(chart)) is not None
],
Comment thread
aminghadersohi marked this conversation as resolved.
)

Expand Down
4 changes: 0 additions & 4 deletions superset/mcp_service/utils/permissions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
"created_by_fk", # Internal user references
},
"dashboard": {
"json_metadata", # May contain sensitive configuration
"position_json", # Internal layout data
"css", # May contain sensitive styling info
"changed_by_fk", # Internal user references
"created_by_fk", # Internal user references
Expand All @@ -64,8 +62,6 @@
"database_id": "can_this_form_get", # Database access permissions
"query_context": "can_explore_json", # Explore permissions
"cache_key": "can_warm_up_cache", # Cache management permissions
"json_metadata": "can_this_form_get", # Advanced dashboard permissions
"position_json": "can_this_form_get", # Dashboard edit permissions
"css": "can_this_form_get", # Dashboard styling permissions
}

Expand Down
Loading
Loading