Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f1995fc
fix(mcp): preserve calling constraints in compact tool discovery
aminghadersohi Sep 25, 2026
e5fec9a
Merge master into sc-121711-mcp-description-truncation (pick up codeq…
aminghadersohi Sep 25, 2026
4c91a5a
fix(mcp): keep every docstring calling constraint in default discovery
aminghadersohi Sep 25, 2026
6cec0b0
fix(mcp): leave room for purpose line in list_datasets and get_dashbo…
aminghadersohi Sep 25, 2026
1d0829a
fix(mcp): restore constraint meaning in list_datasets and get_dashboa…
aminghadersohi Sep 25, 2026
13e9da7
fix(mcp): preserve field-level discovery guidance
aminghadersohi Sep 26, 2026
cd9aee1
fix(mcp): tighten schema wording to fit tool inventory budgets
aminghadersohi Sep 26, 2026
dba4f0a
test(mcp): annotate description truncation tests
aminghadersohi Sep 27, 2026
918ead6
fix(mcp): preserve priority guidance in direct tool catalogs
aminghadersohi Sep 29, 2026
682ac3f
fix(mcp): merge master and preserve description budgeting
aminghadersohi Sep 30, 2026
4544324
test(mcp): normalize paragraph expectations after docstring cleanup
aminghadersohi Sep 30, 2026
87a5768
test(mcp): budget for complete theme description paragraph
aminghadersohi Sep 30, 2026
c21ffa3
Merge branch 'master' into sc-121711-mcp-description-truncation
aminghadersohi Oct 1, 2026
81bcc73
fix(mcp): deduct only authored request instructions from discovery prose
aminghadersohi Oct 1, 2026
4fae158
Merge remote-tracking branch 'origin/master' into sc-121711-mcp-descr…
aminghadersohi Oct 1, 2026
78e8a55
Merge branch 'sc-121711-mcp-description-truncation' of https://github…
aminghadersohi Oct 1, 2026
ad22adc
Merge Apache master to resolve MCP tool description conflicts
aminghadersohi Oct 2, 2026
e2db70f
fix(mcp): drop a trailing lead-in sentence from truncated tool descri…
aminghadersohi Oct 2, 2026
2eebd5a
Merge Apache master to pick up the importers mypy fix
aminghadersohi Oct 3, 2026
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
22 changes: 21 additions & 1 deletion docs/admin_docs/configuration/mcp-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,27 @@ MCP_TOOL_SEARCH_CONFIG = {
| `always_visible` | See above | Tools that always appear in `list_tools`, regardless of search |
| `include_schemas` | `True` | Include the full `inputSchema` in search results by default. When `False` ("summary mode"), search results omit `inputSchema` entirely and include a lightweight `parameters_hint` listing top-level parameter names. Full schemas are always used when a tool is actually invoked via `call_tool`. |
| `compact_schemas` | `True` | Legacy setting selecting the default description limit (300 when `True`, 0 when `False`) if `max_description_length` is omitted. Input schemas preserve `$defs`, `$ref`, nullable unions, and constraints in either mode; titles are omitted. Clients should resolve references within each tool's `inputSchema`. |
| `max_description_length` | `300` | Truncate tool descriptions in search results (0 = no truncation). Applies in both summary and full-schema modes. |
| `max_description_length` | `300` | Budget for discovery prose and request-wrapper instructions (0 = no prose truncation). Complete paragraphs are retained, followed by complete sentences of the next paragraph unless it is a list, heading, or IMPORTANT block. Applies in both summary and full-schema modes. |

The no-query catalog uses the same bounded serializer as targeted search.
Chart creation/update and virtual-dataset tools keep request wrapping, save/preview
behavior, required URL display, and workflow guidance in request metadata rather
than relying on truncated introductory prose. Paragraph truncation never leaves a
partial IMPORTANT block or numbered workflow in the catalog.

Calling constraints are authored as a `Field(description=...)` on the tool's
`request` parameter and served as the `request` property's schema description, so
schema-first clients can identify the wrapper without remembering previous calls.
Summary mode preserves these instructions in `parameters_hint`. Instructions are
never truncated: their length is deducted from the prose budget first. A request
model's own docstring is not a calling instruction and is never deducted. If a
configured limit is smaller than the instructions, prose is omitted rather than
cutting the instructions. Built-in calling instructions are capped at 300
characters by regression tests, and the complete tool entries retain per-tool
size budgets. Field-level descriptions in the default full input schema also
preserve tool-specific search semantics, sortable columns, chart pagination, and
layout guidance. These details are not included in schema-free summary mode.
This setting does not change result-size limits or schema validation.

:::tip
Set `enabled: False` to revert to the traditional "show all tools at once" behavior, which some clients or workflows may prefer.
Expand Down
22 changes: 18 additions & 4 deletions superset/mcp_service/chart/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class GetChartInfoRequest(BaseModel):
form_data_key: str | None = Field(
default=None,
description=(
"Cache key for retrieving unsaved chart state. When a user "
"Cache key from the Explore URL for unsaved chart state. When a user "
"edits a chart in Explore but hasn't saved, the current state is stored "
"with this key. If provided, the tool returns the current unsaved "
"configuration instead of the saved version. "
Expand All @@ -391,8 +391,9 @@ class GetChartInfoRequest(BaseModel):
default=None,
description=(
"When provided, resolves dashboard-level native filters that are in "
"scope for this chart on the given dashboard and returns them under "
"filters.dashboard_filters. Requires the chart to be on the dashboard "
"scope for this chart on the given dashboard and returns their column, "
"operator, and value under filters.dashboard_filters. Requires the chart "
"to be on the dashboard "
"and the caller to have dashboard access."
),
)
Expand All @@ -413,7 +414,8 @@ class GetChartInfoRequest(BaseModel):
description=(
"Top-level fields to include in the response. Defaults to a lean "
"set that excludes 'form_data' (the full chart config, can be 50KB+). "
"Add 'form_data' explicitly when you need the raw chart configuration."
"Add 'form_data' explicitly when you need the raw chart configuration. "
"The url field links to the chart's Explore page in Superset."
),
validation_alias=AliasChoices("select_columns", "columns"),
),
Expand Down Expand Up @@ -3886,6 +3888,18 @@ class ListChartsRequest(
):
"""Request schema for list_charts with clear, unambiguous types."""

order_column: Annotated[
str | None,
Field(
default=None,
description=(
"Sortable columns: id, slice_name, viz_type, description, "
"changed_on, created_on; "
"changed_on_delta_humanized is an alias for changed_on."
),
),
]

certified: Annotated[
StrictBool | None,
Field(
Expand Down
18 changes: 16 additions & 2 deletions superset/mcp_service/chart/tool/generate_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

import logging
import time
from typing import Annotated

from fastmcp import Context
from pydantic import Field
from sqlalchemy.exc import SQLAlchemyError
from superset_core.mcp.decorators import tool, ToolAnnotations

Expand Down Expand Up @@ -76,9 +78,21 @@
),
)
async def generate_chart( # noqa: C901
request: GenerateChartRequest, ctx: Context
request: Annotated[
GenerateChartRequest,
Field(
description=(
'Wrap as {"request": {...}}. '
"Preview only; save_chart=True saves. MUST display chart URL. "
"dataset_id: numeric ID/UUID, NOT schema.table_name. "
"config.chart_type required; line/bar/area/scatter are xy kind values. "
"Check get_chart_type_schema for host-gated types."
)
),
],
ctx: Context,
) -> GenerateChartResponse:
"""Create a chart preview in Superset, optionally saving it permanently.
"""Preview a chart; optionally save.

IMPORTANT BEHAVIOR:
- Charts are NOT saved by default (save_chart=False) - preview only
Expand Down
16 changes: 14 additions & 2 deletions superset/mcp_service/chart/tool/get_chart_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
"""

import logging
from typing import Any, cast
from typing import Annotated, Any, cast

from fastmcp import Context
from marshmallow import ValidationError
from pydantic import Field
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import subqueryload
from superset_core.mcp.decorators import tool, ToolAnnotations
Expand Down Expand Up @@ -444,7 +445,18 @@ def _dump_chart_info(
),
)
async def get_chart_info( # noqa: C901
request: GetChartInfoRequest, ctx: Context
request: Annotated[
GetChartInfoRequest,
Field(
description=(
'Wrap as {"request": {"identifier": 123}}. '
"Use ID/UUID, NOT chart name; discover IDs with list_charts. "
"form_data_key reads unsaved state; permalink_key reads shared "
"Explore state and makes identifier optional."
)
),
],
ctx: Context,
) -> ChartInfo | ChartError:
"""Get chart metadata by ID or UUID.

Expand Down
14 changes: 12 additions & 2 deletions superset/mcp_service/chart/tool/list_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
"""

import logging
from typing import Any, cast, TYPE_CHECKING
from typing import Annotated, Any, cast, TYPE_CHECKING

from fastmcp import Context
from pydantic import Field
from sqlalchemy import case, select
from superset_core.mcp.decorators import tool, ToolAnnotations

Expand Down Expand Up @@ -179,7 +180,16 @@ def _call_dao_list(
),
)
async def list_charts(
request: ListChartsRequest | None = None,
request: Annotated[
ListChartsRequest | None,
Field(
description=(
'Wrap parameters as {"request": {"search": "sales"}}; omit request for defaults. '
"Do NOT pass search, page, page_size or filters as top-level arguments. "
"For people, resolve IDs with find_users and use filters, not search."
)
),
] = None,
ctx: Context = None,
) -> ChartList | ChartError:
"""List charts with filtering and search.
Expand Down
16 changes: 14 additions & 2 deletions superset/mcp_service/chart/tool/update_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@

import logging
import time
from typing import Any
from typing import Annotated, Any

from fastmcp import Context
from pydantic import Field
from sqlalchemy.exc import SQLAlchemyError
from superset_core.mcp.decorators import tool, ToolAnnotations

Expand Down Expand Up @@ -705,7 +706,18 @@ def _create_preview_url(
),
)
async def update_chart( # noqa: C901
request: UpdateChartRequest, ctx: Context
request: Annotated[
UpdateChartRequest,
Field(
description=(
'Wrap as {"request": {...}}. '
"generate_preview=True previews; False persists immediately. "
"MUST display explore URL. identifier: ID/UUID, NOT chart name. "
"Omit config to rename only; add_columns appends table columns."
)
),
],
ctx: Context,
) -> GenerateChartResponse:
"""Update existing chart with new configuration.

Expand Down
18 changes: 16 additions & 2 deletions superset/mcp_service/chart/tool/update_chart_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@

import logging
import time
from typing import Any, Dict
from typing import Annotated, Any, Dict

from fastmcp import Context
from pydantic import Field
from sqlalchemy.exc import SQLAlchemyError
from superset_core.mcp.decorators import tool, ToolAnnotations

Expand Down Expand Up @@ -123,7 +124,20 @@ def _get_previous_form_data(form_data_key: str) -> dict[str, Any] | None:
),
)
def update_chart_preview( # noqa: C901
request: UpdateChartPreviewRequest, ctx: Context
request: Annotated[
UpdateChartPreviewRequest,
Field(
description=(
'Wrap as {"request": {...}}. '
"Cached preview only, not saved. Supplied form_data_key "
"is invalidated; "
"use the returned key. MUST display explore_url. "
"For a fresh preview provide config + dataset_id "
"and omit form_data_key."
)
),
],
ctx: Context,
) -> UpdateChartPreviewResponse:
"""Update cached chart preview without saving.

Expand Down
63 changes: 45 additions & 18 deletions superset/mcp_service/dashboard/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ class ListDashboardsRequest(
):
"""Request schema for list_dashboards with clear, unambiguous types."""

order_column: Annotated[
str | None,
Field(
default=None,
description=(
"Sortable columns: id, dashboard_title, slug, published, "
"changed_on, created_on; "
"changed_on_delta_humanized is an alias for changed_on."
),
),
]

search: Annotated[
str | None,
Field(
default=None,
description=(
"Search matches titles and slugs only, not people. Resolve names "
"with find_users and filter by created_by_fk or changed_by_fk "
"using the user ID. Mutually exclusive with 'filters'."
),
),
]

deleted_state: Annotated[
Literal["include", "only"] | None,
Field(
Expand Down Expand Up @@ -260,10 +284,7 @@ class ListDashboardsRequest(
class GetDashboardInfoRequest(MetadataCacheControl):
"""Request schema for dashboard identifiers and shared permalink URLs.

When permalink_key is provided, the tool will retrieve the dashboard's filter
state from the permalink, allowing you to see what filters the user has applied
(not just the default filter state). This is useful when a user applies filters
in a dashboard but the URL contains a permalink_key.
permalink_key retrieves the user's applied filter state, not just defaults.
"""

model_config = ConfigDict(populate_by_name=True)
Expand Down Expand Up @@ -291,26 +312,31 @@ class GetDashboardInfoRequest(MetadataCacheControl):
filter_state: dict[str, Any] | None = Field(
default=None,
description=(
"Active filters supplied directly rather than via a permalink, so the "
"tool can describe the dashboard as the user currently views it, "
'filtered. Accepts dashboard dataMask state, e.g. {"dataMask": '
"Direct active filters describing the user's dashboard view. "
'Accepts dashboard dataMask state, e.g. {"dataMask": '
'{"<configured filter ID>": {"filterState": {"value": ["EMEA"]}}}}, '
'or {"applied_filters": [{"col": "region", "op": "IN", '
'"val": ["EMEA"]}]}. Native mask values '
"are projected without column metadata for restricted users. Ignored "
"when permalink_key is provided."
'"val": ["EMEA"]}]}. Ignored '
"when permalink_key is provided. Use returned filter_state as context. "
"Restricted users receive native_filter_values (names, types, values, "
"labels, exclusion flags), not raw dataMask/column targets. "
"native_filter_values_incomplete flags unsupported filters/chart state "
"that cannot be summarized safely."
),
)
select_columns: Annotated[
List[str],
Field(
default_factory=lambda: list(DEFAULT_GET_DASHBOARD_INFO_COLUMNS),
description=(
"Top-level fields to include in the response. Defaults to a lean "
"set that excludes 'css' (raw CSS, can be many KB) and 'filter_state' "
"(only relevant when permalink_key is provided). Pass an explicit list "
"to override, e.g. ['id','dashboard_title','charts'] for minimal "
"output, or add 'css' to include raw dashboard CSS."
"Top-level response fields; defaults exclude 'css' (raw CSS, "
"potentially KBs) and 'filter_state' (shared/applied filter context). "
"Override with "
"e.g. ['id','dashboard_title','charts'], or add 'css' for raw CSS. "
"Charts/native_filters may be capped: check chart_count and "
"_truncation_notes. For all charts, call list_charts with "
'request={"filters": [{"col": "dashboards", "opr": "eq", '
'"value": <dashboard id>}]} and paginate with page/page_size.'
),
validation_alias=AliasChoices("select_columns", "columns"),
),
Expand Down Expand Up @@ -704,8 +730,7 @@ class GenerateDashboardRequest(BaseModel):
dashboard_title: str | None = Field(
None,
description=(
"Title for the new dashboard. When omitted a descriptive title "
"is generated from the included chart names."
"Dashboard title; if omitted, generated descriptively from chart names."
),
validation_alias=AliasChoices("dashboard_title", "title", "name"),
)
Expand All @@ -729,7 +754,9 @@ class GenerateDashboardRequest(BaseModel):
"dict). When set, replaces the auto-generated layout entirely. "
"Pass this when you need custom row composition, MARKDOWN "
"blocks, HEADER components, or specific chart widths/heights. "
"Omit to let the tool auto-generate a packed grid from chart_ids."
"Omit for an auto-generated 2-column grid from chart_ids. "
"Each component's parents is recomputed from its children edges "
"before saving; omitted or incomplete parents arrays are fine."
),
)
json_metadata_overrides: Dict[str, Any] | None = Field(
Expand Down
18 changes: 15 additions & 3 deletions superset/mcp_service/dashboard/tool/generate_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"""

import logging
from typing import Any, Dict, List
from typing import Annotated, Any, Dict, List

from fastmcp import Context
from flask import g
from pydantic import ValidationError
from pydantic import Field, ValidationError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from superset_core.mcp.decorators import tool, ToolAnnotations

Expand Down Expand Up @@ -198,7 +198,19 @@ def _generate_title_from_charts(chart_objects: List[Any]) -> str:
),
)
def generate_dashboard( # noqa: C901
request: GenerateDashboardRequest, ctx: Context
request: Annotated[
GenerateDashboardRequest,
Field(
description=(
'Wrap as {"request": {"chart_ids": [1], "dashboard_title": "New"}}. '
"Charts must exist and be accessible. NEW dashboards only; "
"use add_chart_to_existing_dashboard for existing ones. "
"Never use as a fallback if that fails. "
"position_json overrides the default grid."
)
),
],
ctx: Context,
) -> GenerateDashboardResponse:
"""Create a NEW dashboard from chart IDs.

Expand Down
Loading
Loading