feat(mcp): PR1 - Add MCP service scaffold for Apache Superset (#35163)

This commit is contained in:
Amin Ghadersohi
2025-10-24 16:59:23 -07:00
committed by GitHub
parent 7c9720e22b
commit cc6a5dc29a
23 changed files with 1746 additions and 3 deletions
+1
View File
@@ -140,6 +140,7 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = ["fastmcp>=2.10.6"]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
-1
View File
@@ -160,7 +160,6 @@ greenlet==3.1.1
# via
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==23.0.0
# via apache-superset (pyproject.toml)
h11==0.16.0
+1 -1
View File
@@ -16,5 +16,5 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,druid,duckdb,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e ./superset-extensions-cli[test]
+69 -1
View File
@@ -22,6 +22,12 @@ annotated-types==0.7.0
# via
# -c requirements/base-constraint.txt
# pydantic
anyio==4.11.0
# via
# httpx
# mcp
# sse-starlette
# starlette
apispec==6.6.1
# via
# -c requirements/base-constraint.txt
@@ -36,11 +42,14 @@ attrs==25.3.0
# via
# -c requirements/base-constraint.txt
# cattrs
# cyclopts
# jsonschema
# outcome
# referencing
# requests-cache
# trio
authlib==1.6.4
# via fastmcp
babel==2.17.0
# via
# -c requirements/base-constraint.txt
@@ -89,6 +98,8 @@ celery==5.5.2
certifi==2025.6.15
# via
# -c requirements/base-constraint.txt
# httpcore
# httpx
# requests
# selenium
cffi==1.17.1
@@ -114,6 +125,7 @@ click==8.2.1
# click-repl
# flask
# flask-appbuilder
# uvicorn
click-didyoumean==0.3.1
# via
# -c requirements/base-constraint.txt
@@ -153,10 +165,13 @@ cryptography==44.0.3
# via
# -c requirements/base-constraint.txt
# apache-superset
# authlib
# paramiko
# pyopenssl
cycler==0.12.1
# via matplotlib
cyclopts==3.24.0
# via fastmcp
db-dtypes==1.3.1
# via pandas-gbq
defusedxml==0.7.1
@@ -181,6 +196,10 @@ dnspython==2.7.0
# email-validator
docker==7.0.0
# via apache-superset
docstring-parser==0.17.0
# via cyclopts
docutils==0.22.2
# via rich-rst
duckdb==0.10.3
# via
# apache-superset
@@ -191,10 +210,15 @@ email-validator==2.2.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
# pydantic
et-xmlfile==2.0.0
# via
# -c requirements/base-constraint.txt
# openpyxl
exceptiongroup==1.3.0
# via fastmcp
fastmcp==2.10.6
# via apache-superset
filelock==3.12.2
# via virtualenv
flask==2.3.3
@@ -333,7 +357,6 @@ greenlet==3.1.1
# apache-superset
# gevent
# shillelagh
# sqlalchemy
grpcio==1.71.0
# via
# apache-superset
@@ -348,6 +371,8 @@ gunicorn==23.0.0
h11==0.16.0
# via
# -c requirements/base-constraint.txt
# httpcore
# uvicorn
# wsproto
hashids==1.3.1
# via
@@ -358,6 +383,14 @@ holidays==0.25
# -c requirements/base-constraint.txt
# apache-superset
# prophet
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# fastmcp
# mcp
httpx-sse==0.4.1
# via mcp
humanize==4.12.3
# via
# -c requirements/base-constraint.txt
@@ -367,7 +400,9 @@ identify==2.5.36
idna==3.10
# via
# -c requirements/base-constraint.txt
# anyio
# email-validator
# httpx
# requests
# trio
# url-normalize
@@ -400,6 +435,7 @@ jsonschema==4.23.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
# mcp
# openapi-schema-validator
# openapi-spec-validator
jsonschema-path==0.3.4
@@ -464,6 +500,8 @@ matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mcp==1.14.1
# via fastmcp
mdurl==0.1.2
# via
# -c requirements/base-constraint.txt
@@ -503,6 +541,8 @@ odfpy==1.4.1
# via
# -c requirements/base-constraint.txt
# pandas
openapi-pydantic==0.5.1
# via fastmcp
openapi-schema-validator==0.6.3
# via
# -c requirements/base-constraint.txt
@@ -643,10 +683,16 @@ pydantic==2.11.7
# via
# -c requirements/base-constraint.txt
# apache-superset
# fastmcp
# mcp
# openapi-pydantic
# pydantic-settings
pydantic-core==2.33.2
# via
# -c requirements/base-constraint.txt
# pydantic
pydantic-settings==2.10.1
# via mcp
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -682,6 +728,8 @@ pyparsing==3.2.3
# -c requirements/base-constraint.txt
# apache-superset
# matplotlib
pyperclip==1.10.0
# via fastmcp
pysocks==1.7.1
# via
# -c requirements/base-constraint.txt
@@ -719,12 +767,16 @@ python-dotenv==1.1.0
# via
# -c requirements/base-constraint.txt
# apache-superset
# fastmcp
# pydantic-settings
python-geohash==0.8.5
# via
# -c requirements/base-constraint.txt
# apache-superset
python-ldap==3.4.4
# via apache-superset
python-multipart==0.0.20
# via mcp
pytz==2025.2
# via
# -c requirements/base-constraint.txt
@@ -779,7 +831,12 @@ rfc3339-validator==0.1.4
rich==13.9.4
# via
# -c requirements/base-constraint.txt
# cyclopts
# fastmcp
# flask-limiter
# rich-rst
rich-rst==1.3.1
# via cyclopts
rpds-py==0.25.0
# via
# -c requirements/base-constraint.txt
@@ -826,6 +883,7 @@ slack-sdk==3.35.0
sniffio==1.3.1
# via
# -c requirements/base-constraint.txt
# anyio
# trio
sortedcontainers==2.4.0
# via
@@ -856,10 +914,14 @@ sqlglot==27.15.2
# apache-superset
sqloxide==0.1.51
# via apache-superset
sse-starlette==3.0.2
# via mcp
sshtunnel==0.4.0
# via
# -c requirements/base-constraint.txt
# apache-superset
starlette==0.48.0
# via mcp
statsd==4.0.1
# via apache-superset
tabulate==0.9.0
@@ -887,8 +949,10 @@ typing-extensions==4.14.0
# via
# -c requirements/base-constraint.txt
# alembic
# anyio
# apache-superset
# cattrs
# exceptiongroup
# limits
# pydantic
# pydantic-core
@@ -896,11 +960,13 @@ typing-extensions==4.14.0
# referencing
# selenium
# shillelagh
# starlette
# typing-inspection
typing-inspection==0.4.1
# via
# -c requirements/base-constraint.txt
# pydantic
# pydantic-settings
tzdata==2025.2
# via
# -c requirements/base-constraint.txt
@@ -919,6 +985,8 @@ urllib3==2.5.0
# requests
# requests-cache
# selenium
uvicorn==0.37.0
# via mcp
vine==5.1.0
# via
# -c requirements/base-constraint.txt
+44
View File
@@ -0,0 +1,44 @@
# 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.
"""CLI module for MCP service"""
import click
@click.group()
def mcp() -> None:
"""Model Context Protocol service commands"""
pass
@mcp.command()
@click.option("--host", default="127.0.0.1", help="Host to bind to")
@click.option("--port", default=5008, help="Port to bind to")
@click.option("--debug", is_flag=True, help="Enable debug mode")
def run(host: str, port: int, debug: bool) -> None:
"""Run the MCP service"""
try:
from superset.mcp_service.server import run_server
run_server(host=host, port=port, debug=debug)
except ImportError as e:
click.echo(
f"Error: MCP service dependencies not installed: {e}\n"
"Please install with: pip install fastmcp",
err=True,
)
raise click.ClickException("MCP service not available") from e
+234
View File
@@ -0,0 +1,234 @@
<!--
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.
-->
# Superset MCP Service
> **What is this?** The MCP service allows an AI Agent to directly interact with Apache Superset, enabling natural language queries and commands for data visualization.
> **How does it work?** This service is part of the Apache Superset codebase. You need to:
> 1. Have Apache Superset installed and running
> 2. Connect an agent such as Claude Desktop to your Superset instance using this MCP service
> 3. Then Claude can create charts, query data, and manage dashboards
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. It is designed for LLM agents and automation tools, and is built on the FastMCP protocol.
## 🚀 Quickstart
### Option 1: Docker Setup (Recommended) 🎯
The fastest way to get everything running with Docker:
**Prerequisites:** Docker and Docker Compose installed
```bash
# 1. Clone the repository
git clone https://github.com/apache/superset.git
cd superset
# 2. Start Superset and MCP service with docker-compose-light
docker-compose -f docker-compose-light.yml --profile mcp build
docker-compose -f docker-compose-light.yml --profile mcp up -d
# 3. Initialize Superset (first time only)
docker exec -it superset-superset-light-1 superset fab create-admin \
--username admin \
--firstname Admin \
--lastname Admin \
--email admin@localhost \
--password admin
docker exec -it superset-superset-light-1 superset db upgrade
docker exec -it superset-superset-light-1 superset init
```
**That's it!**
- Superset frontend is running at http://localhost:9001 (login: admin/admin)
- MCP service is running on port 5008
- Now configure Claude Desktop (see Step 2 below)
#### What Docker Compose does:
- Sets up PostgreSQL database
- Builds and runs Superset containers
- Starts the MCP service (with `--profile mcp`)
- Handles all networking and dependencies
- Provides hot-reload for development
#### Customizing ports:
```bash
# Use different ports if defaults are in use
NODE_PORT=9002 MCP_PORT=5009 docker-compose -f docker-compose-light.yml --profile mcp up -d
```
### Option 2: Manual Setup
If Docker is not available, you can set up manually:
```bash
# 1. Clone the repository
git clone https://github.com/apache/superset.git
cd superset
# 2. Set up Python environment (Python 3.10 or 3.11 required)
python3 -m venv venv
source venv/bin/activate
# 3. Install dependencies
pip install -e .[development,fastmcp]
cd superset-frontend && npm ci && npm run build && cd ..
# 4. Configure Superset manually
# Create superset_config.py in your current directory:
cat > superset_config.py << 'EOF'
# Apache Superset Configuration
SECRET_KEY = '<your secret here - hint: `secrets.token_urlsafe(42)`>'
# Session configuration for local development
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_NAME = 'superset_session'
PERMANENT_SESSION_LIFETIME = 86400
# CSRF Protection (disable if login loop occurs)
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = None
# MCP Service Configuration
MCP_ADMIN_USERNAME = 'admin'
MCP_DEV_USERNAME = 'admin'
SUPERSET_WEBSERVER_ADDRESS = 'http://localhost:9001'
# WebDriver Configuration for screenshots
WEBDRIVER_BASEURL = 'http://localhost:9001/'
WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL
EOF
# 5. Initialize database
export FLASK_APP=superset
superset db upgrade
superset init
# 6. Create admin user
superset fab create-admin \
--username admin \
--firstname Admin \
--lastname Admin \
--email admin@localhost \
--password admin
# 7. Start Superset (in one terminal)
superset run -p 9001 --with-threads --reload --debugger
# 8. Start frontend (in another terminal)
cd superset-frontend && npm run dev
# 9. Start MCP service (in another terminal, only if you want MCP features)
source venv/bin/activate
superset mcp run --port 5008 --debug
```
Access Superset at http://localhost:9001 (login: admin/admin)
## 🔌 Step 2: Connect Claude Desktop
### For Docker Setup
Since the MCP service runs inside Docker on port 5008, you need to connect Claude Desktop to the HTTP endpoint:
Add this to your Claude Desktop config file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
Since claude desktop doesnt like non https mcp servers you can use this proxy:
```json
{
"mcpServers": {
"Superset MCP Proxy": {
"command": "/<superset folder>/superset/mcp_service/run_proxy.sh",
"args": [],
"env": {}
}
}
}
```
### For Local Setup (Make/Manual)
If running MCP locally (not in Docker), use the direct connection:
```json
{
"mcpServers": {
"superset": {
"command": "npx",
"args": ["/path/to/your/superset/superset/mcp_service"],
"env": {
"PYTHONPATH": "/path/to/your/superset"
}
}
}
}
```
Then restart Claude Desktop. That's it! ✨
### Alternative Connection Methods
<details>
<summary>Direct STDIO with npx</summary>
```json
{
"mcpServers": {
"superset": {
"command": "npx",
"args": ["/absolute/path/to/your/superset/superset/mcp_service", "--stdio"],
"env": {
"PYTHONPATH": "/absolute/path/to/your/superset",
"MCP_ADMIN_USERNAME": "admin"
}
}
}
}
```
</details>
<details>
<summary>Direct STDIO with Python</summary>
```json
{
"mcpServers": {
"superset": {
"command": "/absolute/path/to/your/superset/venv/bin/python",
"args": ["-m", "superset.mcp_service"],
"env": {
"PYTHONPATH": "/absolute/path/to/your/superset"
}
}
}
}
```
</details>
### 📍 Claude Desktop Config Location
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
+44
View File
@@ -0,0 +1,44 @@
# 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.
# superset/mcp_service/__init__.py
"""
Apache Superset MCP Service
This package provides the Model Context Protocol (MCP) service for Apache Superset,
enabling programmatic access to Superset's functionality through a standardized API.
The MCP service operates as a standalone FastMCP server.
Quick Start:
-----------
# Run the MCP server
superset mcp run --port 5009
# The service will be available at:
# http://localhost:5009/mcp/
"""
__version__ = "1.0.0"
# Tools are auto-registered when imported by the MCP service
# Do not import them here to avoid test pollution
__all__ = [
"__version__",
]
+132
View File
@@ -0,0 +1,132 @@
# 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.
"""
Main entry point for running the MCP service in stdio mode.
This allows running the service with: python -m superset.mcp_service
"""
import contextlib
import io
import logging
import os
import sys
from typing import Any
# Must redirect click output BEFORE importing anything that uses it
import click
# Monkey-patch click to redirect output to stderr in stdio mode
if os.environ.get("FASTMCP_TRANSPORT", "stdio") == "stdio":
original_secho = click.secho
def secho_to_stderr(*args: Any, **kwargs: Any) -> Any:
kwargs["file"] = sys.stderr
return original_secho(*args, **kwargs)
click.secho = secho_to_stderr
click.echo = lambda *args, **kwargs: click.echo(*args, file=sys.stderr, **kwargs)
from superset.mcp_service.app import init_fastmcp_server, mcp
def main() -> None:
"""
Run the MCP service in stdio mode with proper output suppression.
"""
# Determine if we're running in stdio mode
transport = os.environ.get("FASTMCP_TRANSPORT", "stdio")
if transport == "stdio":
# Suppress ALL output to stdout except for MCP messages
# This includes Flask initialization messages, warnings, etc.
# Redirect stderr to suppress logging output
# We'll keep stderr for debugging if needed
logging.basicConfig(
level=logging.CRITICAL, # Only show critical errors
stream=sys.stderr,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Disable all Flask/Superset logging to stdout
for logger_name in [
"superset",
"flask",
"werkzeug",
"sqlalchemy",
"flask_appbuilder",
"celery",
"alembic",
]:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.CRITICAL)
# Filter out stdout handlers safely
new_handlers = []
for h in logger.handlers:
if hasattr(h, "stream") and h.stream != sys.stdout:
new_handlers.append(h)
elif not hasattr(h, "stream"):
# Keep handlers that don't have a stream attribute
new_handlers.append(h)
logger.handlers = new_handlers
# Capture any print statements during initialization
captured_output = io.StringIO()
# Set up Flask app context for database access
from superset.mcp_service.flask_singleton import get_flask_app
# Temporarily redirect stdout during Flask app creation
with contextlib.redirect_stdout(captured_output):
flask_app = get_flask_app()
# Initialize the FastMCP server
# Disable auth config for stdio mode to avoid Flask app output
init_fastmcp_server()
# Log captured output to stderr for debugging (optional)
captured = captured_output.getvalue()
if captured and flask_app.config.get("MCP_DEBUG"):
sys.stderr.write(f"[MCP] Suppressed initialization output:\n{captured}\n")
# Run in Flask app context
with flask_app.app_context():
# Run in stdio mode - this will handle JSON-RPC communication
sys.stderr.write("[MCP] Starting in stdio mode (stdin/stdout)\n")
sys.stderr.flush()
try:
mcp.run(transport="stdio")
except (BrokenPipeError, ConnectionResetError) as e:
# Handle client disconnection gracefully
sys.stderr.write(f"[MCP] Client disconnected: {e}\n")
sys.exit(0)
else:
# For other transports, use normal initialization
init_fastmcp_server()
# Run with specified transport
if transport == "streamable-http":
host = os.environ.get("FASTMCP_HOST", "127.0.0.1")
port = int(os.environ.get("FASTMCP_PORT", "5008"))
mcp.run(transport=transport, host=host, port=port)
else:
mcp.run(transport=transport)
if __name__ == "__main__":
main()
+266
View File
@@ -0,0 +1,266 @@
# 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.
"""
FastMCP app factory and initialization for Superset MCP service.
This file provides a configurable factory function to create FastMCP instances
following the Flask application factory pattern. All tool modules should import
mcp from here and use @mcp.tool decorators.
"""
import logging
from typing import Any, Callable, Dict, List, Set
from fastmcp import FastMCP
logger = logging.getLogger(__name__)
# Default instructions for the Superset MCP service
DEFAULT_INSTRUCTIONS = """
You are connected to the Apache Superset MCP (Model Context Protocol) service.
This service provides programmatic access to Superset dashboards, charts, datasets,
SQL Lab, and instance metadata via a comprehensive set of tools.
Available tools:
Dashboard Management:
- list_dashboards: List dashboards with advanced filters (1-based pagination)
- get_dashboard_info: Get detailed dashboard information by ID
- get_dashboard_available_filters: List available dashboard filter fields/operators
- generate_dashboard: Automatically create a dashboard from datasets with AI
- add_chart_to_existing_dashboard: Add a chart to an existing dashboard
Dataset Management:
- list_datasets: List datasets with advanced filters (1-based pagination)
- get_dataset_info: Get detailed dataset information by ID
- get_dataset_available_filters: List available dataset filter fields/operators
Chart Management:
- list_charts: List charts with advanced filters (1-based pagination)
- get_chart_info: Get detailed chart information by ID
- get_chart_preview: Get a visual preview of a chart with image URL
- get_chart_data: Get underlying chart data in text-friendly format
- get_chart_available_filters: List available chart filter fields/operators
- generate_chart: Create a new chart with AI assistance
- update_chart: Update existing chart configuration
- update_chart_preview: Update chart and get preview in one operation
SQL Lab Integration:
- execute_sql: Execute SQL queries and get results
- open_sql_lab_with_context: Generate SQL Lab URL with pre-filled query
Explore & Analysis:
- generate_explore_link: Create pre-configured explore URL with dataset/metrics/filters
System Information:
- get_superset_instance_info: Get instance-wide statistics and metadata
Available Resources:
- superset://instance/metadata: Access instance configuration and metadata
- superset://chart/templates: Access chart configuration templates
Available Prompts:
- superset_quickstart: Interactive guide for getting started with the MCP service
- create_chart_guided: Step-by-step chart creation wizard
General usage tips:
- All listing tools use 1-based pagination (first page is 1)
- Use 'filters' parameter for advanced queries (see *_available_filters tools)
- IDs can be integer or UUID format where supported
- All tools return structured, Pydantic-typed responses
- Chart previews are served as PNG images via custom screenshot endpoints
If you are unsure which tool to use, start with get_superset_instance_info
or use the superset_quickstart prompt for an interactive guide.
"""
def _build_mcp_kwargs(
name: str,
instructions: str,
auth: Any | None,
lifespan: Callable[..., Any] | None,
tools: List[Any] | None,
include_tags: Set[str] | None,
exclude_tags: Set[str] | None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Build FastMCP constructor arguments."""
mcp_kwargs: Dict[str, Any] = {
"name": name,
"instructions": instructions,
}
# Add optional parameters if provided
if auth is not None:
mcp_kwargs["auth"] = auth
if lifespan is not None:
mcp_kwargs["lifespan"] = lifespan
if tools is not None:
mcp_kwargs["tools"] = tools
if include_tags is not None:
mcp_kwargs["include_tags"] = include_tags
if exclude_tags is not None:
mcp_kwargs["exclude_tags"] = exclude_tags
# Add any additional kwargs
mcp_kwargs.update(kwargs)
return mcp_kwargs
def _apply_config(mcp_instance: FastMCP, config: Dict[str, Any] | None) -> None:
"""Apply additional configuration to FastMCP instance."""
if config:
for key, value in config.items():
setattr(mcp_instance, key, value)
def _log_instance_creation(
name: str,
auth: Any | None,
include_tags: Set[str] | None,
exclude_tags: Set[str] | None,
) -> None:
"""Log FastMCP instance creation details."""
logger.info("Created FastMCP instance: %s", name)
if auth:
logger.info("Authentication enabled")
if include_tags or exclude_tags:
logger.info(
"Tag filtering enabled - include: %s, exclude: %s",
include_tags,
exclude_tags,
)
def create_mcp_app(
name: str = "Superset MCP Server",
instructions: str | None = None,
auth: Any | None = None,
lifespan: Callable[..., Any] | None = None,
tools: List[Any] | None = None,
include_tags: Set[str] | None = None,
exclude_tags: Set[str] | None = None,
config: Dict[str, Any] | None = None,
**kwargs: Any,
) -> FastMCP:
"""
Application factory for creating FastMCP instances.
This follows the Flask application factory pattern, allowing users to
configure the FastMCP instance with custom authentication, middleware,
and other settings.
Args:
name: Human-readable server name
instructions: Server description and usage instructions
auth: Authentication provider for securing HTTP transports
lifespan: Async context manager for startup/shutdown logic
tools: List of tools or functions to add to the server
include_tags: Set of tags to include (whitelist)
exclude_tags: Set of tags to exclude (blacklist)
config: Additional configuration dictionary
**kwargs: Additional FastMCP constructor arguments
Returns:
Configured FastMCP instance
"""
# Use default instructions if none provided
if instructions is None:
instructions = DEFAULT_INSTRUCTIONS
# Build FastMCP constructor arguments
mcp_kwargs = _build_mcp_kwargs(
name, instructions, auth, lifespan, tools, include_tags, exclude_tags, **kwargs
)
# Create the FastMCP instance
mcp_instance = FastMCP(**mcp_kwargs)
# Apply any additional configuration
_apply_config(mcp_instance, config)
# Log instance creation
_log_instance_creation(name, auth, include_tags, exclude_tags)
return mcp_instance
# Create default MCP instance for backward compatibility
# Tool modules can import this and use @mcp.tool decorators
mcp = create_mcp_app()
from superset.mcp_service.system.tool import health_check # noqa: F401, E402
def init_fastmcp_server(
name: str = "Superset MCP Server",
instructions: str | None = None,
auth: Any | None = None,
lifespan: Callable[..., Any] | None = None,
tools: List[Any] | None = None,
include_tags: Set[str] | None = None,
exclude_tags: Set[str] | None = None,
config: Dict[str, Any] | None = None,
**kwargs: Any,
) -> FastMCP:
"""
Initialize and configure the FastMCP server.
This function provides a way to create a custom FastMCP instance
instead of using the default global one. If parameters are provided,
a new instance will be created with those settings.
Args:
Same as create_mcp_app()
Returns:
FastMCP instance (either the global one or a new custom one)
"""
# If any custom parameters are provided, create a new instance
custom_params_provided = any(
[
name != "Superset MCP Server",
instructions is not None,
auth is not None,
lifespan is not None,
tools is not None,
include_tags is not None,
exclude_tags is not None,
config is not None,
kwargs,
]
)
if custom_params_provided:
logger.info("Creating custom FastMCP instance with provided configuration")
return create_mcp_app(
name=name,
instructions=instructions,
auth=auth,
lifespan=lifespan,
tools=tools,
include_tags=include_tags,
exclude_tags=exclude_tags,
config=config,
**kwargs,
)
else:
# Use the default global instance
logger.setLevel(logging.DEBUG)
logger.info("Using default FastMCP instance - scaffold version without auth")
return mcp
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env node
/**
* 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.
*/
/**
* Apache Superset MCP (Model Context Protocol) Server Runner
*
* OVERVIEW:
* This Node.js wrapper script provides an npx-compatible entry point for the Superset MCP service.
* It acts as a bridge between npm/npx tooling and the Python-based MCP server implementation.
*
* FUNCTIONALITY:
* - Detects and validates Python environment and Superset installation
* - Supports both stdio (Claude Desktop integration) and HTTP transport modes
* - Handles command-line argument parsing and environment variable configuration
* - Manages Python subprocess lifecycle with proper signal handling
* - Provides comprehensive help documentation and error diagnostics
*
* USAGE PATTERNS (DEVELOPMENT - Not yet published to npm):
* - Direct execution: node superset/mcp_service/bin/superset-mcp.js --stdio
* - HTTP server: node superset/mcp_service/bin/superset-mcp.js --http --port 6000
* - Development debugging: node superset/mcp_service/bin/superset-mcp.js --debug
*
* FUTURE USAGE (Once published to npm registry):
* - npx @superset/mcp-server --stdio
* - npx @superset/mcp-server --http --port 6000
*
* ARCHITECTURE:
* This wrapper enables the MCP service to be distributed as an npm package while
* maintaining the core Python implementation, bridging Node.js tooling with Python execution.
*
* PACKAGE STATUS (as of 2025-01-10):
* - NOT YET PUBLISHED to npm registry
* - Package name reserved: @superset/mcp-server
* - Requires package.json with proper metadata and "bin" field for npx execution
* - Will need to be published to npm registry before npx commands work
*
* TODO FOR NPM PUBLISHING:
* 1. Create package.json with name "@superset/mcp-server"
* 2. Add "bin" field pointing to this file
* 3. Set version, description, repository, license
* 4. Run npm publish with appropriate access rights
*/
const { spawn, execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
// Parse command line arguments
const args = process.argv.slice(2);
const isStdio = args.includes('--stdio') || process.env.FASTMCP_TRANSPORT === 'stdio';
const isDebug = args.includes('--debug') || process.env.MCP_DEBUG === '1';
const showHelp = args.includes('--help') || args.includes('-h');
// Configuration
const DEFAULT_PORT = process.env.MCP_PORT || '5008';
const DEFAULT_HOST = process.env.MCP_HOST || '127.0.0.1';
// Show help
if (showHelp) {
console.log(`
Apache Superset MCP Server
Usage:
Development: node superset/mcp_service/bin/superset-mcp.js [options]
Future (npm): npx @superset/mcp-server [options]
Options:
--stdio Run in stdio mode for direct Claude Desktop integration
--http Run in HTTP mode (default)
--port PORT HTTP port to bind to (default: ${DEFAULT_PORT})
--host HOST HTTP host to bind to (default: ${DEFAULT_HOST})
--debug Enable debug mode
--help Show this help message
Environment Variables:
FASTMCP_TRANSPORT Transport mode (stdio or http)
MCP_PORT HTTP port (default: ${DEFAULT_PORT})
MCP_HOST HTTP host (default: ${DEFAULT_HOST})
MCP_DEBUG Enable debug (set to 1)
PYTHONPATH Python path including Superset root
SUPERSET_CONFIG_PATH Path to superset_config.py
Examples (Development):
# Run in stdio mode for Claude Desktop
node superset/mcp_service/bin/superset-mcp.js --stdio
# Run in HTTP mode on custom port
node superset/mcp_service/bin/superset-mcp.js --http --port 6000
# Run with debug output
node superset/mcp_service/bin/superset-mcp.js --debug
# Or use the Python CLI directly:
superset mcp run --host 127.0.0.1 --port 6000
`);
process.exit(0);
}
// Find Superset root directory
function findSupersetRoot() {
// Start from the mcp_service directory
let currentDir = path.resolve(__dirname, '..');
// Walk up until we find the superset root (contains setup.py or pyproject.toml)
while (currentDir !== path.dirname(currentDir)) {
if (fs.existsSync(path.join(currentDir, 'pyproject.toml')) ||
fs.existsSync(path.join(currentDir, 'setup.py'))) {
// Check if it's actually the superset root (has superset directory)
if (fs.existsSync(path.join(currentDir, 'superset'))) {
return currentDir;
}
}
currentDir = path.dirname(currentDir);
}
// Fallback to environment variable
if (process.env.PYTHONPATH) {
return process.env.PYTHONPATH;
}
throw new Error('Could not find Superset root directory. Please set PYTHONPATH environment variable.');
}
// Find Python executable
function findPython() {
// Check for virtual environment in common locations
const supersetRoot = findSupersetRoot();
const venvPaths = [
path.join(supersetRoot, 'venv', 'bin', 'python'),
path.join(supersetRoot, '.venv', 'bin', 'python'),
path.join(supersetRoot, 'venv', 'Scripts', 'python.exe'),
path.join(supersetRoot, '.venv', 'Scripts', 'python.exe'),
];
for (const venvPath of venvPaths) {
if (fs.existsSync(venvPath)) {
return venvPath;
}
}
// Check if python3 is available
try {
execSync('python3 --version', { stdio: 'ignore' });
return 'python3';
} catch (e) {
// Fall back to python
return 'python';
}
}
// Check Python and Superset installation
function checkEnvironment() {
const python = findPython();
const supersetRoot = findSupersetRoot();
console.error(`Using Python: ${python}`);
console.error(`Superset root: ${supersetRoot}`);
// Check if Superset is installed
try {
execSync(`${python} -c "import superset"`, {
env: { ...process.env, PYTHONPATH: supersetRoot },
stdio: 'ignore'
});
} catch (e) {
console.error(`
Error: Superset is not installed or not accessible.
Please ensure:
1. You have activated your virtual environment
2. Superset is installed (pip install -e .)
3. PYTHONPATH is set correctly
Current PYTHONPATH: ${supersetRoot}
`);
process.exit(1);
}
return { python, supersetRoot };
}
// Main execution
function main() {
const { python, supersetRoot } = checkEnvironment();
// Prepare environment variables
const env = {
...process.env,
PYTHONPATH: supersetRoot,
FASTMCP_TRANSPORT: isStdio ? 'stdio' : 'http',
};
if (!env.SUPERSET_CONFIG_PATH) {
const configPath = path.join(supersetRoot, 'superset_config.py');
if (fs.existsSync(configPath)) {
env.SUPERSET_CONFIG_PATH = configPath;
}
}
if (isDebug) {
env.MCP_DEBUG = '1';
}
// Prepare command and arguments
let pythonArgs;
if (isStdio) {
console.error('Starting Superset MCP server in STDIO mode...');
pythonArgs = ['-m', 'superset.mcp_service'];
} else {
console.error(`Starting Superset MCP server in HTTP mode on ${DEFAULT_HOST}:${DEFAULT_PORT}...`);
// Parse port and host from arguments
const portIndex = args.indexOf('--port');
const port = portIndex !== -1 && args[portIndex + 1] ? args[portIndex + 1] : DEFAULT_PORT;
const hostIndex = args.indexOf('--host');
const host = hostIndex !== -1 && args[hostIndex + 1] ? args[hostIndex + 1] : DEFAULT_HOST;
pythonArgs = [
'-m', 'superset',
'mcp', 'run',
'--host', host,
'--port', port
];
if (isDebug) {
pythonArgs.push('--debug');
}
}
// Spawn the Python process
const pythonProcess = spawn(python, pythonArgs, {
env,
stdio: isStdio ? ['inherit', 'inherit', 'inherit'] : 'inherit',
cwd: supersetRoot
});
// Handle process events
pythonProcess.on('error', (err) => {
console.error('Failed to start MCP server:', err);
process.exit(1);
});
pythonProcess.on('exit', (code, signal) => {
if (signal) {
console.error(`MCP server terminated by signal: ${signal}`);
} else if (code !== 0) {
console.error(`MCP server exited with code: ${code}`);
}
process.exit(code || 0);
});
// Handle termination signals
process.on('SIGINT', () => {
pythonProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
pythonProcess.kill('SIGTERM');
});
}
// Run the main function
main();
+26
View File
@@ -0,0 +1,26 @@
# 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.
"""
Common schemas and utilities for MCP service.
"""
from superset.mcp_service.system.schemas import HealthCheckResponse
__all__ = [
"HealthCheckResponse",
]
+60
View File
@@ -0,0 +1,60 @@
# 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.
"""
Simple module-level Flask app instance for MCP service.
Following the Stack Overflow recommendation:
"a simple module with just the instance is enough"
- The module itself acts as the singleton
- No need for complex patterns or metaclasses
- Clean and Pythonic approach
"""
import logging
from flask import Flask
logger = logging.getLogger(__name__)
logger.info("Creating Flask app instance for MCP service")
try:
from superset.app import create_app
from superset.mcp_service.mcp_config import get_mcp_config
# Create the Flask app instance - this is the singleton
app = create_app()
# Apply MCP configuration - reads from app.config first, falls back to defaults
mcp_config = get_mcp_config(app.config)
app.config.update(mcp_config)
logger.info("Flask app instance created successfully")
except Exception as e:
logger.error("Failed to create Flask app: %s", e)
raise
def get_flask_app() -> Flask:
"""
Get the Flask app instance.
Returns:
Flask: The module-level Flask app instance
"""
return app
+56
View File
@@ -0,0 +1,56 @@
/**
* 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.
*/
/**
* Apache Superset MCP Server
*
* Entry point for the MCP server when used as a Node.js module.
*/
const { spawn } = require('child_process');
const path = require('path');
class SupersetMCPServer {
constructor(options = {}) {
this.options = {
transport: options.transport || 'http',
host: options.host || '127.0.0.1',
port: options.port || 5008,
debug: options.debug || false,
pythonPath: options.pythonPath || null,
supersetRoot: options.supersetRoot || null,
configPath: options.configPath || null,
};
this.process = null;
}
start() {
const runner = require('./bin/superset-mcp.js');
// The bin script handles the execution
}
stop() {
if (this.process) {
this.process.kill();
this.process = null;
}
}
}
module.exports = SupersetMCPServer;
+125
View File
@@ -0,0 +1,125 @@
# 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.
"""Default MCP service configuration for Apache Superset"""
from typing import Any, Dict
# MCP Service Configuration
MCP_ADMIN_USERNAME = "admin"
MCP_DEV_USERNAME = "admin"
SUPERSET_WEBSERVER_ADDRESS = "http://localhost:9001"
# WebDriver Configuration for screenshots
WEBDRIVER_BASEURL = "http://localhost:9001/"
WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL
# Feature flags for MCP
MCP_FEATURE_FLAGS: Dict[str, Any] = {
"MCP_SERVICE": True,
}
# MCP Service Host/Port
MCP_SERVICE_HOST = "localhost"
MCP_SERVICE_PORT = 5008
# MCP Debug mode - shows suppressed initialization output in stdio mode
MCP_DEBUG = False
# Session configuration for local development
MCP_SESSION_CONFIG = {
"SESSION_COOKIE_HTTPONLY": True,
"SESSION_COOKIE_SECURE": False,
"SESSION_COOKIE_SAMESITE": "Lax",
"SESSION_COOKIE_NAME": "superset_session",
"PERMANENT_SESSION_LIFETIME": 86400,
}
# CSRF Protection
MCP_CSRF_CONFIG = {
"WTF_CSRF_ENABLED": True,
"WTF_CSRF_TIME_LIMIT": None,
}
# FastMCP Factory Configuration
MCP_FACTORY_CONFIG = {
"name": "Superset MCP Server",
"instructions": None, # Will use default from app.py
"auth": None, # No authentication by default
"lifespan": None, # No custom lifespan
"tools": None, # Auto-discover tools
"include_tags": None, # Include all tags
"exclude_tags": None, # Exclude no tags
"config": None, # No additional config
}
def get_mcp_config(app_config: Dict[str, Any] | None = None) -> Dict[str, Any]:
"""
Get complete MCP configuration dictionary.
Reads from app_config first, then falls back to defaults if values are not provided.
Args:
app_config: Optional Flask app configuration dict to read values from
"""
app_config = app_config or {}
# Default MCP configuration
defaults = {
"MCP_ADMIN_USERNAME": MCP_ADMIN_USERNAME,
"MCP_DEV_USERNAME": MCP_DEV_USERNAME,
"SUPERSET_WEBSERVER_ADDRESS": SUPERSET_WEBSERVER_ADDRESS,
"WEBDRIVER_BASEURL": WEBDRIVER_BASEURL,
"WEBDRIVER_BASEURL_USER_FRIENDLY": WEBDRIVER_BASEURL_USER_FRIENDLY,
"MCP_SERVICE_HOST": MCP_SERVICE_HOST,
"MCP_SERVICE_PORT": MCP_SERVICE_PORT,
"MCP_DEBUG": MCP_DEBUG,
**MCP_SESSION_CONFIG,
**MCP_CSRF_CONFIG,
}
# Merge app_config over defaults - app_config takes precedence
return {**defaults, **{k: v for k, v in app_config.items() if k in defaults}}
def get_mcp_config_with_overrides(
app_config: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""
Alternative approach: Allow any app_config keys, not just predefined ones.
This version lets users add custom MCP config keys in superset_config.py
that aren't predefined in the defaults.
"""
app_config = app_config or {}
defaults = get_mcp_config()
# Start with defaults, then overlay any app_config values
return {**defaults, **app_config}
def get_mcp_factory_config() -> Dict[str, Any]:
"""
Get FastMCP factory configuration.
This can be customized by users to provide their own auth providers,
middleware, lifespan handlers, and other FastMCP configuration.
Returns:
Dictionary of FastMCP factory configuration options
"""
return MCP_FACTORY_CONFIG.copy()
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@superset/mcp-server",
"version": "1.0.0",
"description": "Apache Superset MCP (Model Context Protocol) Server",
"main": "index.js",
"bin": {
"superset-mcp": "./bin/superset-mcp.js"
},
"scripts": {
"start": "node bin/superset-mcp.js",
"stdio": "node bin/superset-mcp.js --stdio",
"http": "node bin/superset-mcp.js --http"
},
"keywords": [
"mcp",
"superset",
"apache-superset",
"model-context-protocol",
"ai",
"claude"
],
"author": "Apache Superset Contributors",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset/mcp_service"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {},
"devDependencies": {},
"preferGlobal": false
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# 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.
## use in claude like this
# "Superset MCP Proxy": {
# "command": "~/github/superset/superset/mcp_service/run_proxy.sh",
# "args": [],
# "env": {}
# },
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the project root (two levels up from mcp_service)
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Use python from the virtual environment if it exists, otherwise use system python
if [ -f "$PROJECT_ROOT/venv/bin/python" ]; then
PYTHON_PATH="$PROJECT_ROOT/venv/bin/python"
elif [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
PYTHON_PATH="$PROJECT_ROOT/.venv/bin/python"
else
PYTHON_PATH="python3"
fi
# Run the proxy script
"$PYTHON_PATH" "$SCRIPT_DIR/simple_proxy.py"
+101
View File
@@ -0,0 +1,101 @@
# 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.
"""
MCP server for Apache Superset
"""
import logging
import os
from superset.mcp_service.app import create_mcp_app, init_fastmcp_server
from superset.mcp_service.mcp_config import get_mcp_factory_config
def configure_logging(debug: bool = False) -> None:
"""Configure logging for the MCP service."""
import sys
if debug or os.environ.get("SQLALCHEMY_DEBUG"):
# Only configure basic logging if no handlers exist (respects logging.ini)
root_logger = logging.getLogger()
if not root_logger.handlers:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
stream=sys.stderr, # Always log to stderr, not stdout
)
# Only override SQLAlchemy logger levels if they're not explicitly configured
for logger_name in [
"sqlalchemy.engine",
"sqlalchemy.pool",
"sqlalchemy.dialects",
]:
logger = logging.getLogger(logger_name)
# Only set level if it's still at default (WARNING for SQLAlchemy)
if logger.level == logging.WARNING or logger.level == logging.NOTSET:
logger.setLevel(logging.INFO)
# Use logging instead of print to avoid stdout contamination
logging.info("🔍 SQL Debug logging enabled")
def run_server(
host: str = "127.0.0.1",
port: int = 5008,
debug: bool = False,
use_factory_config: bool = False,
) -> None:
"""
Run the MCP service server with FastMCP endpoints.
Uses streamable-http transport for HTTP server mode.
Args:
host: Host to bind to
port: Port to bind to
debug: Enable debug logging
use_factory_config: Use configuration from get_mcp_factory_config()
"""
configure_logging(debug)
if use_factory_config:
# Use factory configuration for customization
logging.info("Creating MCP app from factory configuration...")
factory_config = get_mcp_factory_config()
mcp_instance = create_mcp_app(**factory_config)
else:
# Use default initialization
logging.info("Creating MCP app with default configuration...")
mcp_instance = init_fastmcp_server()
env_key = f"FASTMCP_RUNNING_{port}"
if not os.environ.get(env_key):
os.environ[env_key] = "1"
try:
logging.info("Starting FastMCP on %s:%s", host, port)
mcp_instance.run(transport="streamable-http", host=host, port=port)
except Exception as e:
logging.error("FastMCP failed: %s", e)
os.environ.pop(env_key, None)
else:
logging.info("FastMCP already running on %s:%s", host, port)
if __name__ == "__main__":
run_server()
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# 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.
"""
Simple MCP proxy server that connects to FastMCP server on localhost:5008
"""
import logging
import signal
import sys
from typing import Any
from fastmcp import FastMCP
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Global proxy instance for cleanup
proxy: FastMCP | None = None
def signal_handler(signum: int, frame: Any) -> None:
"""Handle shutdown signals gracefully"""
logger.info("Received signal %s, shutting down gracefully...", signum)
# FastMCP.as_proxy() handles its own cleanup
sys.exit(0)
def main() -> None:
"""Main function to run the proxy"""
global proxy
try:
from fastmcp import FastMCP
# Set up signal handlers for graceful shutdown
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
logger.info("Starting MCP proxy server...")
# Create a proxy to the remote FastMCP server
proxy = FastMCP.as_proxy(
"http://localhost:5008/mcp/", name="Superset MCP Proxy"
)
logger.info("Proxy created successfully, starting...")
# Run the proxy (this will block until interrupted)
proxy.run()
except KeyboardInterrupt:
logger.info("Received keyboard interrupt, shutting down...")
sys.exit(0)
except ImportError as e:
logger.error("Failed to import FastMCP: %s", e)
logger.error("Please install fastmcp: pip install fastmcp")
sys.exit(1)
except Exception as e:
logger.error("Unexpected error: %s", e)
sys.exit(1)
finally:
logger.info("Proxy server stopped")
if __name__ == "__main__":
main()
+16
View File
@@ -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.
+40
View File
@@ -0,0 +1,40 @@
# 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.
"""
Common schemas shared across multiple MCP tools.
This module contains Pydantic models that are used by multiple tools
or represent common response patterns across the MCP service.
"""
from pydantic import BaseModel
class HealthCheckResponse(BaseModel):
"""Response model for health check.
Used by health check tool to return service status and system information.
"""
status: str
timestamp: str
service: str
version: str
python_version: str
platform: str
uptime_seconds: float
@@ -0,0 +1,18 @@
# 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.
"""System tools for MCP service."""
@@ -0,0 +1,67 @@
# 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.
"""Simple health check tool for testing MCP service."""
import datetime
import logging
import platform
import time
from superset.mcp_service.app import mcp
from superset.mcp_service.system.schemas import HealthCheckResponse
logger = logging.getLogger(__name__)
@mcp.tool
async def health_check() -> HealthCheckResponse:
"""
Simple health check tool for testing the MCP service.
Returns basic system information and confirms the service is running.
This is useful for testing connectivity and basic functionality.
Returns:
HealthCheckResponse: Health status and system information
"""
try:
response = HealthCheckResponse(
status="healthy",
timestamp=datetime.datetime.now().isoformat(),
service="Superset MCP Service",
version="1.0.0",
python_version=platform.python_version(),
platform=platform.system(),
uptime_seconds=time.time(), # Simple uptime approximation
)
logger.info("Health check completed successfully")
return response
except Exception as e:
logger.error("Health check failed: %s", e)
# Return error status but don't raise to keep tool working
return HealthCheckResponse(
status="error",
timestamp=datetime.datetime.now().isoformat(),
service="Superset MCP Service",
version="1.0.0",
python_version=platform.python_version(),
platform=platform.system(),
uptime_seconds=0.0,
)
@@ -372,6 +372,8 @@ class TestChartsUpdateCommand(SupersetTestCase):
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_update_v1_response(self, mock_sm_g, mock_c_g, mock_u_g):
"""Test that a chart command updates properties"""
import time
pk = db.session.query(Slice).all()[0].id
user = security_manager.find_user(username="admin")
mock_u_g.user = mock_c_g.user = mock_sm_g.user = user
@@ -383,6 +385,7 @@ class TestChartsUpdateCommand(SupersetTestCase):
}
command = UpdateChartCommand(model_id, json_obj)
last_saved_before = db.session.query(Slice).get(pk).last_saved_at
time.sleep(0.01) # Ensure timestamp will be different
command.run()
chart = db.session.query(Slice).get(pk)
assert chart.last_saved_at != last_saved_before