From 8c52934057fd80f644a1b520622bbf708c3662a0 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Tue, 16 Sep 2025 00:09:51 -0400 Subject: [PATCH 01/16] feat: Add MCP (Model Context Protocol) service scaffold for Apache Superset This commit introduces the basic scaffold for the MCP service that enables AI agents to interact with Apache Superset programmatically. ## Key Components ### Core Structure - **CLI Interface**: `superset mcp run` and `superset mcp setup` commands - **FastMCP Server**: HTTP-based MCP server using FastMCP protocol - **Flask Integration**: Singleton pattern for Superset app integration - **Configuration Setup**: Automated setup script for MCP service config ### Files Added - `superset/cli/mcp.py`: CLI commands for running and setting up MCP service - `superset/mcp_service/`: Core MCP service package with server, app, and config - `superset/mcp_service/scripts/setup.py`: Configuration setup utilities - `superset/config_mcp.py`: MCP-specific configuration template - Development container configurations for MCP-enabled development ### Features - Basic HTTP transport for MCP communication - Configuration management for Superset integration - Development environment setup with Docker support - Node.js proxy support for alternative connection methods This scaffold provides the foundation for subsequent PRs that will add: - Authentication and authorization - Tool implementations (dashboards, charts, datasets) - Advanced middleware and permissions - Production-ready features The implementation follows Superset's architectural patterns and can be extended incrementally without breaking changes. --- .devcontainer/README.md | 11 - .devcontainer/default/devcontainer.json | 19 ++ .devcontainer/devcontainer-base.json | 39 ++++ .devcontainer/setup-dev.sh | 84 ++------ .devcontainer/start-superset.sh | 83 ++----- .devcontainer/with-mcp/devcontainer.json | 29 +++ docker/docker-bootstrap.sh | 4 + requirements/development.in | 2 +- superset/cli/mcp.py | 46 ++++ superset/config_mcp.py | 44 ++++ superset/mcp_service/README.md | 184 ++++++++++++++++ superset/mcp_service/__init__.py | 41 ++++ superset/mcp_service/__main__.py | 136 ++++++++++++ superset/mcp_service/app.py | 100 +++++++++ superset/mcp_service/bin/superset-mcp.js | 263 +++++++++++++++++++++++ superset/mcp_service/flask_singleton.py | 78 +++++++ superset/mcp_service/index.js | 37 ++++ superset/mcp_service/package.json | 35 +++ superset/mcp_service/run_proxy.sh | 26 +++ superset/mcp_service/scripts/__init__.py | 17 ++ superset/mcp_service/scripts/setup.py | 149 +++++++++++++ superset/mcp_service/server.py | 74 +++++++ superset/mcp_service/simple_proxy.py | 74 +++++++ superset/mcp_service/utils/__init__.py | 18 ++ 24 files changed, 1455 insertions(+), 138 deletions(-) create mode 100644 .devcontainer/default/devcontainer.json create mode 100644 .devcontainer/devcontainer-base.json create mode 100644 .devcontainer/with-mcp/devcontainer.json create mode 100644 superset/cli/mcp.py create mode 100644 superset/config_mcp.py create mode 100644 superset/mcp_service/README.md create mode 100644 superset/mcp_service/__init__.py create mode 100644 superset/mcp_service/__main__.py create mode 100644 superset/mcp_service/app.py create mode 100644 superset/mcp_service/bin/superset-mcp.js create mode 100644 superset/mcp_service/flask_singleton.py create mode 100644 superset/mcp_service/index.js create mode 100644 superset/mcp_service/package.json create mode 100644 superset/mcp_service/run_proxy.sh create mode 100644 superset/mcp_service/scripts/__init__.py create mode 100644 superset/mcp_service/scripts/setup.py create mode 100644 superset/mcp_service/server.py create mode 100644 superset/mcp_service/simple_proxy.py create mode 100644 superset/mcp_service/utils/__init__.py diff --git a/.devcontainer/README.md b/.devcontainer/README.md index e5dda78fe309..6b24183edc5e 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -3,14 +3,3 @@ For complete documentation on using GitHub Codespaces with Apache Superset, please see: **[Setting up a Development Environment - GitHub Codespaces](https://superset.apache.org/docs/contributing/development#github-codespaces-cloud-development)** - -## Pre-installed Development Environment - -When you create a new Codespace from this repository, it automatically: - -1. **Creates a Python virtual environment** using `uv venv` -2. **Installs all development dependencies** via `uv pip install -r requirements/development.txt` -3. **Sets up pre-commit hooks** with `pre-commit install` -4. **Activates the virtual environment** automatically in all terminals - -The virtual environment is located at `/workspaces/{repository-name}/.venv` and is automatically activated through environment variables set in the devcontainer configuration. diff --git a/.devcontainer/default/devcontainer.json b/.devcontainer/default/devcontainer.json new file mode 100644 index 000000000000..d09883679470 --- /dev/null +++ b/.devcontainer/default/devcontainer.json @@ -0,0 +1,19 @@ +{ + // Extend the base configuration + "extends": "../devcontainer-base.json", + + "name": "Apache Superset Development (Default)", + + // Forward ports for development + "forwardPorts": [9001], + "portsAttributes": { + "9001": { + "label": "Superset (via Webpack Dev Server)", + "onAutoForward": "notify", + "visibility": "public" + } + }, + + // Auto-start Superset on Codespace resume + "postStartCommand": ".devcontainer/start-superset.sh" +} diff --git a/.devcontainer/devcontainer-base.json b/.devcontainer/devcontainer-base.json new file mode 100644 index 000000000000..59ed6ee1d2fc --- /dev/null +++ b/.devcontainer/devcontainer-base.json @@ -0,0 +1,39 @@ +{ + "name": "Apache Superset Development", + // Keep this in sync with the base image in Dockerfile (ARG PY_VER) + // Using the same base as Dockerfile, but non-slim for dev tools + "image": "python:3.11.13-bookworm", + + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": true, + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/common-utils:2": { + "configureZshAsDefaultShell": true + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + } + }, + + // Run commands after container is created + "postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh", + + // VS Code customizations + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] + } + } +} diff --git a/.devcontainer/setup-dev.sh b/.devcontainer/setup-dev.sh index 91482551bee3..f85211890097 100755 --- a/.devcontainer/setup-dev.sh +++ b/.devcontainer/setup-dev.sh @@ -3,76 +3,30 @@ echo "๐Ÿ”ง Setting up Superset development environment..." -# System dependencies and uv are now pre-installed in the Docker image -# This speeds up Codespace creation significantly! - -# Create virtual environment using uv -echo "๐Ÿ Creating Python virtual environment..." -if ! uv venv; then - echo "โŒ Failed to create virtual environment" - exit 1 -fi - -# Install Python dependencies -echo "๐Ÿ“ฆ Installing Python dependencies..." -if ! uv pip install -r requirements/development.txt; then - echo "โŒ Failed to install Python dependencies" - echo "๐Ÿ’ก You may need to run this manually after the Codespace starts" - exit 1 -fi - -# Install pre-commit hooks -echo "๐Ÿช Installing pre-commit hooks..." -if source .venv/bin/activate && pre-commit install; then - echo "โœ… Pre-commit hooks installed" -else - echo "โš ๏ธ Pre-commit hooks installation failed (non-critical)" -fi +# The universal image has most tools, just need Superset-specific libs +echo "๐Ÿ“ฆ Installing Superset-specific dependencies..." +sudo apt-get update +sudo apt-get install -y \ + libsasl2-dev \ + libldap2-dev \ + libpq-dev \ + tmux \ + gh + +# Install uv for fast Python package management +echo "๐Ÿ“ฆ Installing uv..." +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Add cargo/bin to PATH for uv +echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc +echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc # Install Claude Code CLI via npm echo "๐Ÿค– Installing Claude Code..." -if npm install -g @anthropic-ai/claude-code; then - echo "โœ… Claude Code installed" -else - echo "โš ๏ธ Claude Code installation failed (non-critical)" -fi +npm install -g @anthropic-ai/claude-code # Make the start script executable chmod +x .devcontainer/start-superset.sh -# Add bashrc additions for automatic venv activation -echo "๐Ÿ”ง Setting up automatic environment activation..." -if [ -f ~/.bashrc ]; then - # Check if we've already added our additions - if ! grep -q "Superset Codespaces environment setup" ~/.bashrc; then - echo "" >> ~/.bashrc - cat .devcontainer/bashrc-additions >> ~/.bashrc - echo "โœ… Added automatic venv activation to ~/.bashrc" - else - echo "โœ… Bashrc additions already present" - fi -else - # Create bashrc if it doesn't exist - cat .devcontainer/bashrc-additions > ~/.bashrc - echo "โœ… Created ~/.bashrc with automatic venv activation" -fi - -# Also add to zshrc since that's the default shell -if [ -f ~/.zshrc ] || [ -n "$ZSH_VERSION" ]; then - if ! grep -q "Superset Codespaces environment setup" ~/.zshrc; then - echo "" >> ~/.zshrc - cat .devcontainer/bashrc-additions >> ~/.zshrc - echo "โœ… Added automatic venv activation to ~/.zshrc" - fi -fi - echo "โœ… Development environment setup complete!" -echo "" -echo "๐Ÿ“ The virtual environment will be automatically activated in new terminals" -echo "" -echo "๐Ÿ”„ To activate in this terminal, run:" -echo " source ~/.bashrc" -echo "" -echo "๐Ÿš€ To start Superset:" -echo " start-superset" -echo "" +echo "๐Ÿš€ Run '.devcontainer/start-superset.sh' to start Superset" diff --git a/.devcontainer/start-superset.sh b/.devcontainer/start-superset.sh index 6ba990cae100..b480b04aacbc 100755 --- a/.devcontainer/start-superset.sh +++ b/.devcontainer/start-superset.sh @@ -1,14 +1,14 @@ #!/bin/bash # Startup script for Superset in Codespaces -# Log to a file for debugging -LOG_FILE="/tmp/superset-startup.log" -echo "[$(date)] Starting Superset startup script" >> "$LOG_FILE" -echo "[$(date)] User: $(whoami), PWD: $(pwd)" >> "$LOG_FILE" - echo "๐Ÿš€ Starting Superset in Codespaces..." echo "๐ŸŒ Frontend will be available at port 9001" +# Check if MCP is enabled +if [ "$ENABLE_MCP" = "true" ]; then + echo "๐Ÿค– MCP Service will be available at port 5008" +fi + # Find the workspace directory (Codespaces clones as 'superset', not 'superset-2') WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1) if [ -n "$WORKSPACE_DIR" ]; then @@ -18,71 +18,32 @@ else echo "๐Ÿ“ Using current directory: $(pwd)" fi -# Wait for Docker to be available -echo "โณ Waiting for Docker to start..." -echo "[$(date)] Waiting for Docker..." >> "$LOG_FILE" -max_attempts=30 -attempt=0 -while ! docker info > /dev/null 2>&1; do - if [ $attempt -eq $max_attempts ]; then - echo "โŒ Docker failed to start after $max_attempts attempts" - echo "[$(date)] Docker failed to start after $max_attempts attempts" >> "$LOG_FILE" - echo "๐Ÿ”„ Please restart the Codespace or run this script manually later" - exit 1 - fi - echo " Attempt $((attempt + 1))/$max_attempts..." - echo "[$(date)] Docker check attempt $((attempt + 1))/$max_attempts" >> "$LOG_FILE" - sleep 2 - attempt=$((attempt + 1)) -done -echo "โœ… Docker is ready!" -echo "[$(date)] Docker is ready" >> "$LOG_FILE" - -# Check if Superset containers are already running -if docker ps | grep -q "superset"; then - echo "โœ… Superset containers are already running!" - echo "" - echo "๐ŸŒ To access Superset:" - echo " 1. Click the 'Ports' tab at the bottom of VS Code" - echo " 2. Find port 9001 and click the globe icon to open" - echo " 3. Wait 10-20 minutes for initial startup" - echo "" - echo "๐Ÿ“ Login credentials: admin/admin" - exit 0 +# Check if docker is running +if ! docker info > /dev/null 2>&1; then + echo "โณ Waiting for Docker to start..." + sleep 5 fi # Clean up any existing containers echo "๐Ÿงน Cleaning up existing containers..." -docker-compose -f docker-compose-light.yml down +docker-compose -f docker-compose-light.yml --profile mcp down # Start services -echo "๐Ÿ—๏ธ Starting Superset in background (daemon mode)..." +echo "๐Ÿ—๏ธ Building and starting services..." echo "" - -# Start in detached mode -docker-compose -f docker-compose-light.yml up -d - -echo "" -echo "โœ… Docker Compose started successfully!" -echo "" -echo "๐Ÿ“‹ Important information:" -echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" -echo "โฑ๏ธ Initial startup takes 10-20 minutes" -echo "๐ŸŒ Check the 'Ports' tab for your Superset URL (port 9001)" -echo "๐Ÿ‘ค Login: admin / admin" +echo "๐Ÿ“ Once started, login with:" +echo " Username: admin" +echo " Password: admin" echo "" -echo "๐Ÿ“Š Useful commands:" -echo " docker-compose -f docker-compose-light.yml logs -f # Follow logs" -echo " docker-compose -f docker-compose-light.yml ps # Check status" -echo " docker-compose -f docker-compose-light.yml down # Stop services" -echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" -echo "" -echo "๐Ÿ’ค Keeping terminal open for 60 seconds to test persistence..." -sleep 60 -echo "โœ… Test complete - check if this terminal is still visible!" +echo "๐Ÿ“‹ Running in foreground with live logs (Ctrl+C to stop)..." -# Show final status -docker-compose -f docker-compose-light.yml ps +# Run docker-compose and capture exit code +if [ "$ENABLE_MCP" = "true" ]; then + echo "๐Ÿค– Starting with MCP Service enabled..." + docker-compose -f docker-compose-light.yml --profile mcp up +else + docker-compose -f docker-compose-light.yml up +fi EXIT_CODE=$? # If it failed, provide helpful instructions diff --git a/.devcontainer/with-mcp/devcontainer.json b/.devcontainer/with-mcp/devcontainer.json new file mode 100644 index 000000000000..c3f8b654ebca --- /dev/null +++ b/.devcontainer/with-mcp/devcontainer.json @@ -0,0 +1,29 @@ +{ + // Extend the base configuration + "extends": "../devcontainer-base.json", + + "name": "Apache Superset Development with MCP", + + // Forward ports for development + "forwardPorts": [9001, 5008], + "portsAttributes": { + "9001": { + "label": "Superset (via Webpack Dev Server)", + "onAutoForward": "notify", + "visibility": "public" + }, + "5008": { + "label": "MCP Service (Model Context Protocol)", + "onAutoForward": "notify", + "visibility": "private" + } + }, + + // Auto-start Superset with MCP on Codespace resume + "postStartCommand": "ENABLE_MCP=true .devcontainer/start-superset.sh", + + // Environment variables + "containerEnv": { + "ENABLE_MCP": "true" + } +} diff --git a/docker/docker-bootstrap.sh b/docker/docker-bootstrap.sh index 9d18b66626c0..d8524d928f89 100755 --- a/docker/docker-bootstrap.sh +++ b/docker/docker-bootstrap.sh @@ -86,6 +86,10 @@ case "${1}" in echo "Starting web app..." /usr/bin/run-server.sh ;; + mcp) + echo "Starting MCP service..." + superset mcp run --host 0.0.0.0 --port ${MCP_PORT:-5008} --debug + ;; *) echo "Unknown Operation!!!" ;; diff --git a/requirements/development.in b/requirements/development.in index 28a7862ae842..338775cceeb1 100644 --- a/requirements/development.in +++ b/requirements/development.in @@ -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] diff --git a/superset/cli/mcp.py b/superset/cli/mcp.py new file mode 100644 index 000000000000..6edf0b16c1ab --- /dev/null +++ b/superset/cli/mcp.py @@ -0,0 +1,46 @@ +# 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 +from flask.cli import with_appcontext + +from superset.mcp_service.scripts.setup import run_setup +from superset.mcp_service.server import run_server + + +@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""" + run_server(host=host, port=port, debug=debug) + + +@mcp.command() +@click.option("--force", is_flag=True, help="Force setup even if configuration exists") +@with_appcontext +def setup(force: bool) -> None: + """Set up MCP service for Apache Superset""" + run_setup(force) diff --git a/superset/config_mcp.py b/superset/config_mcp.py new file mode 100644 index 000000000000..14f6ebfb42a4 --- /dev/null +++ b/superset/config_mcp.py @@ -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. +""" +MCP (Model Context Protocol) Service Configuration. + +Copy these settings to your superset_config.py to enable MCP: + + FEATURE_FLAGS = { + **FEATURE_FLAGS, # Keep existing flags + "MCP_SERVICE": True, + } + MCP_SERVICE_HOST = "localhost" + MCP_SERVICE_PORT = 5008 +""" + +import os + +# Enable MCP service integration +FEATURE_FLAGS = { + "MCP_SERVICE": True, +} + +# MCP Service Connection +MCP_SERVICE_HOST = os.environ.get("MCP_SERVICE_HOST", "localhost") +MCP_SERVICE_PORT = int(os.environ.get("MCP_SERVICE_PORT", 5008)) + +# Optional: Adjust rate limits if needed (defaults work for most cases) +# MCP_RATE_LIMIT_REQUESTS = 100 # requests per window +# MCP_RATE_LIMIT_WINDOW_SECONDS = 60 # window size in seconds +# MCP_STREAMING_MAX_SIZE_MB = 10 # max response size diff --git a/superset/mcp_service/README.md b/superset/mcp_service/README.md new file mode 100644 index 000000000000..4fece2f697de --- /dev/null +++ b/superset/mcp_service/README.md @@ -0,0 +1,184 @@ +# 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. Initialize database +superset db upgrade +superset init + +# 5. Create admin user +superset fab create-admin \ + --username admin \ + --firstname Admin \ + --lastname Admin \ + --email admin@localhost \ + --password admin + +# 6. Start Superset (in one terminal) +superset run -p 9001 --with-threads --reload --debugger + +# 7. Start frontend (in another terminal) +cd superset-frontend && npm run dev + +# 8. Start MCP service (in another terminal, optional) +source venv/bin/activate +superset mcp setup +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/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 + +
+Direct STDIO with npx + +```json +{ + "mcpServers": { + "superset": { + "command": "npx", + "args": ["/absolute/path/to/your/superset/superset/mcp_service", "--stdio"], + "env": {} + } + } +} +``` +
+ +
+Direct STDIO with Python + +```json +{ + "mcpServers": { + "superset": { + "command": "/absolute/path/to/your/superset/venv/bin/python", + "args": ["-m", "superset.mcp_service"], + "env": { + "PYTHONPATH": "/absolute/path/to/your/superset" + } + } + } +} +``` +
+ +### ๐Ÿ“ Claude Desktop Config Location + +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` diff --git a/superset/mcp_service/__init__.py b/superset/mcp_service/__init__.py new file mode 100644 index 000000000000..545e99baa4ef --- /dev/null +++ b/superset/mcp_service/__init__.py @@ -0,0 +1,41 @@ +# 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" + +__all__ = [ + "__version__", +] diff --git a/superset/mcp_service/__main__.py b/superset/mcp_service/__main__.py new file mode 100644 index 000000000000..e5864843745b --- /dev/null +++ b/superset/mcp_service/__main__.py @@ -0,0 +1,136 @@ +# 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 +import warnings +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 + + # Suppress warnings + warnings.filterwarnings("ignore") + + # 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 os.environ.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() diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py new file mode 100644 index 000000000000..90599fe16586 --- /dev/null +++ b/superset/mcp_service/app.py @@ -0,0 +1,100 @@ +# 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 instance and initialization for Superset MCP service. +This file provides the global FastMCP instance (mcp) and a function to initialize +the server. All tool modules should import mcp from here and use @mcp.tool decorators. +""" + +import logging + +from fastmcp import FastMCP + +logger = logging.getLogger(__name__) + +# Create MCP instance without auth for scaffold +mcp = FastMCP( + "Superset MCP Server", + 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 init_fastmcp_server() -> FastMCP: + """ + Initialize and configure the FastMCP server. + This should be called before running the server. + """ + logger.setLevel(logging.DEBUG) + logger.info("MCP Server initialized - scaffold version without auth") + return mcp diff --git a/superset/mcp_service/bin/superset-mcp.js b/superset/mcp_service/bin/superset-mcp.js new file mode 100644 index 000000000000..bf191762b3bb --- /dev/null +++ b/superset/mcp_service/bin/superset-mcp.js @@ -0,0 +1,263 @@ +#!/usr/bin/env node + +/** + * 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(); diff --git a/superset/mcp_service/flask_singleton.py b/superset/mcp_service/flask_singleton.py new file mode 100644 index 000000000000..2296268dd067 --- /dev/null +++ b/superset/mcp_service/flask_singleton.py @@ -0,0 +1,78 @@ +""" +Singleton pattern for Flask app creation in MCP service. + +This module ensures that only one Flask app instance is created and reused +throughout the MCP service lifecycle. This prevents issues with multiple +app instances and improves performance. +""" + +import logging +import threading +from typing import Optional + +from flask import Flask + +logger = logging.getLogger(__name__) + +# Singleton instance storage +_flask_app: Optional[Flask] = None +_flask_app_lock = threading.Lock() + + +def get_flask_app() -> Flask: + """ + Get or create the singleton Flask app instance. + + This function ensures that only one Flask app is created, even when called + from multiple threads or contexts. The app is created lazily on first access. + + Returns: + Flask: The singleton Flask app instance + """ + global _flask_app + + # Fast path: if app already exists, return it + if _flask_app is not None: + return _flask_app + + # Slow path: acquire lock and create app if needed + with _flask_app_lock: + # Double-check pattern: verify app still doesn't exist after acquiring lock + if _flask_app is not None: + return _flask_app + + logger.info("Creating singleton Flask app instance for MCP service") + + try: + from superset.app import create_app + from superset.mcp_service.config import DEFAULT_CONFIG + + # Create the Flask app instance + _flask_app = create_app() + + # Apply MCP-specific defaults to app.config if not already set + for key, value in DEFAULT_CONFIG.items(): + if key not in _flask_app.config: + _flask_app.config[key] = value + + logger.info("Flask app singleton created successfully") + return _flask_app + + except Exception as e: + logger.error("Failed to create Flask app singleton: %s", e) + raise + + +def reset_flask_app() -> None: + """ + Reset the singleton Flask app instance. + + This should only be used in testing scenarios where you need to + recreate the app with different configurations. + """ + global _flask_app + + with _flask_app_lock: + if _flask_app is not None: + logger.info("Resetting Flask app singleton") + _flask_app = None diff --git a/superset/mcp_service/index.js b/superset/mcp_service/index.js new file mode 100644 index 000000000000..fcd040f2b085 --- /dev/null +++ b/superset/mcp_service/index.js @@ -0,0 +1,37 @@ +/** + * 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; diff --git a/superset/mcp_service/package.json b/superset/mcp_service/package.json new file mode 100644 index 000000000000..a394d61b7b92 --- /dev/null +++ b/superset/mcp_service/package.json @@ -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 +} diff --git a/superset/mcp_service/run_proxy.sh b/superset/mcp_service/run_proxy.sh new file mode 100644 index 000000000000..2ef411bc4f44 --- /dev/null +++ b/superset/mcp_service/run_proxy.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +## 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" diff --git a/superset/mcp_service/scripts/__init__.py b/superset/mcp_service/scripts/__init__.py new file mode 100644 index 000000000000..01d7d2ca0682 --- /dev/null +++ b/superset/mcp_service/scripts/__init__.py @@ -0,0 +1,17 @@ +# 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 service scripts""" diff --git a/superset/mcp_service/scripts/setup.py b/superset/mcp_service/scripts/setup.py new file mode 100644 index 000000000000..a58e3496b09a --- /dev/null +++ b/superset/mcp_service/scripts/setup.py @@ -0,0 +1,149 @@ +# 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. +"""Setup utilities for MCP service configuration""" + +import secrets +from pathlib import Path + +import click +from colorama import Fore, Style + + +def run_setup(force: bool) -> None: + """Set up MCP service configuration for Apache Superset""" + click.echo(f"{Fore.CYAN}=== Apache Superset MCP Service Setup ==={Style.RESET_ALL}") + click.echo() + + # Check if already set up + config_path = Path("superset_config.py") + + # Configuration file setup + if config_path.exists() and not force: + click.echo( + f"{Fore.YELLOW}โš ๏ธ superset_config.py already exists{Style.RESET_ALL}" + ) + if click.confirm("Do you want to check/add missing MCP settings?"): + _update_config_file(config_path) + else: + click.echo("Keeping existing configuration") + else: + _create_config_file(config_path) + + click.echo() + click.echo(f"{Fore.GREEN}=== Setup Complete! ==={Style.RESET_ALL}") + click.echo() + click.echo("To start MCP service:") + click.echo(" superset mcp run") + + +def _create_config_file(config_path: Path) -> None: + """Create a new superset_config.py file with MCP configuration""" + click.echo("Creating new superset_config.py...") + + config_content = f"""# Apache Superset Configuration +SECRET_KEY = '{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 + +# Feature flags +FEATURE_FLAGS = {{ + "MCP_SERVICE": True, +}} + +# MCP Service Host/Port +MCP_SERVICE_HOST = "localhost" +MCP_SERVICE_PORT = 5008 +""" + + config_path.write_text(config_content) + click.echo(f"{Fore.GREEN}โœ“ Created superset_config.py{Style.RESET_ALL}") + + +def _update_config_file(config_path: Path) -> None: + """Update existing config file with missing MCP settings""" + content = config_path.read_text() + updated = False + additions = [] + + # Check for missing settings + if "SECRET_KEY" not in content: + additions.append(f"SECRET_KEY = '{secrets.token_urlsafe(42)}'") + updated = True + + # Add MCP configuration block if missing + if "MCP_ADMIN_USERNAME" not in content: + additions.append("\n# MCP Service Configuration") + additions.append("MCP_ADMIN_USERNAME = 'admin'") + additions.append("MCP_DEV_USERNAME = 'admin'") + additions.append("SUPERSET_WEBSERVER_ADDRESS = 'http://localhost:9001'") + updated = True + + # Add WebDriver configuration if missing + if "WEBDRIVER_BASEURL" not in content: + additions.append("\n# WebDriver Configuration for screenshots") + additions.append("WEBDRIVER_BASEURL = 'http://localhost:9001/'") + additions.append("WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL") + updated = True + + # Add feature flags if missing + if "MCP_SERVICE" not in content: + # Check if FEATURE_FLAGS exists + if "FEATURE_FLAGS" in content: + # Need to update existing FEATURE_FLAGS + click.echo("Updating FEATURE_FLAGS to enable MCP_SERVICE...") + # This is more complex - would need careful regex replacement + # For now, just append a note + additions.append("\n# Enable MCP Service feature flag") + additions.append("# Add 'MCP_SERVICE': True to your FEATURE_FLAGS dict") + else: + additions.append("\n# Feature flags") + additions.append("FEATURE_FLAGS = {") + additions.append(' "MCP_SERVICE": True,') + additions.append("}") + updated = True + + # Add MCP host/port if missing + if "MCP_SERVICE_HOST" not in content: + additions.append("\n# MCP Service Host/Port") + additions.append('MCP_SERVICE_HOST = "localhost"') + additions.append("MCP_SERVICE_PORT = 5008") + updated = True + + if updated: + # Append all additions to the file + if additions: + content += "\n" + "\n".join(additions) + "\n" + config_path.write_text(content) + click.echo(f"{Fore.GREEN}โœ“ Configuration updated{Style.RESET_ALL}") diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py new file mode 100644 index 000000000000..7fe96dbad947 --- /dev/null +++ b/superset/mcp_service/server.py @@ -0,0 +1,74 @@ +# 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 + +# Apply Flask-AppBuilder compatibility patches before any Superset imports +from superset.mcp_service.app import init_fastmcp_server, mcp + + +def configure_logging(debug: bool = False) -> None: + """Configure logging for the MCP service.""" + import sys + + if debug or os.environ.get("SQLALCHEMY_DEBUG"): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + stream=sys.stderr, # Always log to stderr, not stdout + ) + for logger_name in [ + "sqlalchemy.engine", + "sqlalchemy.pool", + "sqlalchemy.dialects", + ]: + logging.getLogger(logger_name).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) -> None: + """ + Run the MCP service server with FastMCP endpoints. + Uses streamable-http transport for HTTP server mode. + """ + + configure_logging(debug) + # Use logging to stderr instead of print to stdout + logging.info("Creating MCP app...") + init_fastmcp_server() # This will register middleware, etc. + + 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.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() diff --git a/superset/mcp_service/simple_proxy.py b/superset/mcp_service/simple_proxy.py new file mode 100644 index 000000000000..f6d33af0f0c0 --- /dev/null +++ b/superset/mcp_service/simple_proxy.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Simple MCP proxy server that connects to FastMCP server on localhost:5008 +""" + +import logging +import signal +import sys +import time +from typing import Any, Optional + +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: Optional[FastMCP] = None + + +def signal_handler(signum: int, frame: Any) -> None: + """Handle shutdown signals gracefully""" + logger.info("Received signal %s, shutting down gracefully...", signum) + if proxy: + try: + # Give the proxy a moment to clean up + time.sleep(0.1) + except Exception as e: + logger.warning("Error during proxy cleanup: %s", e) + 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() diff --git a/superset/mcp_service/utils/__init__.py b/superset/mcp_service/utils/__init__.py new file mode 100644 index 000000000000..6f5e5059039c --- /dev/null +++ b/superset/mcp_service/utils/__init__.py @@ -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. + +"""MCP service utility modules.""" From 2092da85ea03b1ef7290e8e94e512d3cad61e197 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 13:55:27 -0400 Subject: [PATCH 02/16] refactor(mcp): improve configuration management and setup - Add mcp_config.py with centralized MCP configuration - Refactor setup.py to use import-based config instead of hardcoded strings - Update README with manual setup instructions following standard Superset patterns - Replace hidden MCP_DEBUG env var with proper app.config setting - Remove opinionated warnings filter to let logger.ini handle warnings - Make configuration more maintainable and discoverable --- superset/mcp_service/README.md | 62 +++++++++++++++-- superset/mcp_service/__main__.py | 6 +- superset/mcp_service/bin/superset-mcp.js | 0 superset/mcp_service/mcp_config.py | 86 ++++++++++++++++++++++++ superset/mcp_service/scripts/setup.py | 73 ++++++-------------- 5 files changed, 162 insertions(+), 65 deletions(-) mode change 100644 => 100755 superset/mcp_service/bin/superset-mcp.js create mode 100644 superset/mcp_service/mcp_config.py diff --git a/superset/mcp_service/README.md b/superset/mcp_service/README.md index 4fece2f697de..8a0b7fbe3ba6 100644 --- a/superset/mcp_service/README.md +++ b/superset/mcp_service/README.md @@ -73,11 +73,51 @@ source venv/bin/activate pip install -e .[development,fastmcp] cd superset-frontend && npm ci && npm run build && cd .. -# 4. Initialize database +# 4. Configure Superset manually +# Create superset_config.py in your current directory: +cat > superset_config.py << 'EOF' +# Apache Superset Configuration +SECRET_KEY = '' + +# 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 + +import os + +# Enable MCP service integration +FEATURE_FLAGS = { + "MCP_SERVICE": True, +} + +# MCP Service Connection +MCP_SERVICE_HOST = os.environ.get("MCP_SERVICE_HOST", "localhost") +MCP_SERVICE_PORT = int(os.environ.get("MCP_SERVICE_PORT", 5008)) + +EOF + +# 5. Initialize database +export FLASK_APP=superset superset db upgrade superset init -# 5. Create admin user +# 6. Create admin user superset fab create-admin \ --username admin \ --firstname Admin \ @@ -85,20 +125,30 @@ superset fab create-admin \ --email admin@localhost \ --password admin -# 6. Start Superset (in one terminal) +# 7. Start Superset (in one terminal) superset run -p 9001 --with-threads --reload --debugger -# 7. Start frontend (in another terminal) +# 8. Start frontend (in another terminal) cd superset-frontend && npm run dev -# 8. Start MCP service (in another terminal, optional) +# 9. Start MCP service (in another terminal, only if you want MCP features) source venv/bin/activate -superset mcp setup superset mcp run --port 5008 --debug ``` Access Superset at http://localhost:9001 (login: admin/admin) +### Automated MCP Setup (Optional) + +If you prefer, you can use the automated setup script instead of manually creating `superset_config.py`: + +```bash +# After step 3 above, run this instead of creating the config manually: +superset mcp setup + +# Then continue with steps 5-9 +``` + ## ๐Ÿ”Œ Step 2: Connect Claude Desktop ### For Docker Setup diff --git a/superset/mcp_service/__main__.py b/superset/mcp_service/__main__.py index e5864843745b..cda19c9eaaf8 100644 --- a/superset/mcp_service/__main__.py +++ b/superset/mcp_service/__main__.py @@ -25,7 +25,6 @@ import logging import os import sys -import warnings from typing import Any # Must redirect click output BEFORE importing anything that uses it @@ -86,9 +85,6 @@ def main() -> None: new_handlers.append(h) logger.handlers = new_handlers - # Suppress warnings - warnings.filterwarnings("ignore") - # Capture any print statements during initialization captured_output = io.StringIO() @@ -104,7 +100,7 @@ def main() -> None: # Log captured output to stderr for debugging (optional) captured = captured_output.getvalue() - if captured and os.environ.get("MCP_DEBUG"): + 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 diff --git a/superset/mcp_service/bin/superset-mcp.js b/superset/mcp_service/bin/superset-mcp.js old mode 100644 new mode 100755 diff --git a/superset/mcp_service/mcp_config.py b/superset/mcp_service/mcp_config.py new file mode 100644 index 000000000000..689b91c0cbe9 --- /dev/null +++ b/superset/mcp_service/mcp_config.py @@ -0,0 +1,86 @@ +# 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""" + +import secrets +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, +} + + +def generate_secret_key() -> str: + """Generate a secure random secret key for Superset""" + return secrets.token_urlsafe(42) + + +def get_mcp_config() -> Dict[str, Any]: + """Get complete MCP configuration dictionary""" + config = {} + + # Add MCP-specific settings + config.update( + { + "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, + } + ) + + # Add session and CSRF config + config.update(MCP_SESSION_CONFIG) + config.update(MCP_CSRF_CONFIG) + + return config diff --git a/superset/mcp_service/scripts/setup.py b/superset/mcp_service/scripts/setup.py index a58e3496b09a..826ecee49727 100644 --- a/superset/mcp_service/scripts/setup.py +++ b/superset/mcp_service/scripts/setup.py @@ -16,12 +16,13 @@ # under the License. """Setup utilities for MCP service configuration""" -import secrets from pathlib import Path import click from colorama import Fore, Style +from superset.mcp_service.mcp_config import generate_secret_key + def run_setup(force: bool) -> None: """Set up MCP service configuration for Apache Superset""" @@ -55,36 +56,16 @@ def _create_config_file(config_path: Path) -> None: click.echo("Creating new superset_config.py...") config_content = f"""# Apache Superset Configuration -SECRET_KEY = '{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 +SECRET_KEY = '{generate_secret_key()}' -# CSRF Protection (disable if login loop occurs) -WTF_CSRF_ENABLED = True -WTF_CSRF_TIME_LIMIT = None +# Import MCP configuration +from superset.mcp_service.mcp_config import get_mcp_config, MCP_FEATURE_FLAGS -# 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 +# Apply MCP configuration +locals().update(get_mcp_config()) # Feature flags -FEATURE_FLAGS = {{ - "MCP_SERVICE": True, -}} - -# MCP Service Host/Port -MCP_SERVICE_HOST = "localhost" -MCP_SERVICE_PORT = 5008 +FEATURE_FLAGS = MCP_FEATURE_FLAGS.copy() """ config_path.write_text(config_content) @@ -99,22 +80,17 @@ def _update_config_file(config_path: Path) -> None: # Check for missing settings if "SECRET_KEY" not in content: - additions.append(f"SECRET_KEY = '{secrets.token_urlsafe(42)}'") + additions.append(f"SECRET_KEY = '{generate_secret_key()}'") updated = True - # Add MCP configuration block if missing - if "MCP_ADMIN_USERNAME" not in content: - additions.append("\n# MCP Service Configuration") - additions.append("MCP_ADMIN_USERNAME = 'admin'") - additions.append("MCP_DEV_USERNAME = 'admin'") - additions.append("SUPERSET_WEBSERVER_ADDRESS = 'http://localhost:9001'") - updated = True - - # Add WebDriver configuration if missing - if "WEBDRIVER_BASEURL" not in content: - additions.append("\n# WebDriver Configuration for screenshots") - additions.append("WEBDRIVER_BASEURL = 'http://localhost:9001/'") - additions.append("WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL") + # Add MCP configuration import if missing + if "from superset.mcp_service.mcp_config import" not in content: + additions.append("\n# Import MCP configuration") + additions.append("from superset.mcp_service.mcp_config import (") + additions.append(" get_mcp_config, MCP_FEATURE_FLAGS") + additions.append(")") + additions.append("\n# Apply MCP configuration") + additions.append("locals().update(get_mcp_config())") updated = True # Add feature flags if missing @@ -123,22 +99,11 @@ def _update_config_file(config_path: Path) -> None: if "FEATURE_FLAGS" in content: # Need to update existing FEATURE_FLAGS click.echo("Updating FEATURE_FLAGS to enable MCP_SERVICE...") - # This is more complex - would need careful regex replacement - # For now, just append a note additions.append("\n# Enable MCP Service feature flag") - additions.append("# Add 'MCP_SERVICE': True to your FEATURE_FLAGS dict") + additions.append("FEATURE_FLAGS.update(MCP_FEATURE_FLAGS)") else: additions.append("\n# Feature flags") - additions.append("FEATURE_FLAGS = {") - additions.append(' "MCP_SERVICE": True,') - additions.append("}") - updated = True - - # Add MCP host/port if missing - if "MCP_SERVICE_HOST" not in content: - additions.append("\n# MCP Service Host/Port") - additions.append('MCP_SERVICE_HOST = "localhost"') - additions.append("MCP_SERVICE_PORT = 5008") + additions.append("FEATURE_FLAGS = MCP_FEATURE_FLAGS.copy()") updated = True if updated: From d637b8658b2dfc2f75a50763e088619289390490 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 14:53:01 -0400 Subject: [PATCH 03/16] feat(mcp): add configurable FastMCP factory pattern Add Flask-style application factory for FastMCP instances with support for: - Custom authentication providers via auth parameter - Middleware and lifespan handlers - Tag-based tool filtering (include_tags/exclude_tags) - Additional configuration options - Backward compatibility with existing mcp instance Examples provided in superset/mcp_service/examples/ showing usage patterns for secure, filtered, and custom configurations. --- superset/mcp_service/app.py | 192 ++++++++++++++++-- superset/mcp_service/examples/__init__.py | 18 ++ .../examples/custom_mcp_factory.py | 150 ++++++++++++++ superset/mcp_service/mcp_config.py | 25 +++ superset/mcp_service/server.py | 31 ++- 5 files changed, 396 insertions(+), 20 deletions(-) create mode 100644 superset/mcp_service/examples/__init__.py create mode 100644 superset/mcp_service/examples/custom_mcp_factory.py diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py index 90599fe16586..a0ee2f884532 100644 --- a/superset/mcp_service/app.py +++ b/superset/mcp_service/app.py @@ -16,21 +16,21 @@ # under the License. """ -FastMCP app instance and initialization for Superset MCP service. -This file provides the global FastMCP instance (mcp) and a function to initialize -the server. All tool modules should import mcp from here and use @mcp.tool decorators. +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, Optional, Set from fastmcp import FastMCP logger = logging.getLogger(__name__) -# Create MCP instance without auth for scaffold -mcp = FastMCP( - "Superset MCP Server", - instructions=""" +# 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. @@ -86,15 +86,179 @@ 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: Optional[Any], + lifespan: Optional[Callable[..., Any]], + tools: Optional[List[Any]], + include_tags: Optional[Set[str]], + exclude_tags: Optional[Set[str]], + **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: Optional[Dict[str, Any]]) -> None: + """Apply additional configuration to FastMCP instance.""" + if config: + for key, value in config.items(): + setattr(mcp_instance, key, value) -def init_fastmcp_server() -> FastMCP: +def _log_instance_creation( + name: str, + auth: Optional[Any], + include_tags: Optional[Set[str]], + exclude_tags: Optional[Set[str]], +) -> 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: Optional[str] = None, + auth: Optional[Any] = None, + lifespan: Optional[Callable[..., Any]] = None, + tools: Optional[List[Any]] = None, + include_tags: Optional[Set[str]] = None, + exclude_tags: Optional[Set[str]] = None, + config: Optional[Dict[str, Any]] = 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() + + +def init_fastmcp_server( + name: str = "Superset MCP Server", + instructions: Optional[str] = None, + auth: Optional[Any] = None, + lifespan: Optional[Callable[..., Any]] = None, + tools: Optional[List[Any]] = None, + include_tags: Optional[Set[str]] = None, + exclude_tags: Optional[Set[str]] = None, + config: Optional[Dict[str, Any]] = None, + **kwargs: Any, +) -> FastMCP: """ Initialize and configure the FastMCP server. - This should be called before running the 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) """ - logger.setLevel(logging.DEBUG) - logger.info("MCP Server initialized - scaffold version without auth") - return mcp + # 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 diff --git a/superset/mcp_service/examples/__init__.py b/superset/mcp_service/examples/__init__.py new file mode 100644 index 000000000000..e5f94e30a6f6 --- /dev/null +++ b/superset/mcp_service/examples/__init__.py @@ -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. + +"""Examples for customizing the Superset MCP service.""" diff --git a/superset/mcp_service/examples/custom_mcp_factory.py b/superset/mcp_service/examples/custom_mcp_factory.py new file mode 100644 index 000000000000..4d3219d85602 --- /dev/null +++ b/superset/mcp_service/examples/custom_mcp_factory.py @@ -0,0 +1,150 @@ +# 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. + +""" +Example of how to customize the FastMCP factory with authentication, +middleware, and custom configuration. + +This demonstrates the Flask-style application factory pattern for +creating customized FastMCP instances. +""" + +import asyncio +import logging +from typing import Any + +from superset.mcp_service.app import create_mcp_app +from superset.mcp_service.mcp_config import get_mcp_factory_config + + +class SimpleAuthProvider: + """Example auth provider for demonstration purposes.""" + + def __init__(self, api_key: str): + self.api_key = api_key + + async def authenticate(self, request: Any) -> bool: + """Simple API key authentication.""" + auth_header = getattr(request, "headers", {}).get("Authorization", "") + return auth_header == f"Bearer {self.api_key}" + + +async def custom_lifespan(app: Any) -> Any: + """Custom lifespan handler for startup/shutdown logic.""" + logging.info("๐Ÿš€ Custom FastMCP server starting up...") + + # Startup logic here + yield + + # Shutdown logic here + logging.info("๐Ÿ›‘ Custom FastMCP server shutting down...") + + +def create_secure_mcp_app() -> Any: + """ + Example: Create a secure FastMCP instance with authentication. + """ + auth_provider = SimpleAuthProvider(api_key="your-secret-api-key") + + return create_mcp_app( + name="Secure Superset MCP", + auth=auth_provider, + lifespan=custom_lifespan, + include_tags={"public", "dashboard", "chart"}, # Only expose certain tools + config={"debug": True, "timeout": 30}, + ) + + +def create_tagged_mcp_app() -> Any: + """ + Example: Create FastMCP instance with tag filtering. + """ + return create_mcp_app( + name="Filtered Superset MCP", + include_tags={"dashboard", "chart"}, # Only dashboard and chart tools + exclude_tags={"admin"}, # Exclude admin tools + config={"read_only": True}, + ) + + +def create_custom_instructions_mcp_app() -> Any: + """ + Example: Create FastMCP instance with custom instructions. + """ + custom_instructions = """ + Welcome to the Custom Superset MCP Service! + + This is a specialized instance for dashboard and chart management. + Available tools are limited to read-only operations for security. + + Please use the dashboard and chart management tools to explore data. + """ + + return create_mcp_app( + name="Custom Instructions MCP", + instructions=custom_instructions, + include_tags={"dashboard", "chart"}, + exclude_tags={"sql", "admin"}, + ) + + +def create_mcp_from_config() -> Any: + """ + Example: Create FastMCP instance from configuration file. + """ + # Get base configuration + factory_config = get_mcp_factory_config() + + # Customize the configuration + factory_config.update( + { + "name": "Configured Superset MCP", + "auth": SimpleAuthProvider("config-api-key"), + "include_tags": {"public"}, + "config": {"environment": "production"}, + } + ) + + return create_mcp_app(**factory_config) + + +async def main() -> None: + """Example of running different MCP configurations.""" + + # Example 1: Secure MCP with authentication + secure_mcp = create_secure_mcp_app() + print(f"Created secure MCP: {secure_mcp.name}") + + # Example 2: Tagged/filtered MCP + filtered_mcp = create_tagged_mcp_app() + print(f"Created filtered MCP: {filtered_mcp.name}") + + # Example 3: Custom instructions MCP + custom_mcp = create_custom_instructions_mcp_app() + print(f"Created custom MCP: {custom_mcp.name}") + + # Example 4: Configuration-driven MCP + config_mcp = create_mcp_from_config() + print(f"Created config-driven MCP: {config_mcp.name}") + + +if __name__ == "__main__": + # Configure logging + logging.basicConfig(level=logging.INFO) + + # Run examples + asyncio.run(main()) diff --git a/superset/mcp_service/mcp_config.py b/superset/mcp_service/mcp_config.py index 689b91c0cbe9..545f6c21314d 100644 --- a/superset/mcp_service/mcp_config.py +++ b/superset/mcp_service/mcp_config.py @@ -55,6 +55,18 @@ "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 generate_secret_key() -> str: """Generate a secure random secret key for Superset""" @@ -84,3 +96,16 @@ def get_mcp_config() -> Dict[str, Any]: config.update(MCP_CSRF_CONFIG) return 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() diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py index 7fe96dbad947..47f370e46df9 100644 --- a/superset/mcp_service/server.py +++ b/superset/mcp_service/server.py @@ -23,7 +23,8 @@ import os # Apply Flask-AppBuilder compatibility patches before any Superset imports -from superset.mcp_service.app import init_fastmcp_server, mcp +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: @@ -46,23 +47,41 @@ def configure_logging(debug: bool = False) -> None: logging.info("๐Ÿ” SQL Debug logging enabled") -def run_server(host: str = "127.0.0.1", port: int = 5008, debug: bool = False) -> None: +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) - # Use logging to stderr instead of print to stdout - logging.info("Creating MCP app...") - init_fastmcp_server() # This will register middleware, etc. + + 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.run(transport="streamable-http", host=host, port=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) From f22850ae160df55e904ee1deb98ee7d48d9fe433 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 15:15:35 -0400 Subject: [PATCH 04/16] fix(mcp): correct invalid import in flask_singleton Fix invalid import from non-existent superset.mcp_service.config module to use get_mcp_config() from superset.mcp_service.mcp_config instead. --- superset/mcp_service/flask_singleton.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/superset/mcp_service/flask_singleton.py b/superset/mcp_service/flask_singleton.py index 2296268dd067..f846afaed1f5 100644 --- a/superset/mcp_service/flask_singleton.py +++ b/superset/mcp_service/flask_singleton.py @@ -45,13 +45,14 @@ def get_flask_app() -> Flask: try: from superset.app import create_app - from superset.mcp_service.config import DEFAULT_CONFIG + from superset.mcp_service.mcp_config import get_mcp_config # Create the Flask app instance _flask_app = create_app() # Apply MCP-specific defaults to app.config if not already set - for key, value in DEFAULT_CONFIG.items(): + mcp_config = get_mcp_config() + for key, value in mcp_config.items(): if key not in _flask_app.config: _flask_app.config[key] = value From 87ae565498f5fb47274fdafafcbb05e2081fe61b Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 16:10:22 -0400 Subject: [PATCH 05/16] feat(mcp): add simple health check tool for testing Add health_check tool in system/tool/ that returns: - Service status and timestamp - System information (Python version, platform) - Basic uptime information Tool is automatically registered via import in __init__.py for easy connectivity testing of the MCP service. --- superset/mcp_service/__init__.py | 6 ++ superset/mcp_service/system/tool/__init__.py | 18 ++++ .../mcp_service/system/tool/health_check.py | 84 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 superset/mcp_service/system/tool/__init__.py create mode 100644 superset/mcp_service/system/tool/health_check.py diff --git a/superset/mcp_service/__init__.py b/superset/mcp_service/__init__.py index 545e99baa4ef..1a5cba5057b4 100644 --- a/superset/mcp_service/__init__.py +++ b/superset/mcp_service/__init__.py @@ -36,6 +36,12 @@ __version__ = "1.0.0" +# Import tools to register them with the MCP service +try: + from superset.mcp_service.system.tool import health_check # noqa: F401 +except ImportError: + pass # Tool import is optional + __all__ = [ "__version__", ] diff --git a/superset/mcp_service/system/tool/__init__.py b/superset/mcp_service/system/tool/__init__.py new file mode 100644 index 000000000000..ee6ed6ae14bb --- /dev/null +++ b/superset/mcp_service/system/tool/__init__.py @@ -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.""" diff --git a/superset/mcp_service/system/tool/health_check.py b/superset/mcp_service/system/tool/health_check.py new file mode 100644 index 000000000000..890aafa7225e --- /dev/null +++ b/superset/mcp_service/system/tool/health_check.py @@ -0,0 +1,84 @@ +# 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 + +from pydantic import BaseModel + +from superset.mcp_service.app import mcp + +logger = logging.getLogger(__name__) + + +class HealthCheckResponse(BaseModel): + """Response model for health check.""" + + status: str + timestamp: str + service: str + version: str + python_version: str + platform: str + uptime_seconds: float + + +@mcp.tool() +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: + import time + + # Get basic system information + now = datetime.datetime.now() + + response = HealthCheckResponse( + status="healthy", + timestamp=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, + ) From 7317fd3118c58664147a2faac14650a862e10d5f Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 16:12:34 -0400 Subject: [PATCH 06/16] refactor(mcp): simplify flask singleton and minor improvements - Replace complex threading singleton with simple module-level pattern - Clean up server.py imports and formatting - Update README.md with better configuration examples --- superset/mcp_service/README.md | 5 +- superset/mcp_service/flask_singleton.py | 85 ++++++++----------------- superset/mcp_service/server.py | 1 - 3 files changed, 30 insertions(+), 61 deletions(-) diff --git a/superset/mcp_service/README.md b/superset/mcp_service/README.md index 8a0b7fbe3ba6..c6108add65f6 100644 --- a/superset/mcp_service/README.md +++ b/superset/mcp_service/README.md @@ -204,7 +204,10 @@ Then restart Claude Desktop. That's it! โœจ "superset": { "command": "npx", "args": ["/absolute/path/to/your/superset/superset/mcp_service", "--stdio"], - "env": {} + "env": { + "PYTHONPATH": "/absolute/path/to/your/superset", + "MCP_ADMIN_USERNAME": "admin" + } } } } diff --git a/superset/mcp_service/flask_singleton.py b/superset/mcp_service/flask_singleton.py index f846afaed1f5..956d56f93987 100644 --- a/superset/mcp_service/flask_singleton.py +++ b/superset/mcp_service/flask_singleton.py @@ -1,79 +1,46 @@ """ -Singleton pattern for Flask app creation in MCP service. +Simple module-level Flask app instance for MCP service. -This module ensures that only one Flask app instance is created and reused -throughout the MCP service lifecycle. This prevents issues with multiple -app instances and improves performance. +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 -import threading -from typing import Optional from flask import Flask logger = logging.getLogger(__name__) -# Singleton instance storage -_flask_app: Optional[Flask] = None -_flask_app_lock = threading.Lock() +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 -def get_flask_app() -> Flask: - """ - Get or create the singleton Flask app instance. - - This function ensures that only one Flask app is created, even when called - from multiple threads or contexts. The app is created lazily on first access. - - Returns: - Flask: The singleton Flask app instance - """ - global _flask_app - - # Fast path: if app already exists, return it - if _flask_app is not None: - return _flask_app + # Create the Flask app instance - this is the singleton + app = create_app() - # Slow path: acquire lock and create app if needed - with _flask_app_lock: - # Double-check pattern: verify app still doesn't exist after acquiring lock - if _flask_app is not None: - return _flask_app + # Apply MCP-specific defaults to app.config if not already set + mcp_config = get_mcp_config() + for key, value in mcp_config.items(): + if key not in app.config: + app.config[key] = value - logger.info("Creating singleton Flask app instance for MCP service") + logger.info("Flask app instance created successfully") - try: - from superset.app import create_app - from superset.mcp_service.mcp_config import get_mcp_config +except Exception as e: + logger.error("Failed to create Flask app: %s", e) + raise - # Create the Flask app instance - _flask_app = create_app() - # Apply MCP-specific defaults to app.config if not already set - mcp_config = get_mcp_config() - for key, value in mcp_config.items(): - if key not in _flask_app.config: - _flask_app.config[key] = value - - logger.info("Flask app singleton created successfully") - return _flask_app - - except Exception as e: - logger.error("Failed to create Flask app singleton: %s", e) - raise - - -def reset_flask_app() -> None: +def get_flask_app() -> Flask: """ - Reset the singleton Flask app instance. + Get the Flask app instance. - This should only be used in testing scenarios where you need to - recreate the app with different configurations. + Returns: + Flask: The module-level Flask app instance """ - global _flask_app - - with _flask_app_lock: - if _flask_app is not None: - logger.info("Resetting Flask app singleton") - _flask_app = None + return app diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py index 47f370e46df9..0d910022109e 100644 --- a/superset/mcp_service/server.py +++ b/superset/mcp_service/server.py @@ -22,7 +22,6 @@ import logging import os -# Apply Flask-AppBuilder compatibility patches before any Superset imports from superset.mcp_service.app import create_mcp_app, init_fastmcp_server from superset.mcp_service.mcp_config import get_mcp_factory_config From 442439747c52f6848fd2ce0c50ec0132fda32d29 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 16:26:26 -0400 Subject: [PATCH 07/16] remove(mcp): delete scripts directory and setup command Remove mcp_service/scripts/ directory and superset mcp setup command to simplify the MCP service implementation. --- superset/cli/mcp.py | 10 -- superset/mcp_service/scripts/__init__.py | 17 ---- superset/mcp_service/scripts/setup.py | 114 ----------------------- 3 files changed, 141 deletions(-) delete mode 100644 superset/mcp_service/scripts/__init__.py delete mode 100644 superset/mcp_service/scripts/setup.py diff --git a/superset/cli/mcp.py b/superset/cli/mcp.py index 6edf0b16c1ab..cfd81952bc4c 100644 --- a/superset/cli/mcp.py +++ b/superset/cli/mcp.py @@ -17,9 +17,7 @@ """CLI module for MCP service""" import click -from flask.cli import with_appcontext -from superset.mcp_service.scripts.setup import run_setup from superset.mcp_service.server import run_server @@ -36,11 +34,3 @@ def mcp() -> None: def run(host: str, port: int, debug: bool) -> None: """Run the MCP service""" run_server(host=host, port=port, debug=debug) - - -@mcp.command() -@click.option("--force", is_flag=True, help="Force setup even if configuration exists") -@with_appcontext -def setup(force: bool) -> None: - """Set up MCP service for Apache Superset""" - run_setup(force) diff --git a/superset/mcp_service/scripts/__init__.py b/superset/mcp_service/scripts/__init__.py deleted file mode 100644 index 01d7d2ca0682..000000000000 --- a/superset/mcp_service/scripts/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 service scripts""" diff --git a/superset/mcp_service/scripts/setup.py b/superset/mcp_service/scripts/setup.py deleted file mode 100644 index 826ecee49727..000000000000 --- a/superset/mcp_service/scripts/setup.py +++ /dev/null @@ -1,114 +0,0 @@ -# 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. -"""Setup utilities for MCP service configuration""" - -from pathlib import Path - -import click -from colorama import Fore, Style - -from superset.mcp_service.mcp_config import generate_secret_key - - -def run_setup(force: bool) -> None: - """Set up MCP service configuration for Apache Superset""" - click.echo(f"{Fore.CYAN}=== Apache Superset MCP Service Setup ==={Style.RESET_ALL}") - click.echo() - - # Check if already set up - config_path = Path("superset_config.py") - - # Configuration file setup - if config_path.exists() and not force: - click.echo( - f"{Fore.YELLOW}โš ๏ธ superset_config.py already exists{Style.RESET_ALL}" - ) - if click.confirm("Do you want to check/add missing MCP settings?"): - _update_config_file(config_path) - else: - click.echo("Keeping existing configuration") - else: - _create_config_file(config_path) - - click.echo() - click.echo(f"{Fore.GREEN}=== Setup Complete! ==={Style.RESET_ALL}") - click.echo() - click.echo("To start MCP service:") - click.echo(" superset mcp run") - - -def _create_config_file(config_path: Path) -> None: - """Create a new superset_config.py file with MCP configuration""" - click.echo("Creating new superset_config.py...") - - config_content = f"""# Apache Superset Configuration -SECRET_KEY = '{generate_secret_key()}' - -# Import MCP configuration -from superset.mcp_service.mcp_config import get_mcp_config, MCP_FEATURE_FLAGS - -# Apply MCP configuration -locals().update(get_mcp_config()) - -# Feature flags -FEATURE_FLAGS = MCP_FEATURE_FLAGS.copy() -""" - - config_path.write_text(config_content) - click.echo(f"{Fore.GREEN}โœ“ Created superset_config.py{Style.RESET_ALL}") - - -def _update_config_file(config_path: Path) -> None: - """Update existing config file with missing MCP settings""" - content = config_path.read_text() - updated = False - additions = [] - - # Check for missing settings - if "SECRET_KEY" not in content: - additions.append(f"SECRET_KEY = '{generate_secret_key()}'") - updated = True - - # Add MCP configuration import if missing - if "from superset.mcp_service.mcp_config import" not in content: - additions.append("\n# Import MCP configuration") - additions.append("from superset.mcp_service.mcp_config import (") - additions.append(" get_mcp_config, MCP_FEATURE_FLAGS") - additions.append(")") - additions.append("\n# Apply MCP configuration") - additions.append("locals().update(get_mcp_config())") - updated = True - - # Add feature flags if missing - if "MCP_SERVICE" not in content: - # Check if FEATURE_FLAGS exists - if "FEATURE_FLAGS" in content: - # Need to update existing FEATURE_FLAGS - click.echo("Updating FEATURE_FLAGS to enable MCP_SERVICE...") - additions.append("\n# Enable MCP Service feature flag") - additions.append("FEATURE_FLAGS.update(MCP_FEATURE_FLAGS)") - else: - additions.append("\n# Feature flags") - additions.append("FEATURE_FLAGS = MCP_FEATURE_FLAGS.copy()") - updated = True - - if updated: - # Append all additions to the file - if additions: - content += "\n" + "\n".join(additions) + "\n" - config_path.write_text(content) - click.echo(f"{Fore.GREEN}โœ“ Configuration updated{Style.RESET_ALL}") From fe154005c011f832a6c4f5e3a474e9820bf55bfa Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 19 Sep 2025 16:30:11 -0400 Subject: [PATCH 08/16] fix(mcp): respect existing logging configuration - Only configure basic logging if no handlers exist (respects logging.ini) - Only override SQLAlchemy logger levels if they're at default levels - Prevents interference with production logging configuration --- superset/mcp_service/server.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py index 0d910022109e..2fcb39722d3d 100644 --- a/superset/mcp_service/server.py +++ b/superset/mcp_service/server.py @@ -31,17 +31,26 @@ def configure_logging(debug: bool = False) -> None: import sys if debug or os.environ.get("SQLALCHEMY_DEBUG"): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - stream=sys.stderr, # Always log to stderr, not stdout - ) + # 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", ]: - logging.getLogger(logger_name).setLevel(logging.INFO) + 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") From 51325adfaf8e150d5e606a407f53e86610a09a28 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 15:55:17 -0400 Subject: [PATCH 09/16] fix(mcp): remove blocking sleep from signal handler Remove time.sleep(0.1) from signal handler in simple_proxy.py as it's not a maintainable solution for cleanup. FastMCP proxy should handle its own cleanup when process exits. --- superset/mcp_service/simple_proxy.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/superset/mcp_service/simple_proxy.py b/superset/mcp_service/simple_proxy.py index f6d33af0f0c0..50639897fa99 100644 --- a/superset/mcp_service/simple_proxy.py +++ b/superset/mcp_service/simple_proxy.py @@ -6,7 +6,6 @@ import logging import signal import sys -import time from typing import Any, Optional from fastmcp import FastMCP @@ -24,12 +23,7 @@ def signal_handler(signum: int, frame: Any) -> None: """Handle shutdown signals gracefully""" logger.info("Received signal %s, shutting down gracefully...", signum) - if proxy: - try: - # Give the proxy a moment to clean up - time.sleep(0.1) - except Exception as e: - logger.warning("Error during proxy cleanup: %s", e) + # FastMCP.as_proxy() handles its own cleanup sys.exit(0) From 6354d0bab6d891eb42c7091eec89ae27a39e827f Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 16:04:54 -0400 Subject: [PATCH 10/16] feat(mcp): add Apache license headers to MCP service files Add required Apache Software Foundation license headers to: - superset/mcp_service/flask_singleton.py - superset/mcp_service/simple_proxy.py - superset/mcp_service/run_proxy.sh - superset/mcp_service/index.js - superset/mcp_service/bin/superset-mcp.js Fixes license check violations for MCP service scaffold. --- superset/mcp_service/bin/superset-mcp.js | 19 +++++++++++++++++++ superset/mcp_service/flask_singleton.py | 16 ++++++++++++++++ superset/mcp_service/index.js | 19 +++++++++++++++++++ superset/mcp_service/run_proxy.sh | 16 ++++++++++++++++ superset/mcp_service/simple_proxy.py | 16 ++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/superset/mcp_service/bin/superset-mcp.js b/superset/mcp_service/bin/superset-mcp.js index bf191762b3bb..b9eb7c687805 100755 --- a/superset/mcp_service/bin/superset-mcp.js +++ b/superset/mcp_service/bin/superset-mcp.js @@ -1,5 +1,24 @@ #!/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 * diff --git a/superset/mcp_service/flask_singleton.py b/superset/mcp_service/flask_singleton.py index 956d56f93987..3487182eaa3a 100644 --- a/superset/mcp_service/flask_singleton.py +++ b/superset/mcp_service/flask_singleton.py @@ -1,3 +1,19 @@ +# 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. diff --git a/superset/mcp_service/index.js b/superset/mcp_service/index.js index fcd040f2b085..05f7529c12a2 100644 --- a/superset/mcp_service/index.js +++ b/superset/mcp_service/index.js @@ -1,3 +1,22 @@ +/** + * 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 * diff --git a/superset/mcp_service/run_proxy.sh b/superset/mcp_service/run_proxy.sh index 2ef411bc4f44..8a58d9db7e9d 100644 --- a/superset/mcp_service/run_proxy.sh +++ b/superset/mcp_service/run_proxy.sh @@ -1,4 +1,20 @@ #!/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": { diff --git a/superset/mcp_service/simple_proxy.py b/superset/mcp_service/simple_proxy.py index 50639897fa99..5d95071f457f 100644 --- a/superset/mcp_service/simple_proxy.py +++ b/superset/mcp_service/simple_proxy.py @@ -1,4 +1,20 @@ #!/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 """ From ed4306d34cd3aa200ebbd501f63310f2dc760f76 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 16:06:50 -0400 Subject: [PATCH 11/16] feat(mcp): add FastMCP dependency and base constraints Add FastMCP package dependency to requirements/base.txt for MCP service functionality. Include base-constraint.txt for dependency version management. Update pyproject.toml with any related configuration changes. --- pyproject.toml | 1 + requirements/base-constraint.txt | 472 +++++++++++++++++++++++++++++++ requirements/base.txt | 1 - 3 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 requirements/base-constraint.txt diff --git a/pyproject.toml b/pyproject.toml index 3071e7a91a62..a8da52e04cc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,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.12.3"] firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"] firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"] gevent = ["gevent>=23.9.1"] diff --git a/requirements/base-constraint.txt b/requirements/base-constraint.txt new file mode 100644 index 000000000000..5825dd861d8f --- /dev/null +++ b/requirements/base-constraint.txt @@ -0,0 +1,472 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt + # via apache-superset (pyproject.toml) +alembic==1.15.2 + # via flask-migrate +amqp==5.3.1 + # via kombu +annotated-types==0.7.0 + # via pydantic +apispec==6.6.1 + # via + # -r requirements/base.in + # flask-appbuilder +apsw==3.50.1.0 + # via shillelagh +async-timeout==4.0.3 + # via -r requirements/base.in +attrs==25.3.0 + # via + # cattrs + # jsonschema + # outcome + # referencing + # requests-cache + # trio +babel==2.17.0 + # via flask-babel +backoff==2.2.1 + # via apache-superset (pyproject.toml) +bcrypt==4.3.0 + # via paramiko +billiard==4.2.1 + # via celery +blinker==1.9.0 + # via flask +bottleneck==1.5.0 + # via apache-superset (pyproject.toml) +brotli==1.1.0 + # via flask-compress +cachelib==0.13.0 + # via + # flask-caching + # flask-session +cachetools==5.5.2 + # via google-auth +cattrs==25.1.1 + # via requests-cache +celery==5.5.2 + # via apache-superset (pyproject.toml) +certifi==2025.6.15 + # via + # requests + # selenium +cffi==1.17.1 + # via + # cryptography + # pynacl +charset-normalizer==3.4.2 + # via requests +click==8.2.1 + # via + # apache-superset (pyproject.toml) + # celery + # click-didyoumean + # click-option-group + # click-plugins + # click-repl + # flask + # flask-appbuilder +click-didyoumean==0.3.1 + # via celery +click-option-group==0.5.7 + # via apache-superset (pyproject.toml) +click-plugins==1.1.1 + # via celery +click-repl==0.3.0 + # via celery +colorama==0.4.6 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder +cron-descriptor==1.4.5 + # via apache-superset (pyproject.toml) +croniter==6.0.0 + # via apache-superset (pyproject.toml) +cryptography==44.0.3 + # via + # apache-superset (pyproject.toml) + # paramiko + # pyopenssl +defusedxml==0.7.1 + # via odfpy +deprecated==1.2.18 + # via limits +deprecation==2.1.0 + # via apache-superset (pyproject.toml) +dnspython==2.7.0 + # via email-validator +email-validator==2.2.0 + # via flask-appbuilder +et-xmlfile==2.0.0 + # via openpyxl +flask==2.3.3 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder + # flask-babel + # flask-caching + # flask-compress + # flask-cors + # flask-jwt-extended + # flask-limiter + # flask-login + # flask-migrate + # flask-session + # flask-sqlalchemy + # flask-wtf +flask-appbuilder==5.0.0 + # via + # apache-superset (pyproject.toml) + # apache-superset-core +flask-babel==3.1.0 + # via flask-appbuilder +flask-caching==2.3.1 + # via apache-superset (pyproject.toml) +flask-compress==1.17 + # via apache-superset (pyproject.toml) +flask-cors==4.0.2 + # via apache-superset (pyproject.toml) +flask-jwt-extended==4.7.1 + # via flask-appbuilder +flask-limiter==3.12 + # via flask-appbuilder +flask-login==0.6.3 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder +flask-migrate==3.1.0 + # via apache-superset (pyproject.toml) +flask-session==0.8.0 + # via apache-superset (pyproject.toml) +flask-sqlalchemy==2.5.1 + # via + # flask-appbuilder + # flask-migrate +flask-talisman==1.1.0 + # via apache-superset (pyproject.toml) +flask-wtf==1.2.2 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder +geographiclib==2.0 + # via geopy +geopy==2.4.1 + # via apache-superset (pyproject.toml) +google-auth==2.40.3 + # via shillelagh +greenlet==3.1.1 + # via + # apache-superset (pyproject.toml) + # shillelagh +gunicorn==23.0.0 + # via apache-superset (pyproject.toml) +h11==0.16.0 + # via wsproto +hashids==1.3.1 + # via apache-superset (pyproject.toml) +holidays==0.25 + # via apache-superset (pyproject.toml) +humanize==4.12.3 + # via apache-superset (pyproject.toml) +idna==3.10 + # via + # email-validator + # requests + # trio + # url-normalize +isodate==0.7.2 + # via apache-superset (pyproject.toml) +itsdangerous==2.2.0 + # via + # flask + # flask-wtf +jinja2==3.1.6 + # via + # flask + # flask-babel +jsonpath-ng==1.7.0 + # via apache-superset (pyproject.toml) +jsonschema==4.23.0 + # via + # flask-appbuilder + # openapi-schema-validator +jsonschema-specifications==2025.4.1 + # via + # jsonschema + # openapi-schema-validator +kombu==5.5.3 + # via celery +korean-lunar-calendar==0.3.1 + # via holidays +limits==5.1.0 + # via flask-limiter +mako==1.3.10 + # via + # apache-superset (pyproject.toml) + # alembic +markdown==3.8 + # via apache-superset (pyproject.toml) +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug + # wtforms +marshmallow==3.26.1 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder + # marshmallow-sqlalchemy + # marshmallow-union +marshmallow-sqlalchemy==1.4.0 + # via + # -r requirements/base.in + # flask-appbuilder +marshmallow-union==0.1.15 + # via apache-superset (pyproject.toml) +mdurl==0.1.2 + # via markdown-it-py +msgpack==1.0.8 + # via apache-superset (pyproject.toml) +msgspec==0.19.0 + # via flask-session +nh3==0.2.21 + # via apache-superset (pyproject.toml) +numexpr==2.10.2 + # via -r requirements/base.in +numpy==1.26.4 + # via + # apache-superset (pyproject.toml) + # bottleneck + # numexpr + # pandas + # pyarrow +odfpy==1.4.1 + # via pandas +openapi-schema-validator==0.6.3 + # via -r requirements/base.in +openpyxl==3.1.5 + # via pandas +ordered-set==4.1.0 + # via flask-limiter +outcome==1.3.0.post0 + # via + # trio + # trio-websocket +packaging==25.0 + # via + # apache-superset (pyproject.toml) + # apispec + # deprecation + # gunicorn + # limits + # marshmallow + # shillelagh +pandas==2.1.4 + # via apache-superset (pyproject.toml) +paramiko==3.5.1 + # via + # apache-superset (pyproject.toml) + # sshtunnel +parsedatetime==2.6 + # via apache-superset (pyproject.toml) +pgsanity==0.2.9 + # via apache-superset (pyproject.toml) +pillow==11.3.0 + # via apache-superset (pyproject.toml) +platformdirs==4.3.8 + # via requests-cache +ply==3.11 + # via jsonpath-ng +polyline==2.0.2 + # via apache-superset (pyproject.toml) +prison==0.2.1 + # via flask-appbuilder +prompt-toolkit==3.0.51 + # via click-repl +pyarrow==16.1.0 + # via apache-superset (pyproject.toml) +pyasn1==0.6.1 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 + # via google-auth +pycparser==2.22 + # via cffi +pydantic==2.11.7 + # via apache-superset (pyproject.toml) +pydantic-core==2.33.2 + # via pydantic +pygments==2.19.1 + # via rich +pyjwt==2.10.1 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder + # flask-jwt-extended +pynacl==1.5.0 + # via paramiko +pyopenssl==25.1.0 + # via shillelagh +pyparsing==3.2.3 + # via apache-superset (pyproject.toml) +pysocks==1.7.1 + # via urllib3 +python-dateutil==2.9.0.post0 + # via + # apache-superset (pyproject.toml) + # celery + # croniter + # flask-appbuilder + # holidays + # pandas + # shillelagh +python-dotenv==1.1.0 + # via apache-superset (pyproject.toml) +python-geohash==0.8.5 + # via apache-superset (pyproject.toml) +pytz==2025.2 + # via + # croniter + # flask-babel + # pandas +pyxlsb==1.0.10 + # via pandas +pyyaml==6.0.2 + # via + # apache-superset (pyproject.toml) + # apispec +redis==4.6.0 + # via apache-superset (pyproject.toml) +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.4 + # via + # requests-cache + # shillelagh +requests-cache==1.2.1 + # via shillelagh +rfc3339-validator==0.1.4 + # via openapi-schema-validator +rich==13.9.4 + # via flask-limiter +rpds-py==0.25.0 + # via + # jsonschema + # referencing +rsa==4.9.1 + # via google-auth +selenium==4.32.0 + # via apache-superset (pyproject.toml) +shillelagh==1.3.5 + # via apache-superset (pyproject.toml) +simplejson==3.20.1 + # via apache-superset (pyproject.toml) +six==1.17.0 + # via + # prison + # python-dateutil + # rfc3339-validator + # wtforms-json +slack-sdk==3.35.0 + # via apache-superset (pyproject.toml) +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +sqlalchemy==1.4.54 + # via + # apache-superset (pyproject.toml) + # alembic + # flask-appbuilder + # flask-sqlalchemy + # marshmallow-sqlalchemy + # shillelagh + # sqlalchemy-utils +sqlalchemy-utils==0.38.3 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder +sqlglot==27.15.2 + # via apache-superset (pyproject.toml) +sshtunnel==0.4.0 + # via apache-superset (pyproject.toml) +tabulate==0.9.0 + # via apache-superset (pyproject.toml) +trio==0.30.0 + # via + # selenium + # trio-websocket +trio-websocket==0.12.2 + # via selenium +typing-extensions==4.14.0 + # via + # apache-superset (pyproject.toml) + # alembic + # cattrs + # limits + # pydantic + # pydantic-core + # pyopenssl + # referencing + # selenium + # shillelagh + # typing-inspection +typing-inspection==0.4.1 + # via pydantic +tzdata==2025.2 + # via + # kombu + # pandas +url-normalize==2.2.1 + # via requests-cache +urllib3==2.5.0 + # via + # -r requirements/base.in + # requests + # requests-cache + # selenium +vine==5.1.0 + # via + # amqp + # celery + # kombu +watchdog==6.0.0 + # via apache-superset (pyproject.toml) +wcwidth==0.2.13 + # via prompt-toolkit +websocket-client==1.8.0 + # via selenium +werkzeug==3.1.3 + # via + # -r requirements/base.in + # flask + # flask-appbuilder + # flask-jwt-extended + # flask-login +wrapt==1.17.2 + # via deprecated +wsproto==1.2.0 + # via trio-websocket +wtforms==3.2.1 + # via + # apache-superset (pyproject.toml) + # flask-appbuilder + # flask-wtf + # wtforms-json +wtforms-json==0.3.5 + # via apache-superset (pyproject.toml) +xlrd==2.0.1 + # via pandas +xlsxwriter==3.0.9 + # via + # apache-superset (pyproject.toml) + # pandas +zstandard==0.23.0 + # via flask-compress diff --git a/requirements/base.txt b/requirements/base.txt index 2373528df94a..7794e5a2457a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -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 From 2c6332ac1f7ba44c6e79b5837216c6df30eb51a4 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 16:19:07 -0400 Subject: [PATCH 12/16] update readme --- superset/config_mcp.py | 44 ---------------------------------- superset/mcp_service/README.md | 22 ----------------- 2 files changed, 66 deletions(-) delete mode 100644 superset/config_mcp.py diff --git a/superset/config_mcp.py b/superset/config_mcp.py deleted file mode 100644 index 14f6ebfb42a4..000000000000 --- a/superset/config_mcp.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 (Model Context Protocol) Service Configuration. - -Copy these settings to your superset_config.py to enable MCP: - - FEATURE_FLAGS = { - **FEATURE_FLAGS, # Keep existing flags - "MCP_SERVICE": True, - } - MCP_SERVICE_HOST = "localhost" - MCP_SERVICE_PORT = 5008 -""" - -import os - -# Enable MCP service integration -FEATURE_FLAGS = { - "MCP_SERVICE": True, -} - -# MCP Service Connection -MCP_SERVICE_HOST = os.environ.get("MCP_SERVICE_HOST", "localhost") -MCP_SERVICE_PORT = int(os.environ.get("MCP_SERVICE_PORT", 5008)) - -# Optional: Adjust rate limits if needed (defaults work for most cases) -# MCP_RATE_LIMIT_REQUESTS = 100 # requests per window -# MCP_RATE_LIMIT_WINDOW_SECONDS = 60 # window size in seconds -# MCP_STREAMING_MAX_SIZE_MB = 10 # max response size diff --git a/superset/mcp_service/README.md b/superset/mcp_service/README.md index c6108add65f6..ed6cea923542 100644 --- a/superset/mcp_service/README.md +++ b/superset/mcp_service/README.md @@ -99,17 +99,6 @@ SUPERSET_WEBSERVER_ADDRESS = 'http://localhost:9001' WEBDRIVER_BASEURL = 'http://localhost:9001/' WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL -import os - -# Enable MCP service integration -FEATURE_FLAGS = { - "MCP_SERVICE": True, -} - -# MCP Service Connection -MCP_SERVICE_HOST = os.environ.get("MCP_SERVICE_HOST", "localhost") -MCP_SERVICE_PORT = int(os.environ.get("MCP_SERVICE_PORT", 5008)) - EOF # 5. Initialize database @@ -138,17 +127,6 @@ superset mcp run --port 5008 --debug Access Superset at http://localhost:9001 (login: admin/admin) -### Automated MCP Setup (Optional) - -If you prefer, you can use the automated setup script instead of manually creating `superset_config.py`: - -```bash -# After step 3 above, run this instead of creating the config manually: -superset mcp setup - -# Then continue with steps 5-9 -``` - ## ๐Ÿ”Œ Step 2: Connect Claude Desktop ### For Docker Setup From b472e01e26de3b9b120a9e6ec034ef1a2bbe7f44 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 17:01:41 -0400 Subject: [PATCH 13/16] fix(mcp): make MCP service optional and fix exception chaining - Make MCP service imports optional in CLI to prevent CI failures - Add proper exception chaining with "from e" to fix B904 lint error - MCP service now gracefully handles missing fastmcp dependency - Provides helpful error message when dependencies are missing This allows CI to pass without fastmcp while maintaining MCP functionality when the dependency is installed. --- superset/cli/mcp.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/superset/cli/mcp.py b/superset/cli/mcp.py index cfd81952bc4c..8ec4831f3240 100644 --- a/superset/cli/mcp.py +++ b/superset/cli/mcp.py @@ -18,8 +18,6 @@ import click -from superset.mcp_service.server import run_server - @click.group() def mcp() -> None: @@ -33,4 +31,14 @@ def mcp() -> None: @click.option("--debug", is_flag=True, help="Enable debug mode") def run(host: str, port: int, debug: bool) -> None: """Run the MCP service""" - run_server(host=host, port=port, debug=debug) + 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 From a22c9bb31f5a28b0b7354573ae440effd26ae9ed Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Mon, 22 Sep 2025 17:12:28 -0400 Subject: [PATCH 14/16] updat Please enter the commit message for your changes. Lines starting --- superset/mcp_service/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/superset/mcp_service/README.md b/superset/mcp_service/README.md index ed6cea923542..6b44c25ce468 100644 --- a/superset/mcp_service/README.md +++ b/superset/mcp_service/README.md @@ -1,3 +1,22 @@ + + # 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. From 1a040d6480cfa23a4b1f31023c4d365d449755c9 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Tue, 23 Sep 2025 10:17:28 -0400 Subject: [PATCH 15/16] fix the fastmcp dep issue by downgrading fastmcp for now --- pyproject.toml | 2 +- requirements/base-constraint.txt | 472 ------------------------------- requirements/development.txt | 70 ++++- 3 files changed, 70 insertions(+), 474 deletions(-) delete mode 100644 requirements/base-constraint.txt diff --git a/pyproject.toml b/pyproject.toml index a8da52e04cc9..a0306e4139de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,7 +139,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.12.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"] diff --git a/requirements/base-constraint.txt b/requirements/base-constraint.txt deleted file mode 100644 index 5825dd861d8f..000000000000 --- a/requirements/base-constraint.txt +++ /dev/null @@ -1,472 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt - # via apache-superset (pyproject.toml) -alembic==1.15.2 - # via flask-migrate -amqp==5.3.1 - # via kombu -annotated-types==0.7.0 - # via pydantic -apispec==6.6.1 - # via - # -r requirements/base.in - # flask-appbuilder -apsw==3.50.1.0 - # via shillelagh -async-timeout==4.0.3 - # via -r requirements/base.in -attrs==25.3.0 - # via - # cattrs - # jsonschema - # outcome - # referencing - # requests-cache - # trio -babel==2.17.0 - # via flask-babel -backoff==2.2.1 - # via apache-superset (pyproject.toml) -bcrypt==4.3.0 - # via paramiko -billiard==4.2.1 - # via celery -blinker==1.9.0 - # via flask -bottleneck==1.5.0 - # via apache-superset (pyproject.toml) -brotli==1.1.0 - # via flask-compress -cachelib==0.13.0 - # via - # flask-caching - # flask-session -cachetools==5.5.2 - # via google-auth -cattrs==25.1.1 - # via requests-cache -celery==5.5.2 - # via apache-superset (pyproject.toml) -certifi==2025.6.15 - # via - # requests - # selenium -cffi==1.17.1 - # via - # cryptography - # pynacl -charset-normalizer==3.4.2 - # via requests -click==8.2.1 - # via - # apache-superset (pyproject.toml) - # celery - # click-didyoumean - # click-option-group - # click-plugins - # click-repl - # flask - # flask-appbuilder -click-didyoumean==0.3.1 - # via celery -click-option-group==0.5.7 - # via apache-superset (pyproject.toml) -click-plugins==1.1.1 - # via celery -click-repl==0.3.0 - # via celery -colorama==0.4.6 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder -cron-descriptor==1.4.5 - # via apache-superset (pyproject.toml) -croniter==6.0.0 - # via apache-superset (pyproject.toml) -cryptography==44.0.3 - # via - # apache-superset (pyproject.toml) - # paramiko - # pyopenssl -defusedxml==0.7.1 - # via odfpy -deprecated==1.2.18 - # via limits -deprecation==2.1.0 - # via apache-superset (pyproject.toml) -dnspython==2.7.0 - # via email-validator -email-validator==2.2.0 - # via flask-appbuilder -et-xmlfile==2.0.0 - # via openpyxl -flask==2.3.3 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder - # flask-babel - # flask-caching - # flask-compress - # flask-cors - # flask-jwt-extended - # flask-limiter - # flask-login - # flask-migrate - # flask-session - # flask-sqlalchemy - # flask-wtf -flask-appbuilder==5.0.0 - # via - # apache-superset (pyproject.toml) - # apache-superset-core -flask-babel==3.1.0 - # via flask-appbuilder -flask-caching==2.3.1 - # via apache-superset (pyproject.toml) -flask-compress==1.17 - # via apache-superset (pyproject.toml) -flask-cors==4.0.2 - # via apache-superset (pyproject.toml) -flask-jwt-extended==4.7.1 - # via flask-appbuilder -flask-limiter==3.12 - # via flask-appbuilder -flask-login==0.6.3 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder -flask-migrate==3.1.0 - # via apache-superset (pyproject.toml) -flask-session==0.8.0 - # via apache-superset (pyproject.toml) -flask-sqlalchemy==2.5.1 - # via - # flask-appbuilder - # flask-migrate -flask-talisman==1.1.0 - # via apache-superset (pyproject.toml) -flask-wtf==1.2.2 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder -geographiclib==2.0 - # via geopy -geopy==2.4.1 - # via apache-superset (pyproject.toml) -google-auth==2.40.3 - # via shillelagh -greenlet==3.1.1 - # via - # apache-superset (pyproject.toml) - # shillelagh -gunicorn==23.0.0 - # via apache-superset (pyproject.toml) -h11==0.16.0 - # via wsproto -hashids==1.3.1 - # via apache-superset (pyproject.toml) -holidays==0.25 - # via apache-superset (pyproject.toml) -humanize==4.12.3 - # via apache-superset (pyproject.toml) -idna==3.10 - # via - # email-validator - # requests - # trio - # url-normalize -isodate==0.7.2 - # via apache-superset (pyproject.toml) -itsdangerous==2.2.0 - # via - # flask - # flask-wtf -jinja2==3.1.6 - # via - # flask - # flask-babel -jsonpath-ng==1.7.0 - # via apache-superset (pyproject.toml) -jsonschema==4.23.0 - # via - # flask-appbuilder - # openapi-schema-validator -jsonschema-specifications==2025.4.1 - # via - # jsonschema - # openapi-schema-validator -kombu==5.5.3 - # via celery -korean-lunar-calendar==0.3.1 - # via holidays -limits==5.1.0 - # via flask-limiter -mako==1.3.10 - # via - # apache-superset (pyproject.toml) - # alembic -markdown==3.8 - # via apache-superset (pyproject.toml) -markdown-it-py==3.0.0 - # via rich -markupsafe==3.0.2 - # via - # jinja2 - # mako - # werkzeug - # wtforms -marshmallow==3.26.1 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder - # marshmallow-sqlalchemy - # marshmallow-union -marshmallow-sqlalchemy==1.4.0 - # via - # -r requirements/base.in - # flask-appbuilder -marshmallow-union==0.1.15 - # via apache-superset (pyproject.toml) -mdurl==0.1.2 - # via markdown-it-py -msgpack==1.0.8 - # via apache-superset (pyproject.toml) -msgspec==0.19.0 - # via flask-session -nh3==0.2.21 - # via apache-superset (pyproject.toml) -numexpr==2.10.2 - # via -r requirements/base.in -numpy==1.26.4 - # via - # apache-superset (pyproject.toml) - # bottleneck - # numexpr - # pandas - # pyarrow -odfpy==1.4.1 - # via pandas -openapi-schema-validator==0.6.3 - # via -r requirements/base.in -openpyxl==3.1.5 - # via pandas -ordered-set==4.1.0 - # via flask-limiter -outcome==1.3.0.post0 - # via - # trio - # trio-websocket -packaging==25.0 - # via - # apache-superset (pyproject.toml) - # apispec - # deprecation - # gunicorn - # limits - # marshmallow - # shillelagh -pandas==2.1.4 - # via apache-superset (pyproject.toml) -paramiko==3.5.1 - # via - # apache-superset (pyproject.toml) - # sshtunnel -parsedatetime==2.6 - # via apache-superset (pyproject.toml) -pgsanity==0.2.9 - # via apache-superset (pyproject.toml) -pillow==11.3.0 - # via apache-superset (pyproject.toml) -platformdirs==4.3.8 - # via requests-cache -ply==3.11 - # via jsonpath-ng -polyline==2.0.2 - # via apache-superset (pyproject.toml) -prison==0.2.1 - # via flask-appbuilder -prompt-toolkit==3.0.51 - # via click-repl -pyarrow==16.1.0 - # via apache-superset (pyproject.toml) -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.2 - # via google-auth -pycparser==2.22 - # via cffi -pydantic==2.11.7 - # via apache-superset (pyproject.toml) -pydantic-core==2.33.2 - # via pydantic -pygments==2.19.1 - # via rich -pyjwt==2.10.1 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder - # flask-jwt-extended -pynacl==1.5.0 - # via paramiko -pyopenssl==25.1.0 - # via shillelagh -pyparsing==3.2.3 - # via apache-superset (pyproject.toml) -pysocks==1.7.1 - # via urllib3 -python-dateutil==2.9.0.post0 - # via - # apache-superset (pyproject.toml) - # celery - # croniter - # flask-appbuilder - # holidays - # pandas - # shillelagh -python-dotenv==1.1.0 - # via apache-superset (pyproject.toml) -python-geohash==0.8.5 - # via apache-superset (pyproject.toml) -pytz==2025.2 - # via - # croniter - # flask-babel - # pandas -pyxlsb==1.0.10 - # via pandas -pyyaml==6.0.2 - # via - # apache-superset (pyproject.toml) - # apispec -redis==4.6.0 - # via apache-superset (pyproject.toml) -referencing==0.36.2 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.4 - # via - # requests-cache - # shillelagh -requests-cache==1.2.1 - # via shillelagh -rfc3339-validator==0.1.4 - # via openapi-schema-validator -rich==13.9.4 - # via flask-limiter -rpds-py==0.25.0 - # via - # jsonschema - # referencing -rsa==4.9.1 - # via google-auth -selenium==4.32.0 - # via apache-superset (pyproject.toml) -shillelagh==1.3.5 - # via apache-superset (pyproject.toml) -simplejson==3.20.1 - # via apache-superset (pyproject.toml) -six==1.17.0 - # via - # prison - # python-dateutil - # rfc3339-validator - # wtforms-json -slack-sdk==3.35.0 - # via apache-superset (pyproject.toml) -sniffio==1.3.1 - # via trio -sortedcontainers==2.4.0 - # via trio -sqlalchemy==1.4.54 - # via - # apache-superset (pyproject.toml) - # alembic - # flask-appbuilder - # flask-sqlalchemy - # marshmallow-sqlalchemy - # shillelagh - # sqlalchemy-utils -sqlalchemy-utils==0.38.3 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder -sqlglot==27.15.2 - # via apache-superset (pyproject.toml) -sshtunnel==0.4.0 - # via apache-superset (pyproject.toml) -tabulate==0.9.0 - # via apache-superset (pyproject.toml) -trio==0.30.0 - # via - # selenium - # trio-websocket -trio-websocket==0.12.2 - # via selenium -typing-extensions==4.14.0 - # via - # apache-superset (pyproject.toml) - # alembic - # cattrs - # limits - # pydantic - # pydantic-core - # pyopenssl - # referencing - # selenium - # shillelagh - # typing-inspection -typing-inspection==0.4.1 - # via pydantic -tzdata==2025.2 - # via - # kombu - # pandas -url-normalize==2.2.1 - # via requests-cache -urllib3==2.5.0 - # via - # -r requirements/base.in - # requests - # requests-cache - # selenium -vine==5.1.0 - # via - # amqp - # celery - # kombu -watchdog==6.0.0 - # via apache-superset (pyproject.toml) -wcwidth==0.2.13 - # via prompt-toolkit -websocket-client==1.8.0 - # via selenium -werkzeug==3.1.3 - # via - # -r requirements/base.in - # flask - # flask-appbuilder - # flask-jwt-extended - # flask-login -wrapt==1.17.2 - # via deprecated -wsproto==1.2.0 - # via trio-websocket -wtforms==3.2.1 - # via - # apache-superset (pyproject.toml) - # flask-appbuilder - # flask-wtf - # wtforms-json -wtforms-json==0.3.5 - # via apache-superset (pyproject.toml) -xlrd==2.0.1 - # via pandas -xlsxwriter==3.0.9 - # via - # apache-superset (pyproject.toml) - # pandas -zstandard==0.23.0 - # via flask-compress diff --git a/requirements/development.txt b/requirements/development.txt index eb05da006043..a49536a8f09b 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -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==1.3.2 # via duckdb-engine duckdb-engine==0.17.0 @@ -189,10 +208,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 @@ -331,7 +355,6 @@ greenlet==3.1.1 # apache-superset # gevent # shillelagh - # sqlalchemy grpcio==1.71.0 # via # apache-superset @@ -346,6 +369,8 @@ gunicorn==23.0.0 h11==0.16.0 # via # -c requirements/base-constraint.txt + # httpcore + # uvicorn # wsproto hashids==1.3.1 # via @@ -356,6 +381,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 @@ -365,7 +398,9 @@ identify==2.5.36 idna==3.10 # via # -c requirements/base-constraint.txt + # anyio # email-validator + # httpx # requests # trio # url-normalize @@ -398,6 +433,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 @@ -462,6 +498,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 @@ -501,6 +539,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 @@ -641,10 +681,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 @@ -680,6 +726,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 @@ -717,12 +765,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 @@ -777,7 +829,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 @@ -824,6 +881,7 @@ slack-sdk==3.35.0 sniffio==1.3.1 # via # -c requirements/base-constraint.txt + # anyio # trio sortedcontainers==2.4.0 # via @@ -854,10 +912,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 @@ -885,8 +947,10 @@ typing-extensions==4.14.0 # via # -c requirements/base-constraint.txt # alembic + # anyio # apache-superset # cattrs + # exceptiongroup # limits # pydantic # pydantic-core @@ -894,11 +958,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 @@ -917,6 +983,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 From 6bb4feb5c70d9a22062291e6cd4d5e59e40a1ed7 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Thu, 25 Sep 2025 13:42:25 -0400 Subject: [PATCH 16/16] refactor(mcp): improve config loading to read from app.config first Update MCP config loading to properly read from Flask app.config before falling back to defaults. This allows users to override MCP settings in their superset_config.py file. - Refactor get_mcp_config() to accept app_config parameter - Use Pythonic dict unpacking for cleaner config merging - Add type hints with modern union syntax - Update flask_singleton.py to pass app.config to get_mcp_config() --- superset/mcp_service/flask_singleton.py | 8 ++- superset/mcp_service/mcp_config.py | 66 ++++++++++++++++--------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/superset/mcp_service/flask_singleton.py b/superset/mcp_service/flask_singleton.py index 3487182eaa3a..17816a36e408 100644 --- a/superset/mcp_service/flask_singleton.py +++ b/superset/mcp_service/flask_singleton.py @@ -39,11 +39,9 @@ # Create the Flask app instance - this is the singleton app = create_app() - # Apply MCP-specific defaults to app.config if not already set - mcp_config = get_mcp_config() - for key, value in mcp_config.items(): - if key not in app.config: - app.config[key] = value + # 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") diff --git a/superset/mcp_service/mcp_config.py b/superset/mcp_service/mcp_config.py index 545f6c21314d..13b8b36a7924 100644 --- a/superset/mcp_service/mcp_config.py +++ b/superset/mcp_service/mcp_config.py @@ -73,29 +73,49 @@ def generate_secret_key() -> str: return secrets.token_urlsafe(42) -def get_mcp_config() -> Dict[str, Any]: - """Get complete MCP configuration dictionary""" - config = {} - - # Add MCP-specific settings - config.update( - { - "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, - } - ) - - # Add session and CSRF config - config.update(MCP_SESSION_CONFIG) - config.update(MCP_CSRF_CONFIG) - - return 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]: