diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py index 31bd6664423..348846ca8fb 100644 --- a/superset/mcp_service/chart/tool/generate_chart.py +++ b/superset/mcp_service/chart/tool/generate_chart.py @@ -25,6 +25,7 @@ from typing import Any, Dict, List from urllib.parse import parse_qs, urlparse from fastmcp import Context +from mcp.types import ToolAnnotations from superset.commands.exceptions import CommandException from superset.extensions import event_logger @@ -119,7 +120,14 @@ def _compile_chart( return CompileResult(success=False, error=str(exc)) -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Generate chart", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart", method_permission_name="write") @parse_request(GenerateChartRequest) async def generate_chart( # noqa: C901 diff --git a/superset/mcp_service/chart/tool/get_chart_data.py b/superset/mcp_service/chart/tool/get_chart_data.py index 50200bea294..431f63daf19 100644 --- a/superset/mcp_service/chart/tool/get_chart_data.py +++ b/superset/mcp_service/chart/tool/get_chart_data.py @@ -25,6 +25,7 @@ from typing import Any, Dict, List, TYPE_CHECKING from fastmcp import Context from flask import current_app +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -76,7 +77,14 @@ def _get_cached_form_data(form_data_key: str) -> str | None: return None -@mcp.tool(tags=["data"]) +@mcp.tool( + tags=["data"], + annotations=ToolAnnotations( + title="Get chart data", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart") @parse_request(GetChartDataRequest) async def get_chart_data( # noqa: C901 diff --git a/superset/mcp_service/chart/tool/get_chart_info.py b/superset/mcp_service/chart/tool/get_chart_info.py index 393439b08b7..3020965fa8a 100644 --- a/superset/mcp_service/chart/tool/get_chart_info.py +++ b/superset/mcp_service/chart/tool/get_chart_info.py @@ -22,6 +22,7 @@ MCP tool: get_chart_info import logging from fastmcp import Context +from mcp.types import ToolAnnotations from sqlalchemy.orm import subqueryload from superset.commands.exceptions import CommandException @@ -57,7 +58,14 @@ def _get_cached_form_data(form_data_key: str) -> str | None: return None -@mcp.tool(tags=["discovery"]) +@mcp.tool( + tags=["discovery"], + annotations=ToolAnnotations( + title="Get chart info", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart") @parse_request(GetChartInfoRequest) async def get_chart_info( diff --git a/superset/mcp_service/chart/tool/get_chart_preview.py b/superset/mcp_service/chart/tool/get_chart_preview.py index cadadc2a0c4..bf0b8360f05 100644 --- a/superset/mcp_service/chart/tool/get_chart_preview.py +++ b/superset/mcp_service/chart/tool/get_chart_preview.py @@ -23,6 +23,7 @@ import logging from typing import Any, Dict, List, Protocol from fastmcp import Context +from mcp.types import ToolAnnotations from superset.commands.exceptions import CommandException from superset.exceptions import SupersetException @@ -2072,7 +2073,14 @@ async def _get_chart_preview_internal( # noqa: C901 ) -@mcp.tool(tags=["data"]) +@mcp.tool( + tags=["data"], + annotations=ToolAnnotations( + title="Get chart preview", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart") @parse_request(GetChartPreviewRequest) async def get_chart_preview( diff --git a/superset/mcp_service/chart/tool/list_charts.py b/superset/mcp_service/chart/tool/list_charts.py index 49a84342424..a93c7bdce12 100644 --- a/superset/mcp_service/chart/tool/list_charts.py +++ b/superset/mcp_service/chart/tool/list_charts.py @@ -23,6 +23,7 @@ import logging from typing import cast, TYPE_CHECKING from fastmcp import Context +from mcp.types import ToolAnnotations if TYPE_CHECKING: from superset.models.slice import Slice @@ -63,7 +64,14 @@ SORTABLE_CHART_COLUMNS = [ ] -@mcp.tool(tags=["core"]) +@mcp.tool( + tags=["core"], + annotations=ToolAnnotations( + title="List charts", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart") @parse_request(ListChartsRequest) async def list_charts(request: ListChartsRequest, ctx: Context) -> ChartList: diff --git a/superset/mcp_service/chart/tool/update_chart.py b/superset/mcp_service/chart/tool/update_chart.py index 6e41783f0db..2cfce0a89c9 100644 --- a/superset/mcp_service/chart/tool/update_chart.py +++ b/superset/mcp_service/chart/tool/update_chart.py @@ -23,6 +23,7 @@ import logging import time from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -46,7 +47,14 @@ from superset.utils import json logger = logging.getLogger(__name__) -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Update chart", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart", method_permission_name="write") @parse_request(UpdateChartRequest) async def update_chart( diff --git a/superset/mcp_service/chart/tool/update_chart_preview.py b/superset/mcp_service/chart/tool/update_chart_preview.py index 992bcd75f5b..aefb35d2cab 100644 --- a/superset/mcp_service/chart/tool/update_chart_preview.py +++ b/superset/mcp_service/chart/tool/update_chart_preview.py @@ -24,6 +24,7 @@ import time from typing import Any, Dict from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -45,7 +46,14 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Update chart preview", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Chart", method_permission_name="write") @parse_request(UpdateChartPreviewRequest) def update_chart_preview( diff --git a/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py b/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py index 83967209c05..7052fadd337 100644 --- a/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py +++ b/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py @@ -26,6 +26,7 @@ import re from typing import Any, Dict from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -307,7 +308,14 @@ def _ensure_layout_structure( layout["DASHBOARD_VERSION_KEY"] = "v2" -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Add chart to existing dashboard", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dashboard", method_permission_name="write") @parse_request(AddChartToDashboardRequest) def add_chart_to_existing_dashboard( diff --git a/superset/mcp_service/dashboard/tool/generate_dashboard.py b/superset/mcp_service/dashboard/tool/generate_dashboard.py index 5f0ed4ee5cc..353cbe3b95f 100644 --- a/superset/mcp_service/dashboard/tool/generate_dashboard.py +++ b/superset/mcp_service/dashboard/tool/generate_dashboard.py @@ -25,6 +25,7 @@ import logging from typing import Any, Dict, List from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -179,7 +180,14 @@ def _generate_title_from_charts(chart_objects: List[Any]) -> str: return title -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Generate dashboard", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dashboard", method_permission_name="write") @parse_request(GenerateDashboardRequest) def generate_dashboard( diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_info.py b/superset/mcp_service/dashboard/tool/get_dashboard_info.py index b062cc1e2a7..94fe67002e9 100644 --- a/superset/mcp_service/dashboard/tool/get_dashboard_info.py +++ b/superset/mcp_service/dashboard/tool/get_dashboard_info.py @@ -26,6 +26,7 @@ import logging from datetime import datetime, timezone from fastmcp import Context +from mcp.types import ToolAnnotations from sqlalchemy.orm import subqueryload from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError @@ -60,7 +61,14 @@ def _get_permalink_state(permalink_key: str) -> DashboardPermalinkValue | None: return None -@mcp.tool(tags=["discovery"]) +@mcp.tool( + tags=["discovery"], + annotations=ToolAnnotations( + title="Get dashboard info", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dashboard") @parse_request(GetDashboardInfoRequest) async def get_dashboard_info( diff --git a/superset/mcp_service/dashboard/tool/list_dashboards.py b/superset/mcp_service/dashboard/tool/list_dashboards.py index bb75ffb0653..257cf8a7c0c 100644 --- a/superset/mcp_service/dashboard/tool/list_dashboards.py +++ b/superset/mcp_service/dashboard/tool/list_dashboards.py @@ -26,6 +26,7 @@ import logging from typing import TYPE_CHECKING from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -64,7 +65,14 @@ SORTABLE_DASHBOARD_COLUMNS = [ ] -@mcp.tool(tags=["core"]) +@mcp.tool( + tags=["core"], + annotations=ToolAnnotations( + title="List dashboards", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dashboard") @parse_request(ListDashboardsRequest) async def list_dashboards( diff --git a/superset/mcp_service/dataset/tool/get_dataset_info.py b/superset/mcp_service/dataset/tool/get_dataset_info.py index 01072d5c046..699c1d0b446 100644 --- a/superset/mcp_service/dataset/tool/get_dataset_info.py +++ b/superset/mcp_service/dataset/tool/get_dataset_info.py @@ -26,6 +26,7 @@ import logging from datetime import datetime, timezone from fastmcp import Context +from mcp.types import ToolAnnotations from sqlalchemy.orm import joinedload, subqueryload from superset.extensions import event_logger @@ -43,7 +44,14 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool(tags=["discovery"]) +@mcp.tool( + tags=["discovery"], + annotations=ToolAnnotations( + title="Get dataset info", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dataset") @parse_request(GetDatasetInfoRequest) async def get_dataset_info( diff --git a/superset/mcp_service/dataset/tool/list_datasets.py b/superset/mcp_service/dataset/tool/list_datasets.py index 1879171aeb3..c84b2cc98ca 100644 --- a/superset/mcp_service/dataset/tool/list_datasets.py +++ b/superset/mcp_service/dataset/tool/list_datasets.py @@ -26,6 +26,7 @@ import logging from typing import TYPE_CHECKING from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -62,7 +63,14 @@ SORTABLE_DATASET_COLUMNS = [ ] -@mcp.tool(tags=["core"]) +@mcp.tool( + tags=["core"], + annotations=ToolAnnotations( + title="List datasets", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Dataset") @parse_request(ListDatasetsRequest) async def list_datasets(request: ListDatasetsRequest, ctx: Context) -> DatasetList: diff --git a/superset/mcp_service/explore/tool/generate_explore_link.py b/superset/mcp_service/explore/tool/generate_explore_link.py index db131fdb9d0..ec044c6a436 100644 --- a/superset/mcp_service/explore/tool/generate_explore_link.py +++ b/superset/mcp_service/explore/tool/generate_explore_link.py @@ -26,6 +26,7 @@ from typing import Any, Dict from urllib.parse import parse_qs, urlparse from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -40,7 +41,14 @@ from superset.mcp_service.chart.schemas import ( from superset.mcp_service.utils.schema_utils import parse_request -@mcp.tool(tags=["explore"]) +@mcp.tool( + tags=["explore"], + annotations=ToolAnnotations( + title="Generate explore link", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="Explore") @parse_request(GenerateExploreLinkRequest) async def generate_explore_link( diff --git a/superset/mcp_service/sql_lab/tool/execute_sql.py b/superset/mcp_service/sql_lab/tool/execute_sql.py index ad36059845a..6b3bd5dc527 100644 --- a/superset/mcp_service/sql_lab/tool/execute_sql.py +++ b/superset/mcp_service/sql_lab/tool/execute_sql.py @@ -25,6 +25,7 @@ and timeout protection. import logging from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -39,7 +40,14 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Execute SQL query", + readOnlyHint=False, + destructiveHint=True, + ), +) @mcp_auth_hook( class_permission_name="SQLLab", method_permission_name="execute_sql_query" ) diff --git a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py index cae32de24e9..a04bd51566d 100644 --- a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py +++ b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py @@ -25,6 +25,7 @@ import logging from urllib.parse import urlencode from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -38,7 +39,14 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool(tags=["explore"]) +@mcp.tool( + tags=["explore"], + annotations=ToolAnnotations( + title="Open SQL Lab with context", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="SQLLab") @parse_request(OpenSqlLabRequest) def open_sql_lab_with_context( diff --git a/superset/mcp_service/sql_lab/tool/save_sql_query.py b/superset/mcp_service/sql_lab/tool/save_sql_query.py index 979d4feb1ff..f65322b6781 100644 --- a/superset/mcp_service/sql_lab/tool/save_sql_query.py +++ b/superset/mcp_service/sql_lab/tool/save_sql_query.py @@ -28,6 +28,7 @@ from __future__ import annotations import logging from fastmcp import Context +from mcp.types import ToolAnnotations from sqlalchemy.exc import SQLAlchemyError from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -44,7 +45,14 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool(tags=["mutate"]) +@mcp.tool( + tags=["mutate"], + annotations=ToolAnnotations( + title="Save SQL query", + readOnlyHint=False, + destructiveHint=False, + ), +) @mcp_auth_hook(class_permission_name="SavedQuery", method_permission_name="write") @parse_request(SaveSqlQueryRequest) async def save_sql_query( @@ -131,7 +139,7 @@ async def save_sql_query( except SQLAlchemyError as e: from superset import db - db.session.rollback() + db.session.rollback() # pylint: disable=consider-using-transaction await ctx.error( "Failed to save SQL query: error=%s, database_id=%s" % (str(e), request.database_id) diff --git a/superset/mcp_service/system/tool/get_instance_info.py b/superset/mcp_service/system/tool/get_instance_info.py index 599aefad95b..3c26cc0cc22 100644 --- a/superset/mcp_service/system/tool/get_instance_info.py +++ b/superset/mcp_service/system/tool/get_instance_info.py @@ -23,6 +23,7 @@ InstanceInfoCore for flexible, extensible metrics calculation. import logging from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -74,7 +75,14 @@ _instance_info_core = InstanceInfoCore( ) -@mcp.tool(tags=["core"]) +@mcp.tool( + tags=["core"], + annotations=ToolAnnotations( + title="Get instance info", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook @parse_request(GetSupersetInstanceInfoRequest) def get_instance_info( diff --git a/superset/mcp_service/system/tool/get_schema.py b/superset/mcp_service/system/tool/get_schema.py index 7fa6af495e6..f0557408daf 100644 --- a/superset/mcp_service/system/tool/get_schema.py +++ b/superset/mcp_service/system/tool/get_schema.py @@ -27,6 +27,7 @@ import logging from typing import Callable, Literal from fastmcp import Context +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -121,7 +122,14 @@ _SCHEMA_CORE_FACTORIES: dict[ } -@mcp.tool(tags=["discovery"]) +@mcp.tool( + tags=["discovery"], + annotations=ToolAnnotations( + title="Get schema", + readOnlyHint=True, + destructiveHint=False, + ), +) @parse_request(GetSchemaRequest) async def get_schema(request: GetSchemaRequest, ctx: Context) -> GetSchemaResponse: """ diff --git a/superset/mcp_service/system/tool/health_check.py b/superset/mcp_service/system/tool/health_check.py index de465ef3930..839fe44382e 100644 --- a/superset/mcp_service/system/tool/health_check.py +++ b/superset/mcp_service/system/tool/health_check.py @@ -23,6 +23,7 @@ import platform import time from flask import current_app +from mcp.types import ToolAnnotations from superset.extensions import event_logger from superset.mcp_service.app import mcp @@ -35,7 +36,14 @@ logger = logging.getLogger(__name__) _start_time = time.monotonic() -@mcp.tool(tags=["core"]) +@mcp.tool( + tags=["core"], + annotations=ToolAnnotations( + title="Health check", + readOnlyHint=True, + destructiveHint=False, + ), +) @mcp_auth_hook async def health_check() -> HealthCheckResponse: """