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
11 changes: 11 additions & 0 deletions superset-core/src/superset_core/mcp/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ def another_tool(value: int) -> str:

from typing import Any, Callable, TypeVar

try:
from mcp.types import ToolAnnotations
except (
ImportError
): # MCP extras may not be installed in superset-core-only environments
ToolAnnotations = dict

# Type variable for decorated functions
F = TypeVar("F", bound=Callable[..., Any])

Expand All @@ -50,6 +57,7 @@ def tool(
protect: bool = True,
class_permission_name: str | None = None,
method_permission_name: str | None = None,
annotations: ToolAnnotations | None = None,
) -> Any: # Use Any to avoid mypy issues with dependency injection
"""
Decorator to register an MCP tool with optional authentication.
Expand Down Expand Up @@ -77,6 +85,8 @@ def my_tool(): ...
permission checking via security_manager.can_access().
method_permission_name: FAB action name (e.g., "read", "write").
Defaults to "write" if tags includes "mutate", else "read".
annotations: MCP tool annotations (title, readOnlyHint, destructiveHint, etc.)
These hints help MCP clients understand tool behavior and safety.
Returns:
Decorator function that registers and wraps the tool, or the wrapped function
Expand Down Expand Up @@ -178,4 +188,5 @@ async def public_prompt_handler(ctx: Context) -> str:
__all__ = [
"tool",
"prompt",
"ToolAnnotations",
]
8 changes: 8 additions & 0 deletions superset/core/mcp/core_mcp_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
import logging
from typing import Any, Callable, Optional, TypeVar

try:
from mcp.types import ToolAnnotations
except ImportError:
ToolAnnotations = dict

from superset.extensions.context import get_current_extension_context

# Type variable for decorated functions
Expand Down Expand Up @@ -62,6 +67,7 @@ def create_tool_decorator(
protect: bool = True,
class_permission_name: Optional[str] = None,
method_permission_name: Optional[str] = None,
annotations: ToolAnnotations | None = None,
) -> Callable[[F], F] | F:
"""
Create the concrete MCP tool decorator implementation.
Expand All @@ -82,6 +88,7 @@ def create_tool_decorator(
(e.g., "Chart", "Dashboard", "SQLLab"). Enables permission checking.
method_permission_name: FAB action name (e.g., "read", "write").
Defaults to "write" if tags has "mutate", else "read".
annotations: MCP tool annotations (title, readOnlyHint, destructiveHint, etc.)

Returns:
Decorator that registers and wraps the tool with optional authentication,
Expand Down Expand Up @@ -130,6 +137,7 @@ def decorator(func: F) -> F:
name=tool_name,
description=tool_description,
tags=tool_tags,
annotations=annotations,
)
mcp.add_tool(tool)

Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/generate_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from urllib.parse import parse_qs, urlparse

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.commands.exceptions import CommandException
from superset.extensions import event_logger
Expand Down Expand Up @@ -119,7 +119,15 @@ def _compile_chart(
return CompileResult(success=False, error=str(exc))


@tool(tags=["mutate"], class_permission_name="Chart")
@tool(
tags=["mutate"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="Create chart",
readOnlyHint=False,
destructiveHint=False,
),
)
@parse_request(GenerateChartRequest)
async def generate_chart( # noqa: C901
request: GenerateChartRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/get_chart_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from fastmcp import Context
from flask import current_app
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

if TYPE_CHECKING:
from superset.models.slice import Slice
Expand Down Expand Up @@ -74,7 +74,15 @@ def _get_cached_form_data(form_data_key: str) -> str | None:
return None


@tool(tags=["data"], class_permission_name="Chart")
@tool(
tags=["data"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="Get chart data",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(GetChartDataRequest)
async def get_chart_data( # noqa: C901
request: GetChartDataRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/get_chart_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from fastmcp import Context
from sqlalchemy.orm import subqueryload
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.commands.exceptions import CommandException
from superset.commands.explore.form_data.parameters import CommandParameters
Expand Down Expand Up @@ -115,7 +115,15 @@ def _apply_unsaved_state_override(result: ChartInfo, form_data_key: str) -> None
)


@tool(tags=["discovery"], class_permission_name="Chart")
@tool(
tags=["discovery"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="Get chart info",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(GetChartInfoRequest)
async def get_chart_info(
request: GetChartInfoRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/get_chart_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from typing import Any, Dict, List, Protocol

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.commands.exceptions import CommandException
from superset.exceptions import SupersetException
Expand Down Expand Up @@ -2177,7 +2177,15 @@ def __init__(self, form_data: Dict[str, Any]):
)


@tool(tags=["data"], class_permission_name="Chart")
@tool(
tags=["data"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="Get chart preview",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(GetChartPreviewRequest)
async def get_chart_preview(
request: GetChartPreviewRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/list_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from typing import cast, TYPE_CHECKING

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

if TYPE_CHECKING:
from superset.models.slice import Slice
Expand Down Expand Up @@ -62,7 +62,15 @@
]


@tool(tags=["core"], class_permission_name="Chart")
@tool(
tags=["core"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="List charts",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(ListChartsRequest)
async def list_charts(request: ListChartsRequest, ctx: Context) -> ChartList:
"""List charts with filtering and search.
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/chart/tool/update_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import time

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.chart.chart_utils import (
Expand All @@ -45,7 +45,15 @@
logger = logging.getLogger(__name__)


@tool(tags=["mutate"], class_permission_name="Chart")
@tool(
tags=["mutate"],
class_permission_name="Chart",
annotations=ToolAnnotations(
title="Update chart",
readOnlyHint=False,
destructiveHint=True,
),
)
@parse_request(UpdateChartRequest)
async def update_chart(
request: UpdateChartRequest, ctx: Context
Expand Down
13 changes: 11 additions & 2 deletions superset/mcp_service/chart/tool/update_chart_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Any, Dict

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.chart.chart_utils import (
Expand All @@ -44,7 +44,16 @@
logger = logging.getLogger(__name__)


@tool(tags=["mutate"], class_permission_name="Chart", method_permission_name="write")
@tool(
tags=["mutate"],
class_permission_name="Chart",
method_permission_name="write",
annotations=ToolAnnotations(
title="Update chart preview",
readOnlyHint=False,
destructiveHint=True,
),
)
@parse_request(UpdateChartPreviewRequest)
def update_chart_preview(
request: UpdateChartPreviewRequest, ctx: Context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from typing import Any, Dict

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.chart.schemas import serialize_chart_object
Expand Down Expand Up @@ -305,7 +305,15 @@ def _ensure_layout_structure(
layout["DASHBOARD_VERSION_KEY"] = "v2"


@tool(tags=["mutate"], class_permission_name="Dashboard")
@tool(
tags=["mutate"],
class_permission_name="Dashboard",
annotations=ToolAnnotations(
title="Add chart to dashboard",
readOnlyHint=False,
destructiveHint=False,
),
)
@parse_request(AddChartToDashboardRequest)
def add_chart_to_existing_dashboard(
request: AddChartToDashboardRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/dashboard/tool/generate_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from typing import Any, Dict, List

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.chart.schemas import serialize_chart_object
Expand Down Expand Up @@ -178,7 +178,15 @@ def _generate_title_from_charts(chart_objects: List[Any]) -> str:
return title


@tool(tags=["mutate"], class_permission_name="Dashboard")
@tool(
tags=["mutate"],
class_permission_name="Dashboard",
annotations=ToolAnnotations(
title="Create dashboard",
readOnlyHint=False,
destructiveHint=False,
),
)
@parse_request(GenerateDashboardRequest)
def generate_dashboard(
request: GenerateDashboardRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/dashboard/tool/get_dashboard_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from fastmcp import Context
from sqlalchemy.orm import subqueryload
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
from superset.dashboards.permalink.types import DashboardPermalinkValue
Expand Down Expand Up @@ -59,7 +59,15 @@ def _get_permalink_state(permalink_key: str) -> DashboardPermalinkValue | None:
return None


@tool(tags=["discovery"], class_permission_name="Dashboard")
@tool(
tags=["discovery"],
class_permission_name="Dashboard",
annotations=ToolAnnotations(
title="Get dashboard info",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(GetDashboardInfoRequest)
async def get_dashboard_info(
request: GetDashboardInfoRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/dashboard/tool/list_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from typing import TYPE_CHECKING

from fastmcp import Context
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

if TYPE_CHECKING:
from superset.models.dashboard import Dashboard
Expand Down Expand Up @@ -63,7 +63,15 @@
]


@tool(tags=["core"], class_permission_name="Dashboard")
@tool(
tags=["core"],
class_permission_name="Dashboard",
annotations=ToolAnnotations(
title="List dashboards",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(ListDashboardsRequest)
async def list_dashboards(
request: ListDashboardsRequest, ctx: Context
Expand Down
12 changes: 10 additions & 2 deletions superset/mcp_service/dataset/tool/get_dataset_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from fastmcp import Context
from sqlalchemy.orm import joinedload, subqueryload
from superset_core.mcp.decorators import tool
from superset_core.mcp.decorators import tool, ToolAnnotations

from superset.extensions import event_logger
from superset.mcp_service.dataset.schemas import (
Expand All @@ -42,7 +42,15 @@
logger = logging.getLogger(__name__)


@tool(tags=["discovery"], class_permission_name="Dataset")
@tool(
tags=["discovery"],
class_permission_name="Dataset",
annotations=ToolAnnotations(
title="Get dataset info",
readOnlyHint=True,
destructiveHint=False,
),
)
@parse_request(GetDatasetInfoRequest)
async def get_dataset_info(
request: GetDatasetInfoRequest, ctx: Context
Expand Down
Loading
Loading