diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py index c9148590f0d..89e584f428f 100644 --- a/superset/mcp_service/chart/tool/generate_chart.py +++ b/superset/mcp_service/chart/tool/generate_chart.py @@ -48,7 +48,7 @@ from superset.utils import json logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @parse_request(GenerateChartRequest) async def generate_chart( # noqa: C901 diff --git a/superset/mcp_service/chart/tool/get_chart_available_filters.py b/superset/mcp_service/chart/tool/get_chart_available_filters.py index 30df99e1591..0a907f51ef6 100644 --- a/superset/mcp_service/chart/tool/get_chart_available_filters.py +++ b/superset/mcp_service/chart/tool/get_chart_available_filters.py @@ -35,7 +35,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @parse_request(GetChartAvailableFiltersRequest) def get_chart_available_filters( diff --git a/superset/mcp_service/chart/tool/get_chart_data.py b/superset/mcp_service/chart/tool/get_chart_data.py index e47478b8346..8ce34256df1 100644 --- a/superset/mcp_service/chart/tool/get_chart_data.py +++ b/superset/mcp_service/chart/tool/get_chart_data.py @@ -41,7 +41,7 @@ from superset.mcp_service.utils.cache_utils import get_cache_status_from_result logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["data"]) @mcp_auth_hook async def get_chart_data( # noqa: C901 request: GetChartDataRequest, ctx: Context diff --git a/superset/mcp_service/chart/tool/get_chart_info.py b/superset/mcp_service/chart/tool/get_chart_info.py index a6b3aafeaa7..6568fb10e05 100644 --- a/superset/mcp_service/chart/tool/get_chart_info.py +++ b/superset/mcp_service/chart/tool/get_chart_info.py @@ -37,7 +37,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @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 3f2ed87e551..3916a4be99d 100644 --- a/superset/mcp_service/chart/tool/get_chart_preview.py +++ b/superset/mcp_service/chart/tool/get_chart_preview.py @@ -2020,7 +2020,7 @@ async def _get_chart_preview_internal( # noqa: C901 ) -@mcp.tool +@mcp.tool(tags=["data"]) @mcp_auth_hook @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 0ef888a006a..133d59c76ab 100644 --- a/superset/mcp_service/chart/tool/list_charts.py +++ b/superset/mcp_service/chart/tool/list_charts.py @@ -66,7 +66,7 @@ SORTABLE_CHART_COLUMNS = [ ] -@mcp.tool +@mcp.tool(tags=["core"]) @mcp_auth_hook @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 302be0bded1..f5425dccf8a 100644 --- a/superset/mcp_service/chart/tool/update_chart.py +++ b/superset/mcp_service/chart/tool/update_chart.py @@ -48,7 +48,7 @@ from superset.utils import json logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @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 7cf69bdb6b6..2095813ce3c 100644 --- a/superset/mcp_service/chart/tool/update_chart_preview.py +++ b/superset/mcp_service/chart/tool/update_chart_preview.py @@ -46,7 +46,7 @@ from superset.mcp_service.utils.url_utils import get_mcp_service_url logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @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 671b0850d46..ca6301f5b8d 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 @@ -135,7 +135,7 @@ def _ensure_layout_structure(layout: Dict[str, Any], row_key: str) -> None: layout["DASHBOARD_VERSION_KEY"] = "v2" -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @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 caa727f449c..b0b60400f0f 100644 --- a/superset/mcp_service/dashboard/tool/generate_dashboard.py +++ b/superset/mcp_service/dashboard/tool/generate_dashboard.py @@ -118,7 +118,7 @@ def _create_dashboard_layout(chart_objects: List[Any]) -> Dict[str, Any]: return layout -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @parse_request(GenerateDashboardRequest) def generate_dashboard( diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_available_filters.py b/superset/mcp_service/dashboard/tool/get_dashboard_available_filters.py index 8839f634685..4a9e8867586 100644 --- a/superset/mcp_service/dashboard/tool/get_dashboard_available_filters.py +++ b/superset/mcp_service/dashboard/tool/get_dashboard_available_filters.py @@ -34,7 +34,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @parse_request(GetDashboardAvailableFiltersRequest) async def get_dashboard_available_filters( diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_info.py b/superset/mcp_service/dashboard/tool/get_dashboard_info.py index 19c398b1516..1cd08f32486 100644 --- a/superset/mcp_service/dashboard/tool/get_dashboard_info.py +++ b/superset/mcp_service/dashboard/tool/get_dashboard_info.py @@ -41,7 +41,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @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 4c274f15b41..51dded6c9c8 100644 --- a/superset/mcp_service/dashboard/tool/list_dashboards.py +++ b/superset/mcp_service/dashboard/tool/list_dashboards.py @@ -65,7 +65,7 @@ SORTABLE_DASHBOARD_COLUMNS = [ ] -@mcp.tool +@mcp.tool(tags=["core"]) @mcp_auth_hook @parse_request(ListDashboardsRequest) async def list_dashboards( diff --git a/superset/mcp_service/dataset/tool/get_dataset_available_filters.py b/superset/mcp_service/dataset/tool/get_dataset_available_filters.py index 6f1b1038bdb..0a9197e82fd 100644 --- a/superset/mcp_service/dataset/tool/get_dataset_available_filters.py +++ b/superset/mcp_service/dataset/tool/get_dataset_available_filters.py @@ -34,7 +34,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @parse_request(GetDatasetAvailableFiltersRequest) async def get_dataset_available_filters( diff --git a/superset/mcp_service/dataset/tool/get_dataset_info.py b/superset/mcp_service/dataset/tool/get_dataset_info.py index bbdd7e0f96f..d343d2a97ca 100644 --- a/superset/mcp_service/dataset/tool/get_dataset_info.py +++ b/superset/mcp_service/dataset/tool/get_dataset_info.py @@ -41,7 +41,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["discovery"]) @mcp_auth_hook @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 70615a3b450..d6e761ba529 100644 --- a/superset/mcp_service/dataset/tool/list_datasets.py +++ b/superset/mcp_service/dataset/tool/list_datasets.py @@ -68,7 +68,7 @@ SORTABLE_DATASET_COLUMNS = [ ] -@mcp.tool +@mcp.tool(tags=["core"]) @mcp_auth_hook @parse_request(ListDatasetsRequest) async def list_datasets(request: ListDatasetsRequest, ctx: Context) -> DatasetList: diff --git a/superset/mcp_service/docs/tool-search-optimization.md b/superset/mcp_service/docs/tool-search-optimization.md new file mode 100644 index 00000000000..d93fd2bd63a --- /dev/null +++ b/superset/mcp_service/docs/tool-search-optimization.md @@ -0,0 +1,139 @@ + + +# Tool Search Optimization for Superset MCP Service + +This guide explains how to optimize context usage when connecting to Superset's MCP service using Anthropic's Tool Search Tool feature. + +## Overview + +Superset's MCP service provides 21 tools across various categories. Loading all tool definitions upfront can consume significant context tokens (~15-20K tokens). The Tool Search Tool feature allows Claude to dynamically discover tools on-demand, reducing initial context overhead by up to 85%. + +## Tool Categories + +Superset MCP tools are categorized with tags to help clients configure optimal loading strategies: + +| Tag | Description | Tools | Recommended Strategy | +|-----|-------------|-------|---------------------| +| `core` | Essential discovery and health tools | `health_check`, `get_instance_info`, `list_charts`, `list_dashboards`, `list_datasets` | Always load | +| `discovery` | Detailed resource information | `get_chart_info`, `get_chart_available_filters`, `get_dashboard_info`, `get_dashboard_available_filters`, `get_dataset_info`, `get_dataset_available_filters` | Can defer | +| `data` | Data retrieval and previews | `get_chart_preview`, `get_chart_data` | Defer | +| `mutate` | Create/modify resources | `generate_chart`, `update_chart`, `update_chart_preview`, `generate_dashboard`, `add_chart_to_existing_dashboard`, `execute_sql` | Defer | +| `explore` | URL generation for exploration | `generate_explore_link`, `open_sql_lab_with_context` | Defer | + +## Client Configuration + +### Claude API Configuration + +When calling the Claude API with Superset MCP tools, configure `defer_loading` based on tool categories: + +```json +{ + "type": "mcp_toolset", + "mcp_server_name": "superset", + "default_config": {"defer_loading": true}, + "configs": { + "health_check": {"defer_loading": false}, + "get_instance_info": {"defer_loading": false}, + "list_charts": {"defer_loading": false}, + "list_dashboards": {"defer_loading": false}, + "list_datasets": {"defer_loading": false} + } +} +``` + +This configuration: +- **Always loads** the 5 core tools (~4-5K tokens) +- **Defers** the remaining 16 tools until needed +- Reduces initial context from ~15-20K tokens to ~4-5K tokens + +### Claude Desktop Configuration + +For Claude Desktop (`claude_desktop_config.json`), add the Superset MCP server with defer_loading: + +```json +{ + "mcpServers": { + "superset": { + "command": "npx", + "args": ["@superset/mcp-server", "--stdio"], + "env": { + "SUPERSET_URL": "http://localhost:8088", + "SUPERSET_ACCESS_TOKEN": "your-token" + }, + "toolConfig": { + "default": {"defer_loading": true}, + "overrides": { + "health_check": {"defer_loading": false}, + "get_instance_info": {"defer_loading": false}, + "list_charts": {"defer_loading": false}, + "list_dashboards": {"defer_loading": false}, + "list_datasets": {"defer_loading": false} + } + } + } + } +} +``` + +## Token Savings Estimates + +| Configuration | Estimated Initial Tokens | Savings | +|--------------|-------------------------|---------| +| All tools loaded | ~15-20K | Baseline | +| Core only + defer | ~4-5K | ~75% reduction | +| Minimal (health only) | ~500 | ~97% reduction | + +## How It Works + +1. **Initial Load**: Only `core` tagged tools are loaded when the session starts +2. **On-Demand Discovery**: When Claude needs a deferred tool (e.g., user asks to "create a chart"), it searches for and loads the relevant tool +3. **Context Preservation**: Deferred tools are only loaded into context when actually needed + +## Usage Patterns + +### Common Workflows + +**Data Exploration Flow** (loads progressively): +1. `list_datasets` (core, always loaded) +2. `get_dataset_info` (discovery, loaded on-demand) +3. `generate_explore_link` (explore, loaded on-demand) + +**Chart Creation Flow** (loads progressively): +1. `list_datasets` (core, always loaded) +2. `get_dataset_info` (discovery, loaded on-demand) +3. `generate_chart` (mutate, loaded on-demand) +4. `get_chart_preview` (data, loaded on-demand) + +**Dashboard Building Flow** (loads progressively): +1. `list_charts` (core, always loaded) +2. `generate_dashboard` (mutate, loaded on-demand) + +## Best Practices + +1. **Always keep core tools loaded**: These are used in nearly every session +2. **Defer mutate tools**: These are only needed when explicitly creating/modifying resources +3. **Defer data tools**: Preview/data retrieval is typically after initial exploration +4. **Monitor token usage**: Track your actual usage patterns and adjust accordingly + +## References + +- [Anthropic Tool Search Tool Documentation](https://www.anthropic.com/engineering/advanced-tool-use) +- [MCP Protocol Specification](https://modelcontextprotocol.io/) +- [Superset MCP Service Documentation](../CLAUDE.md) diff --git a/superset/mcp_service/explore/tool/generate_explore_link.py b/superset/mcp_service/explore/tool/generate_explore_link.py index fcdb76b05a4..494a4aea5ad 100644 --- a/superset/mcp_service/explore/tool/generate_explore_link.py +++ b/superset/mcp_service/explore/tool/generate_explore_link.py @@ -38,7 +38,7 @@ from superset.mcp_service.chart.schemas import ( from superset.mcp_service.utils.schema_utils import parse_request -@mcp.tool +@mcp.tool(tags=["explore"]) @mcp_auth_hook @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 ca2302b9b7c..4f312132c70 100644 --- a/superset/mcp_service/sql_lab/tool/execute_sql.py +++ b/superset/mcp_service/sql_lab/tool/execute_sql.py @@ -38,7 +38,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["mutate"]) @mcp_auth_hook @parse_request(ExecuteSqlRequest) async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlResponse: 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 61693b97d73..7db91221c1a 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 @@ -37,7 +37,7 @@ from superset.mcp_service.utils.schema_utils import parse_request logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["explore"]) @mcp_auth_hook @parse_request(OpenSqlLabRequest) def open_sql_lab_with_context( diff --git a/superset/mcp_service/system/tool/get_instance_info.py b/superset/mcp_service/system/tool/get_instance_info.py index 7c55d601bb9..7dc2710a8a6 100644 --- a/superset/mcp_service/system/tool/get_instance_info.py +++ b/superset/mcp_service/system/tool/get_instance_info.py @@ -70,7 +70,7 @@ _instance_info_core = InstanceInfoCore( ) -@mcp.tool +@mcp.tool(tags=["core"]) @mcp_auth_hook @parse_request(GetSupersetInstanceInfoRequest) def get_instance_info( diff --git a/superset/mcp_service/system/tool/health_check.py b/superset/mcp_service/system/tool/health_check.py index 7d90b32556d..89fee52394c 100644 --- a/superset/mcp_service/system/tool/health_check.py +++ b/superset/mcp_service/system/tool/health_check.py @@ -32,7 +32,7 @@ from superset.utils.version import get_version_metadata logger = logging.getLogger(__name__) -@mcp.tool +@mcp.tool(tags=["core"]) @mcp_auth_hook async def health_check(ctx: Context) -> HealthCheckResponse: """