"""
Stats Compass MCP Server.
A unified MCP server that supports both stdio and HTTP transports.
All tool definitions are shared - only the transport differs.
Usage:
# stdio (for Claude Desktop, VS Code local)
stats-compass-mcp run
# HTTP (for remote/hosted deployments)
stats-compass-mcp serve --port 8000
"""
import logging
import os
from fastmcp import FastMCP
from stats_compass_mcp.session import SessionManager
from stats_compass_mcp.tools import register_all_tools
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# ============================================================================
# Configuration from environment
# ============================================================================
MEMORY_LIMIT_MB = float(os.getenv("STATS_COMPASS_MEMORY_LIMIT_MB", "500"))
MAX_SESSIONS = int(os.getenv("STATS_COMPASS_MAX_SESSIONS", "100"))
# ============================================================================
# Create server and dependencies
# ============================================================================
def create_mcp_server(
name: str = "stats-compass",
with_storage: bool = False
) -> FastMCP:
"""
Create a configured FastMCP server with all tools registered.
Args:
name: Server name
with_storage: Whether to enable file upload storage (for remote deployments)
Returns:
Configured FastMCP server
"""
mcp = FastMCP(
name,
instructions=(
"Stats Compass is a data analysis toolkit. "
"Sessions are created automatically - no need to call create_session. "
"Your data is isolated to your session. "
"Use load_dataset() for sample data, or load_csv()/load_excel() for local files."
"Do not rely on code generation for analysis - use the provided stats compass tools."
)
)
# Create session manager (single instance)
session_manager = SessionManager(
memory_limit_mb=MEMORY_LIMIT_MB,
max_sessions=MAX_SESSIONS
)
# Optional storage backend for remote deployments
storage = None
if with_storage:
try:
from stats_compass_mcp.storage import create_storage_backend
storage = create_storage_backend()
logger.info("Storage backend enabled for file uploads")
except ImportError:
logger.warning("Storage backend not available - file uploads disabled")
# Register all tools (single source of truth)
register_all_tools(mcp, session_manager, storage=storage)
logger.info(f"Server '{name}' configured with {MAX_SESSIONS} max sessions")
return mcp
# ============================================================================
# Module-level server instance (for CLI)
# ============================================================================
# This is created lazily when needed
_mcp: FastMCP | None = None
def get_server(with_storage: bool = False) -> FastMCP:
"""Get or create the MCP server instance."""
global _mcp
if _mcp is None:
_mcp = create_mcp_server(with_storage=with_storage)
return _mcp
# ============================================================================
# Entry points for different transports
# ============================================================================
def run_stdio() -> None:
"""Run server with stdio transport (for local MCP clients)."""
logger.info("Starting Stats Compass MCP (stdio transport)")
mcp = create_mcp_server(with_storage=False)
mcp.run()
def run_http(host: str = "0.0.0.0", port: int = 8000) -> None: # nosec B104
"""Run server with HTTP transport (for remote deployments)."""
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Mount
from stats_compass_mcp.upload import create_upload_routes
logger.info(f"Starting Stats Compass MCP (HTTP transport) at {host}:{port}")
logger.info(f"Config: memory_limit={MEMORY_LIMIT_MB}MB, max_sessions={MAX_SESSIONS}")
mcp = create_mcp_server(with_storage=True)
# Create combined app with MCP + upload routes
mcp_app = mcp.http_app()
upload_routes = create_upload_routes()
# Combine routes: upload routes first, then mount MCP app
# IMPORTANT: Pass mcp_app.lifespan to initialize the task group
app = Starlette(
routes=[
*upload_routes,
Mount("/", mcp_app), # MCP handles /mcp and /sse
],
lifespan=mcp_app.lifespan, # Required for FastMCP's async task group
)
logger.info("Upload endpoints: GET /upload, POST /api/upload")
uvicorn.run(
app,
host=host,
port=port,
)
# Allow running directly for testing
if __name__ == "__main__":
run_stdio()