2828 dashboard_title="Sales Dashboard",
2929 published=True,
3030 owners=[UserInfo(id=1, username="admin")],
31- charts=[ChartInfo (id=1, slice_name="Sales Chart")]
31+ charts=[DashboardChartSummary (id=1, slice_name="Sales Chart")]
3232 )
3333
3434 # For dashboard list responses
6565
6666from __future__ import annotations
6767
68+ import logging
6869from datetime import datetime
6970from typing import Annotated , Any , Dict , List , Literal , TYPE_CHECKING
7071
8384 from superset .models .dashboard import Dashboard
8485
8586from superset .daos .base import ColumnOperator , ColumnOperatorEnum
86- from superset .mcp_service .chart .schemas import ChartInfo , serialize_chart_object
8787from superset .mcp_service .common .cache_schemas import MetadataCacheControl
8888from superset .mcp_service .constants import DEFAULT_PAGE_SIZE , MAX_PAGE_SIZE
8989from superset .mcp_service .system .schemas import (
9797 _remove_dangerous_unicode ,
9898 _strip_html_tags ,
9999)
100+ from superset .utils .json import loads as json_loads
100101
101102
102103class DashboardError (BaseModel ):
@@ -298,6 +299,43 @@ class GetDashboardInfoRequest(MetadataCacheControl):
298299 )
299300
300301
302+ logger = logging .getLogger (__name__ )
303+
304+
305+ class NativeFilterSummary (BaseModel ):
306+ """Lightweight summary of a native filter for LLM consumption.
307+
308+ Extracts only the fields needed to understand what filters are
309+ available on a dashboard: name, type, and which columns they target.
310+ """
311+
312+ id : str | None = Field (None , description = "Filter ID" )
313+ name : str | None = Field (None , description = "Filter display name" )
314+ filter_type : str | None = Field (
315+ None , description = "Filter type (e.g. filter_select, filter_range)"
316+ )
317+ targets : List [Dict [str , Any ]] = Field (
318+ default_factory = list ,
319+ description = "Filter targets (column name and dataset ID)" ,
320+ )
321+
322+
323+ class DashboardChartSummary (BaseModel ):
324+ """Lightweight chart representation for dashboard context.
325+
326+ Contains only the fields needed for LLMs to understand which charts
327+ are on a dashboard, omitting verbose fields like form_data, tags,
328+ owners, and timestamps that bloat the response.
329+ """
330+
331+ id : int | None = Field (None , description = "Chart ID" )
332+ slice_name : str | None = Field (None , description = "Chart name" )
333+ viz_type : str | None = Field (None , description = "Visualization type" )
334+ datasource_name : str | None = Field (None , description = "Datasource name" )
335+ url : str | None = Field (None , description = "Chart explore page URL" )
336+ description : str | None = Field (None , description = "Chart description" )
337+
338+
301339class DashboardInfo (BaseModel ):
302340 id : int | None = None
303341 dashboard_title : str | None = None
@@ -306,8 +344,6 @@ class DashboardInfo(BaseModel):
306344 css : str | None = None
307345 certified_by : str | None = None
308346 certification_details : str | None = None
309- json_metadata : str | None = None
310- position_json : str | None = None
311347 published : bool | None = None
312348 is_managed_externally : bool | None = None
313349 external_url : str | None = None
@@ -323,7 +359,32 @@ class DashboardInfo(BaseModel):
323359 owners : List [UserInfo ] = Field (default_factory = list )
324360 tags : List [TagInfo ] = Field (default_factory = list )
325361 roles : List [RoleInfo ] = Field (default_factory = list )
326- charts : List [ChartInfo ] = Field (default_factory = list )
362+ charts : List [DashboardChartSummary ] = Field (default_factory = list )
363+
364+ # Structured filter information extracted from json_metadata
365+ native_filters : List [NativeFilterSummary ] = Field (
366+ default_factory = list ,
367+ description = (
368+ "Native filters configured on this dashboard. Extracted from "
369+ "json_metadata for LLM consumption — includes filter name, type, "
370+ "and target columns."
371+ ),
372+ )
373+ cross_filters_enabled : bool | None = Field (
374+ None ,
375+ description = "Whether cross-filtering between charts is enabled." ,
376+ )
377+
378+ # Omission metadata — tells the agent what was stripped and why
379+ omitted_fields : Dict [str , str ] = Field (
380+ default_factory = dict ,
381+ description = (
382+ "Fields omitted from this response to reduce size. Keys are field "
383+ "names, values describe what was omitted and how to access the full "
384+ "data. Useful filter information has been extracted into "
385+ "native_filters and cross_filters_enabled above."
386+ ),
387+ )
327388
328389 # Fields for permalink/filter state support
329390 permalink_key : str | None = Field (
@@ -473,6 +534,121 @@ class GenerateDashboardResponse(BaseModel):
473534 error : str | None = Field (None , description = "Error message, if creation failed" )
474535
475536
537+ def _parse_json_metadata (json_metadata_str : str | None ) -> Dict [str , Any ] | None :
538+ """Parse json_metadata string into a dict, returning None on any failure.
539+
540+ Handles None/empty input, invalid JSON, and non-dict JSON values
541+ (e.g. ``"[]"``, ``"123"``) by returning None instead of raising.
542+ """
543+ if not json_metadata_str :
544+ return None
545+ try :
546+ metadata = json_loads (json_metadata_str )
547+ except (ValueError , TypeError ):
548+ return None
549+ if not isinstance (metadata , dict ):
550+ return None
551+ return metadata
552+
553+
554+ def _extract_native_filters (json_metadata_str : str | None ) -> List [NativeFilterSummary ]:
555+ """Extract native filter summaries from raw json_metadata string.
556+
557+ Parses the json_metadata JSON blob and pulls out only the filter
558+ name, type, and targets — dropping verbose fields like controlValues,
559+ defaultDataMask, scope, and cascadeParentIds.
560+ """
561+ metadata = _parse_json_metadata (json_metadata_str )
562+ if metadata is None :
563+ return []
564+
565+ native_filters = metadata .get ("native_filter_configuration" , [])
566+ if not isinstance (native_filters , list ):
567+ return []
568+
569+ summaries : List [NativeFilterSummary ] = []
570+ for f in native_filters :
571+ if not isinstance (f , dict ):
572+ continue
573+ raw_targets = f .get ("targets" , [])
574+ if not isinstance (raw_targets , list ):
575+ raw_targets = []
576+ targets = [t for t in raw_targets if isinstance (t , dict )]
577+ summaries .append (
578+ NativeFilterSummary (
579+ id = f .get ("id" ),
580+ name = f .get ("name" ),
581+ filter_type = f .get ("filterType" ),
582+ targets = targets ,
583+ )
584+ )
585+ return summaries
586+
587+
588+ def _extract_cross_filters_enabled (json_metadata_str : str | None ) -> bool | None :
589+ """Extract the cross_filters_enabled flag from json_metadata."""
590+ metadata = _parse_json_metadata (json_metadata_str )
591+ if metadata is None :
592+ return None
593+ cross_filters = metadata .get ("cross_filters_enabled" )
594+ if isinstance (cross_filters , bool ):
595+ return cross_filters
596+ return None
597+
598+
599+ def _build_omitted_fields (
600+ json_metadata_str : str | None , position_json_str : str | None
601+ ) -> Dict [str , str ]:
602+ """Build omission metadata describing which fields were stripped and why.
603+
604+ Uses the shared OmittedFieldsBuilder utility so the pattern is consistent
605+ across all MCP tool serializers.
606+ """
607+ from superset .mcp_service .utils .response_utils import OmittedFieldsBuilder
608+
609+ return (
610+ OmittedFieldsBuilder ()
611+ .add_raw_field (
612+ "position_json" ,
613+ raw_value = position_json_str ,
614+ reason = (
615+ "Internal layout tree with component positions/hierarchy. "
616+ "Not useful for analysis or LLM context."
617+ ),
618+ )
619+ .add_extracted_field (
620+ "json_metadata" ,
621+ raw_value = json_metadata_str ,
622+ reason = (
623+ "native_filters and cross_filters_enabled extracted into "
624+ "dedicated fields above."
625+ ),
626+ )
627+ .build ()
628+ )
629+
630+
631+ def serialize_chart_summary (chart : Any ) -> DashboardChartSummary | None :
632+ """Serialize a chart to a lightweight summary for dashboard context."""
633+ if not chart :
634+ return None
635+ from superset .mcp_service .utils .url_utils import get_superset_base_url
636+
637+ chart_id = getattr (chart , "id" , None )
638+ chart_url = None
639+ if chart_id is not None :
640+ chart_url = f"{ get_superset_base_url ()} /explore/?slice_id={ chart_id } "
641+
642+ return DashboardChartSummary (
643+ id = chart_id ,
644+ slice_name = getattr (chart , "slice_name" , None ),
645+ viz_type = getattr (chart , "viz_type" , None ),
646+ datasource_name = getattr (chart , "datasource_name" , None ),
647+ url = chart_url ,
648+ description = getattr (chart , "description" , None ),
649+ )
650+
651+
476652def dashboard_serializer (dashboard : "Dashboard" ) -> DashboardInfo :
477653 from superset .mcp_service .utils .url_utils import get_superset_base_url
478654
@@ -488,8 +664,6 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
488664 css = dashboard .css ,
489665 certified_by = dashboard .certified_by ,
490666 certification_details = dashboard .certification_details ,
491- json_metadata = dashboard .json_metadata ,
492- position_json = dashboard .position_json ,
493667 published = dashboard .published ,
494668 is_managed_externally = dashboard .is_managed_externally ,
495669 external_url = dashboard .external_url ,
@@ -506,6 +680,16 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
506680 created_on_humanized = dashboard .created_on_humanized ,
507681 changed_on_humanized = dashboard .changed_on_humanized ,
508682 chart_count = len (dashboard .slices ) if dashboard .slices else 0 ,
683+ native_filters = _extract_native_filters (
684+ getattr (dashboard , "json_metadata" , None )
685+ ),
686+ cross_filters_enabled = _extract_cross_filters_enabled (
687+ getattr (dashboard , "json_metadata" , None )
688+ ),
689+ omitted_fields = _build_omitted_fields (
690+ getattr (dashboard , "json_metadata" , None ),
691+ getattr (dashboard , "position_json" , None ),
692+ ),
509693 owners = [
510694 info
511695 for owner in dashboard .owners
@@ -524,7 +708,11 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
524708 ]
525709 if dashboard .roles
526710 else [],
527- charts = [serialize_chart_object (chart ) for chart in dashboard .slices ]
711+ charts = [
712+ summary
713+ for chart in dashboard .slices
714+ if (summary := serialize_chart_summary (chart )) is not None
715+ ]
528716 if dashboard .slices
529717 else [],
530718 )
@@ -551,6 +739,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
551739 f"{ get_superset_base_url ()} /superset/dashboard/{ slug or dashboard_id } /"
552740 )
553741
742+ json_metadata_str = getattr (dashboard , "json_metadata" , None )
743+ position_json_str = getattr (dashboard , "position_json" , None )
744+
554745 return DashboardInfo (
555746 id = dashboard_id ,
556747 dashboard_title = getattr (dashboard , "dashboard_title" , None ),
@@ -571,8 +762,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
571762 css = getattr (dashboard , "css" , None ),
572763 certified_by = getattr (dashboard , "certified_by" , None ),
573764 certification_details = getattr (dashboard , "certification_details" , None ),
574- json_metadata = getattr (dashboard , "json_metadata" , None ),
575- position_json = getattr (dashboard , "position_json" , None ),
765+ native_filters = _extract_native_filters (json_metadata_str ),
766+ cross_filters_enabled = _extract_cross_filters_enabled (json_metadata_str ),
767+ omitted_fields = _build_omitted_fields (json_metadata_str , position_json_str ),
576768 is_managed_externally = getattr (dashboard , "is_managed_externally" , None ),
577769 external_url = getattr (dashboard , "external_url" , None ),
578770 uuid = str (getattr (dashboard , "uuid" , "" ))
@@ -599,7 +791,9 @@ def serialize_dashboard_object(dashboard: Any) -> DashboardInfo:
599791 if getattr (dashboard , "roles" , None )
600792 else [],
601793 charts = [
602- serialize_chart_object (chart ) for chart in getattr (dashboard , "slices" , [])
794+ summary
795+ for chart in getattr (dashboard , "slices" , [])
796+ if (summary := serialize_chart_summary (chart )) is not None
603797 ]
604798 if getattr (dashboard , "slices" , None )
605799 else [],
0 commit comments