Skip to content
14 changes: 13 additions & 1 deletion superset/mcp_service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def get_default_instructions(branding: str = "Apache Superset") -> str:
- generate_dashboard: Create a dashboard from chart IDs
- add_chart_to_existing_dashboard: Add a chart to an existing dashboard

Database Connections:
- list_databases: List database connections with advanced filters (1-based pagination)
- get_database_info: Get detailed database connection info by ID (backend, capabilities)

Dataset Management:
- list_datasets: List datasets with advanced filters (1-based pagination)
- get_dataset_info: Get detailed dataset information by ID (includes columns/metrics)
Expand Down Expand Up @@ -114,12 +118,14 @@ def get_default_instructions(branding: str = "Apache Superset") -> str:
3. generate_explore_link(dataset_id, config) -> preview interactively
4. generate_chart(dataset_id, config, save_chart=True) -> save permanently

To find your own charts/dashboards:
To find your own charts/dashboards/databases:
1. get_instance_info -> get current_user.id
2. list_charts(filters=[{{"col": "created_by_fk",
"opr": "eq", "value": current_user.id}}])
3. Or: list_dashboards(filters=[{{"col": "created_by_fk",
"opr": "eq", "value": current_user.id}}])
4. Or: list_databases(filters=[{{"col": "created_by_fk",
"opr": "eq", "value": current_user.id}}])

To explore data with SQL:
1. list_datasets -> find a dataset and note its database_id
Expand Down Expand Up @@ -168,6 +174,8 @@ def get_default_instructions(branding: str = "Apache Superset") -> str:
filters=[{{"col": "created_by_fk", "opr": "eq", "value": <user_id>}}]
- My dashboards:
filters=[{{"col": "created_by_fk", "opr": "eq", "value": <user_id>}}]
- My databases:
filters=[{{"col": "created_by_fk", "opr": "eq", "value": <user_id>}}]

To modify an existing chart (add filters, change metrics, change dimensions, etc.):
1. get_chart_info(chart_id) -> examine current configuration
Expand Down Expand Up @@ -432,6 +440,10 @@ def create_mcp_app(
get_dashboard_info,
list_dashboards,
)
from superset.mcp_service.database.tool import ( # noqa: F401, E402
get_database_info,
list_databases,
)
from superset.mcp_service.dataset.tool import ( # noqa: F401, E402
get_dataset_info,
list_datasets,
Expand Down
96 changes: 92 additions & 4 deletions superset/mcp_service/common/schema_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from pydantic import BaseModel, Field
from sqlalchemy.inspection import inspect

from superset.mcp_service.constants import ModelType


class ColumnMetadata(BaseModel):
"""Metadata for a selectable column."""
Expand All @@ -52,7 +54,7 @@ class ModelSchemaInfo(BaseModel):
- Default values for each
"""

model_type: Literal["chart", "dataset", "dashboard"] = Field(
model_type: ModelType = Field(
..., description="The model type this schema describes"
)
select_columns: list[ColumnMetadata] = Field(
Expand Down Expand Up @@ -82,9 +84,7 @@ class ModelSchemaInfo(BaseModel):
class GetSchemaRequest(BaseModel):
"""Request schema for unified get_schema tool."""

model_type: Literal["chart", "dataset", "dashboard"] = Field(
..., description="Model type to get schema for"
)
model_type: ModelType = Field(..., description="Model type to get schema for")


class GetSchemaResponse(BaseModel):
Expand Down Expand Up @@ -180,6 +180,7 @@ def get_columns_from_model(
model_cls: Type[Any],
default_columns: list[str],
extra_columns: dict[str, ColumnMetadata] | None = None,
exclude_columns: set[str] | None = None,
) -> list[ColumnMetadata]:
"""
Dynamically extract column metadata from a SQLAlchemy model.
Expand All @@ -188,6 +189,7 @@ def get_columns_from_model(
model_cls: The SQLAlchemy model class to inspect
default_columns: List of column names that should be marked as defaults
extra_columns: Additional columns not on the model (e.g., computed fields)
exclude_columns: Column names to omit (e.g., sensitive fields)

Returns:
List of ColumnMetadata objects for all columns
Expand All @@ -197,6 +199,8 @@ def get_columns_from_model(

for col in mapper.columns:
col_name = col.key
if exclude_columns and col_name in exclude_columns:
continue
col_type = _get_sqlalchemy_type_name(col.type)
# Get description from column doc, comment, or fallback mapping
description = (
Expand Down Expand Up @@ -452,6 +456,68 @@ def get_columns_from_model(
}


# Database configuration
DATABASE_DEFAULT_COLUMNS = [
"id",
"database_name",
"backend",
"expose_in_sqllab",
Comment thread
aminghadersohi marked this conversation as resolved.
"changed_on",
"changed_on_humanized",
]
DATABASE_SORTABLE_COLUMNS = [
"id",
"database_name",
"changed_on",
"created_on",
]
DATABASE_SEARCH_COLUMNS = ["database_name"]
DATABASE_EXTRA_COLUMNS: dict[str, ColumnMetadata] = {
"backend": ColumnMetadata(
name="backend",
description="Database backend type (e.g., postgresql, mysql)",
type="str",
is_default=True,
),
"changed_by": ColumnMetadata(
name="changed_by",
description="Last modifier username",
type="str",
is_default=False,
),
"changed_by_name": ColumnMetadata(
name="changed_by_name",
description="Last modifier display name",
type="str",
is_default=False,
),
"changed_on_humanized": ColumnMetadata(
name="changed_on_humanized",
description="Humanized modification time",
type="str",
is_default=True,
),
"created_by": ColumnMetadata(
name="created_by",
description="Creator username",
type="str",
is_default=False,
),
"created_by_name": ColumnMetadata(
name="created_by_name",
description="Creator display name",
type="str",
is_default=False,
),
"created_on_humanized": ColumnMetadata(
name="created_on_humanized",
description="Humanized creation time",
type="str",
is_default=False,
),
}


def get_chart_columns() -> list[ColumnMetadata]:
"""Get column metadata for Chart model dynamically."""
from superset.models.slice import Slice
Expand All @@ -477,6 +543,27 @@ def get_dashboard_columns() -> list[ColumnMetadata]:
)


# Sensitive columns that should not be exposed via schema discovery
DATABASE_EXCLUDE_COLUMNS = {
"sqlalchemy_uri",
"password",
"encrypted_extra",
"server_cert",
}


def get_database_columns() -> list[ColumnMetadata]:
"""Get column metadata for Database model dynamically."""
from superset.models.core import Database

return get_columns_from_model(
Database,
DATABASE_DEFAULT_COLUMNS,
DATABASE_EXTRA_COLUMNS,
exclude_columns=DATABASE_EXCLUDE_COLUMNS,
)


def get_all_column_names(columns: list[ColumnMetadata]) -> list[str]:
"""Extract all column names from column metadata list."""
return [col.name for col in columns]
Expand All @@ -487,3 +574,4 @@ def get_all_column_names(columns: list[ColumnMetadata]) -> list[str]:
CHART_ALL_COLUMNS: list[str] = []
DATASET_ALL_COLUMNS: list[str] = []
DASHBOARD_ALL_COLUMNS: list[str] = []
DATABASE_ALL_COLUMNS: list[str] = []
5 changes: 5 additions & 0 deletions superset/mcp_service/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
# under the License.
"""Constants for the MCP service."""

from typing import Literal

# Supported model types for schema discovery and MCP tools
ModelType = Literal["chart", "dataset", "dashboard", "database"]

# Pagination defaults
DEFAULT_PAGE_SIZE = 10 # Default number of items per page
MAX_PAGE_SIZE = 100 # Maximum allowed page_size to prevent oversized responses
Expand Down
16 changes: 16 additions & 0 deletions superset/mcp_service/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
Loading
Loading