mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
feat(mcp): PR1 - Add MCP service scaffold for Apache Superset (#35163)
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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__",
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Executable
+282
@@ -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();
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user