Spaces:
Sleeping
Sleeping
areeb1501
commited on
Commit
·
626b033
0
Parent(s):
Initial commit - Instant MCP platform
Browse files- .DS_Store +0 -0
- .env.example +87 -0
- .gitignore +8 -0
- Dockerfile +72 -0
- README.md +898 -0
- app.py +393 -0
- claude.md +4 -0
- mcp_tools/__init__.py +16 -0
- mcp_tools/ai_assistant.py +1084 -0
- mcp_tools/deployment_tools.py +1855 -0
- mcp_tools/security_tools.py +112 -0
- mcp_tools/stats_tools.py +105 -0
- requirements.txt +30 -0
- ui_components/__init__.py +17 -0
- ui_components/admin_panel.py +277 -0
- ui_components/ai_chat_deployment.py +847 -0
- ui_components/code_editor.py +310 -0
- ui_components/log_viewer.py +183 -0
- ui_components/stats_dashboard.py +261 -0
- utils/__init__.py +34 -0
- utils/database.py +381 -0
- utils/models.py +551 -0
- utils/security_scanner.py +373 -0
- utils/simple_tracking.py +75 -0
- utils/usage_tracker.py +447 -0
- utils/webhook_receiver.py +23 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.env.example
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# MCP Deployment Platform - Environment Variables
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# Copy this file to .env and fill in your actual values
|
| 5 |
+
# Never commit .env to version control!
|
| 6 |
+
|
| 7 |
+
# ============================================================================
|
| 8 |
+
# Database Configuration
|
| 9 |
+
# ============================================================================
|
| 10 |
+
# PostgreSQL connection string
|
| 11 |
+
# Format: postgresql://username:password@host:port/database_name
|
| 12 |
+
DATABASE_URL=postgresql://user:password@localhost:5432/mcp_deployer
|
| 13 |
+
|
| 14 |
+
# ============================================================================
|
| 15 |
+
# Modal.com Deployment Credentials
|
| 16 |
+
# ============================================================================
|
| 17 |
+
# Get these from: https://modal.com/settings
|
| 18 |
+
MODAL_TOKEN_ID=your_modal_token_id_here
|
| 19 |
+
MODAL_TOKEN_SECRET=your_modal_token_secret_here
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# AI Security Scanning (Optional)
|
| 23 |
+
# ============================================================================
|
| 24 |
+
# OpenAI API key for security code scanning
|
| 25 |
+
# If not set, security scanning will be disabled
|
| 26 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 27 |
+
|
| 28 |
+
# Alternative: Nebius AI (OpenAI-compatible)
|
| 29 |
+
# NEBIUS_API_KEY=your_nebius_api_key_here
|
| 30 |
+
# NEBIUS_API_BASE=https://api.nebius.com/v1
|
| 31 |
+
|
| 32 |
+
# ============================================================================
|
| 33 |
+
# SambaNova AI Configuration (for AI Assistant alternative models)
|
| 34 |
+
# ============================================================================
|
| 35 |
+
# SambaNova API key for using SambaNova models in AI Assistant
|
| 36 |
+
# Get from: https://cloud.sambanova.ai/
|
| 37 |
+
# Supports models: Meta-Llama-3.3-70B-Instruct, DeepSeek-V3-0324,
|
| 38 |
+
# Llama-4-Maverick-17B-128E-Instruct, Qwen3-32B,
|
| 39 |
+
# gpt-oss-120b, DeepSeek-V3.1
|
| 40 |
+
SAMBANOVA_API_KEY=your_sambanova_api_key_here
|
| 41 |
+
|
| 42 |
+
# SambaNova API base URL (OpenAI-compatible endpoint)
|
| 43 |
+
SAMBANOVA_BASE_URL=https://api.sambanova.ai/v1
|
| 44 |
+
|
| 45 |
+
# ============================================================================
|
| 46 |
+
# Server Configuration
|
| 47 |
+
# ============================================================================
|
| 48 |
+
# Port for the Gradio server (default: 7860 for HF Spaces)
|
| 49 |
+
PORT=7860
|
| 50 |
+
|
| 51 |
+
# ============================================================================
|
| 52 |
+
# Webhook Usage Tracking Configuration
|
| 53 |
+
# ============================================================================
|
| 54 |
+
# Webhook URL for receiving usage data from deployed MCP servers
|
| 55 |
+
# This should point to your Gradio app's webhook endpoint
|
| 56 |
+
# Format: http://your-app-url/api/webhook/usage
|
| 57 |
+
MCP_WEBHOOK_URL=http://localhost:7860/api/webhook/usage
|
| 58 |
+
|
| 59 |
+
# Webhook secret for HMAC signature validation
|
| 60 |
+
# IMPORTANT: Generate a random secret key using:
|
| 61 |
+
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
| 62 |
+
MCP_WEBHOOK_SECRET=your_generated_webhook_secret_here
|
| 63 |
+
|
| 64 |
+
# Enable or disable webhook tracking
|
| 65 |
+
MCP_WEBHOOK_ENABLED=true
|
| 66 |
+
|
| 67 |
+
# Base URL for your Gradio app (used for auto-configuration)
|
| 68 |
+
MCP_BASE_URL=http://localhost:7860
|
| 69 |
+
|
| 70 |
+
# Webhook rate limit (requests per minute per deployment)
|
| 71 |
+
MCP_WEBHOOK_RATE_LIMIT=1000
|
| 72 |
+
|
| 73 |
+
# ============================================================================
|
| 74 |
+
# Email Notifications (Optional)
|
| 75 |
+
# ============================================================================
|
| 76 |
+
# Resend API key for email notifications
|
| 77 |
+
# RESEND_API_KEY=your_resend_api_key_here
|
| 78 |
+
|
| 79 |
+
# ============================================================================
|
| 80 |
+
# Hugging Face Spaces Configuration
|
| 81 |
+
# ============================================================================
|
| 82 |
+
# When deploying to HF Spaces, set these in Space Settings → Variables and Secrets:
|
| 83 |
+
# - DATABASE_URL (Secret)
|
| 84 |
+
# - MODAL_TOKEN_ID (Secret)
|
| 85 |
+
# - MODAL_TOKEN_SECRET (Secret)
|
| 86 |
+
# - OPENAI_API_KEY (Secret, optional)
|
| 87 |
+
# - PORT (Variable, default: 7860)
|
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tests/
|
| 2 |
+
*.log
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.env
|
| 6 |
+
enhancements/
|
| 7 |
+
deployments_archive
|
| 8 |
+
.claude
|
Dockerfile
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile for Gradio MCP Deployment Platform
|
| 2 |
+
# Optimized for Hugging Face Spaces Docker deployment
|
| 3 |
+
FROM python:3.12-slim
|
| 4 |
+
|
| 5 |
+
# Set working directory
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Install system dependencies
|
| 9 |
+
# - git: Required for Modal CLI and version control
|
| 10 |
+
# - curl: For health checks and HTTP requests
|
| 11 |
+
# - build-essential: Required for compiling some Python packages (psycopg2)
|
| 12 |
+
# - libpq-dev: PostgreSQL development libraries for psycopg2
|
| 13 |
+
RUN apt-get update && apt-get install -y \
|
| 14 |
+
git \
|
| 15 |
+
curl \
|
| 16 |
+
build-essential \
|
| 17 |
+
libpq-dev \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Copy requirements first (for better Docker caching)
|
| 21 |
+
COPY requirements.txt .
|
| 22 |
+
|
| 23 |
+
# Install Python dependencies
|
| 24 |
+
# Using --no-cache-dir to reduce image size
|
| 25 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
+
|
| 27 |
+
# Create a non-root user for HF Spaces compatibility
|
| 28 |
+
# HF Spaces runs containers as user with uid 1000
|
| 29 |
+
RUN useradd -m -u 1000 user
|
| 30 |
+
|
| 31 |
+
# Create necessary directories with proper permissions
|
| 32 |
+
RUN mkdir -p /app/deployments /home/user/.modal && \
|
| 33 |
+
chown -R user:user /app /home/user/.modal
|
| 34 |
+
|
| 35 |
+
# Copy application code and required directories
|
| 36 |
+
COPY --chown=user:user app.py .
|
| 37 |
+
COPY --chown=user:user mcp_tools/ ./mcp_tools/
|
| 38 |
+
COPY --chown=user:user ui_components/ ./ui_components/
|
| 39 |
+
COPY --chown=user:user utils/ ./utils/
|
| 40 |
+
|
| 41 |
+
# Switch to non-root user
|
| 42 |
+
USER user
|
| 43 |
+
|
| 44 |
+
# Set home directory for the user
|
| 45 |
+
ENV HOME=/home/user
|
| 46 |
+
|
| 47 |
+
# Expose port 7860 (Gradio's default port, also used by Hugging Face Spaces)
|
| 48 |
+
EXPOSE 7860
|
| 49 |
+
|
| 50 |
+
# Set environment variables for Gradio
|
| 51 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 52 |
+
PORT=7860 \
|
| 53 |
+
GRADIO_SERVER_NAME="0.0.0.0" \
|
| 54 |
+
GRADIO_SERVER_PORT=7860 \
|
| 55 |
+
GRADIO_MCP_SERVER=True
|
| 56 |
+
|
| 57 |
+
# Health check
|
| 58 |
+
# Checks if Gradio app is responding on the specified port
|
| 59 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 60 |
+
CMD curl -f http://localhost:${PORT:-7860}/ || exit 1
|
| 61 |
+
|
| 62 |
+
# Startup script
|
| 63 |
+
# 1. Configure Modal authentication if credentials are provided
|
| 64 |
+
# 2. Launch Gradio app with MCP server enabled
|
| 65 |
+
CMD if [ -n "$MODAL_TOKEN_ID" ] && [ -n "$MODAL_TOKEN_SECRET" ]; then \
|
| 66 |
+
echo "🔐 Configuring Modal authentication..."; \
|
| 67 |
+
modal token set --token-id "$MODAL_TOKEN_ID" --token-secret "$MODAL_TOKEN_SECRET"; \
|
| 68 |
+
fi && \
|
| 69 |
+
echo "🚀 Starting Gradio MCP Deployment Platform..." && \
|
| 70 |
+
echo "📊 Web UI will be available at: http://0.0.0.0:${PORT:-7860}" && \
|
| 71 |
+
echo "📡 MCP endpoint will be at: http://0.0.0.0:${PORT:-7860}/gradio_api/mcp/" && \
|
| 72 |
+
python app.py
|
README.md
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Instant MCP
|
| 3 |
+
emoji: ⚡
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
short_description: Deploy MCP servers instantly from anywhere, powered by Modal
|
| 10 |
+
tags: ["mcp-in-action-track-enterprise", "mcp-in-action-track-consumer", "building-mcp-track-enterprise"]
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# ⚡ Instant MCP - Deploy Anywhere, Connect Everywhere
|
| 14 |
+
|
| 15 |
+
> **Instantly deploy MCP servers and access them from anywhere. Powered by Modal.**
|
| 16 |
+
|
| 17 |
+
Transform your workflow by deploying Model Context Protocol (MCP) servers in seconds, not hours. Connect to external APIs, save on token costs, and extend your AI capabilities with unlimited custom tools.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 🏆 Built for MCP's 1st Birthday Hackathon
|
| 22 |
+
|
| 23 |
+
**Submission Tracks:**
|
| 24 |
+
- 🔧 **Building MCP Track** - Enterprise
|
| 25 |
+
- 🤖 **MCP in Action Track** - Enterprise & Consumer
|
| 26 |
+
|
| 27 |
+
**Demo Video:** [Coming Soon - Placeholder]
|
| 28 |
+
|
| 29 |
+
**Social Media Post:** [Link to be added]
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 🎯 Sponsors & Key Technologies
|
| 34 |
+
|
| 35 |
+
<div align="center">
|
| 36 |
+
|
| 37 |
+
### Powered By
|
| 38 |
+
|
| 39 |
+
| Technology | Usage |
|
| 40 |
+
|------------|-------|
|
| 41 |
+
|  | **Serverless deployment** - Zero-downtime deployments with automatic scaling |
|
| 42 |
+
|  | **Claude AI** - Intelligent code generation and deployment assistance |
|
| 43 |
+
|  | **Gradio v6** - Interactive UI with enhanced mobile support and real-time updates |
|
| 44 |
+
|  | **AI Security Scanning** - Intelligent vulnerability detection before deployment |
|
| 45 |
+
|  | **Alternative LLM** - Cost-effective AI assistance with Llama 3.3 70B |
|
| 46 |
+
|  | **Hosting & Deployment** - Platform for sharing and deployment |
|
| 47 |
+
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## 🚀 What is Instant MCP?
|
| 53 |
+
|
| 54 |
+
**Instant MCP** is a complete platform that transforms how you create, deploy, and manage MCP servers. Built with Gradio v6 and powered by Modal's serverless infrastructure, it enables developers to:
|
| 55 |
+
|
| 56 |
+
✅ **Deploy MCP servers instantly** - From idea to production in under 60 seconds
|
| 57 |
+
✅ **Zero infrastructure management** - Modal handles scaling, cold starts, and costs
|
| 58 |
+
✅ **AI-assisted development** - Claude and SambaNova integration for intelligent code generation
|
| 59 |
+
✅ **Enterprise-grade security** - Automated vulnerability scanning with Nebius AI
|
| 60 |
+
✅ **Comprehensive analytics** - Track usage, performance, and costs in real-time
|
| 61 |
+
✅ **Cost optimization** - Scale to zero when idle, pay only for what you use
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 💡 Why Instant MCP? Real-World Use Cases
|
| 66 |
+
|
| 67 |
+
### 1. 🔌 **Connect to External APIs for Cost Savings**
|
| 68 |
+
|
| 69 |
+
Instead of using expensive Claude API calls for every task, deploy specialized MCP servers that:
|
| 70 |
+
- Cache API responses locally
|
| 71 |
+
- Batch multiple requests
|
| 72 |
+
- Use cheaper alternatives for simple tasks
|
| 73 |
+
- **Result:** Save 60-80% on token costs for repetitive operations
|
| 74 |
+
|
| 75 |
+
**Example:** Deploy a weather MCP server that caches forecasts instead of asking Claude to fetch them repeatedly.
|
| 76 |
+
|
| 77 |
+
### 2. 🎨 **Use Gemini for Frontend Development**
|
| 78 |
+
|
| 79 |
+
Create an MCP server that connects to Google's Gemini API for:
|
| 80 |
+
- UI/UX design suggestions
|
| 81 |
+
- Frontend code generation
|
| 82 |
+
- Visual component creation
|
| 83 |
+
- **Benefit:** Use Gemini's specialized capabilities while keeping Claude for backend logic
|
| 84 |
+
|
| 85 |
+
### 3. 🔍 **Perplexity as Web Search Engine**
|
| 86 |
+
|
| 87 |
+
Deploy an MCP server with Perplexity integration to:
|
| 88 |
+
- Perform web searches without consuming Claude tokens
|
| 89 |
+
- Get real-time information from the internet
|
| 90 |
+
- Extend conversation limits by offloading research to external tools
|
| 91 |
+
- **Impact:** 10x longer Claude sessions without hitting usage limits
|
| 92 |
+
|
| 93 |
+
**Example Use Case:**
|
| 94 |
+
```
|
| 95 |
+
User asks Claude: "What are the latest developments in quantum computing?"
|
| 96 |
+
→ Claude calls your Perplexity MCP server
|
| 97 |
+
→ Perplexity searches and summarizes
|
| 98 |
+
→ Claude receives results without token overhead
|
| 99 |
+
→ User gets answer, your session continues
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### 4. 🔬 **Building Research Tools**
|
| 103 |
+
|
| 104 |
+
Create specialized research assistants with MCP servers that:
|
| 105 |
+
- Query academic databases (arXiv, PubMed, Google Scholar)
|
| 106 |
+
- Aggregate data from multiple sources
|
| 107 |
+
- Process and summarize large documents
|
| 108 |
+
- Track citations and references
|
| 109 |
+
- **Workflow Improvement:** Researchers get automated literature reviews instead of manual searches
|
| 110 |
+
|
| 111 |
+
### 5. 🏢 **Enterprise Integration**
|
| 112 |
+
|
| 113 |
+
Deploy MCP servers that connect to:
|
| 114 |
+
- Internal databases and CRM systems
|
| 115 |
+
- Company knowledge bases
|
| 116 |
+
- Proprietary APIs and microservices
|
| 117 |
+
- Legacy systems without API exposure
|
| 118 |
+
- **Value:** Bring enterprise data to Claude without exposing credentials or building complex integrations
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## ✨ Key Features
|
| 123 |
+
|
| 124 |
+
### 🎯 Core Capabilities
|
| 125 |
+
|
| 126 |
+
#### 1. **Instant Deployment to Modal**
|
| 127 |
+
- One-click deployment from UI or AI chat
|
| 128 |
+
- Automatic dependency detection
|
| 129 |
+
- Zero-downtime updates
|
| 130 |
+
- Cost-optimized configuration (scales to zero)
|
| 131 |
+
- Public HTTPS endpoints instantly
|
| 132 |
+
|
| 133 |
+
#### 2. **AI-Powered Development** (Gradio v6 Feature: Agentic Chatbot)
|
| 134 |
+
- **Claude Sonnet 4** integration for intelligent code generation
|
| 135 |
+
- **SambaNova Llama 3.3 70B** as cost-effective alternative
|
| 136 |
+
- Natural language to MCP server conversion
|
| 137 |
+
- Automated debugging and optimization
|
| 138 |
+
- Code review and security suggestions
|
| 139 |
+
|
| 140 |
+
#### 3. **Enterprise-Grade Security**
|
| 141 |
+
- **Nebius AI-powered scanning** before every deployment
|
| 142 |
+
- Detects: SQL injection, command injection, malicious code
|
| 143 |
+
- Severity-based blocking (High/Critical vulnerabilities blocked)
|
| 144 |
+
- Audit trail for all security scans
|
| 145 |
+
- Manual scan tool for pre-deployment testing
|
| 146 |
+
|
| 147 |
+
#### 4. **Comprehensive Analytics Dashboard**
|
| 148 |
+
- Real-time usage statistics
|
| 149 |
+
- Tool popularity tracking
|
| 150 |
+
- Client distribution analysis
|
| 151 |
+
- Performance metrics (response times, success rates)
|
| 152 |
+
- Cost tracking and optimization insights
|
| 153 |
+
- Timeline visualizations (hourly/daily aggregations)
|
| 154 |
+
|
| 155 |
+
#### 5. **Production-Ready Database** (PostgreSQL)
|
| 156 |
+
- Scalable storage with connection pooling
|
| 157 |
+
- ACID transactions for data integrity
|
| 158 |
+
- Complete audit logging
|
| 159 |
+
- Soft delete with history preservation
|
| 160 |
+
- Advanced queries via SQLAlchemy ORM
|
| 161 |
+
|
| 162 |
+
### 🎨 Gradio v6 Features Used
|
| 163 |
+
|
| 164 |
+
This project showcases several **Gradio v6** capabilities:
|
| 165 |
+
|
| 166 |
+
1. **Enhanced MCP Support** (`mcp_server=True`)
|
| 167 |
+
- Built-in MCP server endpoint at `/gradio_api/mcp/`
|
| 168 |
+
- Automatic tool registration with `gr.api()`
|
| 169 |
+
- Streamable HTTP transport for tool calls
|
| 170 |
+
|
| 171 |
+
2. **Improved Component System**
|
| 172 |
+
- Tabbed interface for organized workflows
|
| 173 |
+
- Real-time updates without page refresh
|
| 174 |
+
- Custom CSS for polished UI
|
| 175 |
+
|
| 176 |
+
3. **Better API Control**
|
| 177 |
+
- `show_api=False` for UI-only handlers
|
| 178 |
+
- Explicit tool registration for MCP exposure
|
| 179 |
+
- FastAPI integration for custom endpoints
|
| 180 |
+
|
| 181 |
+
4. **Mobile-Responsive Design**
|
| 182 |
+
- Adaptive layouts for all screen sizes
|
| 183 |
+
- Touch-optimized controls
|
| 184 |
+
- Progressive web app capabilities
|
| 185 |
+
|
| 186 |
+
5. **Real-Time Streaming**
|
| 187 |
+
- Streaming chat responses from Claude/SambaNova
|
| 188 |
+
- Live deployment status updates
|
| 189 |
+
- Progressive tool execution feedback
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
## 🛠️ Available MCP Tools
|
| 194 |
+
|
| 195 |
+
### Deployment Management
|
| 196 |
+
|
| 197 |
+
#### `deploy_mcp_server`
|
| 198 |
+
**Deploy a new MCP server to Modal.com**
|
| 199 |
+
|
| 200 |
+
```python
|
| 201 |
+
{
|
| 202 |
+
"server_name": "weather-api",
|
| 203 |
+
"mcp_tools_code": "from fastmcp import FastMCP...",
|
| 204 |
+
"extra_pip_packages": "requests,pandas",
|
| 205 |
+
"description": "Weather data and forecasts",
|
| 206 |
+
"category": "APIs",
|
| 207 |
+
"tags": ["weather", "data"],
|
| 208 |
+
"author": "Your Name",
|
| 209 |
+
"version": "1.0.0"
|
| 210 |
+
}
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
**Features:**
|
| 214 |
+
- Automatic security scanning (Nebius AI)
|
| 215 |
+
- Dependency detection and installation
|
| 216 |
+
- Cost-optimized Modal configuration
|
| 217 |
+
- Instant HTTPS endpoint generation
|
| 218 |
+
- Complete audit logging
|
| 219 |
+
|
| 220 |
+
---
|
| 221 |
+
|
| 222 |
+
#### `list_deployments`
|
| 223 |
+
**Get all deployed MCP servers with statistics**
|
| 224 |
+
|
| 225 |
+
Returns deployment list with:
|
| 226 |
+
- URLs and endpoints
|
| 227 |
+
- Usage statistics
|
| 228 |
+
- Status and health checks
|
| 229 |
+
- Last used timestamps
|
| 230 |
+
- Total request counts
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
#### `get_deployment_status`
|
| 235 |
+
**Check detailed status of a deployment**
|
| 236 |
+
|
| 237 |
+
```python
|
| 238 |
+
{
|
| 239 |
+
"deployment_id": "deploy-mcp-weather-abc123"
|
| 240 |
+
}
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
- Live status check (is it running on Modal?)
|
| 245 |
+
- Current URL and endpoint
|
| 246 |
+
- Configuration details
|
| 247 |
+
- Usage statistics
|
| 248 |
+
- Health metrics
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
#### `get_deployment_code`
|
| 253 |
+
**Retrieve the source code of a deployment**
|
| 254 |
+
|
| 255 |
+
Use this before modifying a deployment to see current code, packages, and tools.
|
| 256 |
+
|
| 257 |
+
---
|
| 258 |
+
|
| 259 |
+
#### `update_deployment_code`
|
| 260 |
+
**Update and redeploy an MCP server**
|
| 261 |
+
|
| 262 |
+
```python
|
| 263 |
+
{
|
| 264 |
+
"deployment_id": "deploy-mcp-weather-abc123",
|
| 265 |
+
"mcp_tools_code": "updated code...",
|
| 266 |
+
"extra_pip_packages": ["requests", "beautifulsoup4"],
|
| 267 |
+
"server_name": "new-name",
|
| 268 |
+
"description": "Updated description"
|
| 269 |
+
}
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
**Smart Updates:**
|
| 273 |
+
- Preserves URL (reuses same Modal app name)
|
| 274 |
+
- Brief downtime (~5-10 seconds)
|
| 275 |
+
- Automatic backup before update
|
| 276 |
+
- Security re-scanning
|
| 277 |
+
- Rollback capability via history
|
| 278 |
+
|
| 279 |
+
---
|
| 280 |
+
|
| 281 |
+
#### `delete_deployment`
|
| 282 |
+
**Remove a deployment from Modal**
|
| 283 |
+
|
| 284 |
+
```python
|
| 285 |
+
{
|
| 286 |
+
"deployment_id": "deploy-mcp-weather-abc123",
|
| 287 |
+
"confirm": true
|
| 288 |
+
}
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
**Safe Deletion:**
|
| 292 |
+
- Requires explicit confirmation
|
| 293 |
+
- Soft delete (preserves history)
|
| 294 |
+
- Stops Modal app billing
|
| 295 |
+
- Maintains audit trail
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
### Security Tools
|
| 300 |
+
|
| 301 |
+
#### `scan_deployment_security`
|
| 302 |
+
**Scan MCP code for vulnerabilities WITHOUT deploying**
|
| 303 |
+
|
| 304 |
+
```python
|
| 305 |
+
{
|
| 306 |
+
"mcp_tools_code": "your code...",
|
| 307 |
+
"server_name": "my-server",
|
| 308 |
+
"extra_pip_packages": ["requests"],
|
| 309 |
+
"description": "Optional context"
|
| 310 |
+
}
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
**Powered by Nebius AI** - Detects:
|
| 314 |
+
- ❌ Code injection (SQL, command, etc.)
|
| 315 |
+
- ❌ Malicious network behavior
|
| 316 |
+
- ❌ Resource abuse patterns
|
| 317 |
+
- ❌ Destructive operations
|
| 318 |
+
- ❌ Known malicious packages
|
| 319 |
+
|
| 320 |
+
**Severity Levels:**
|
| 321 |
+
- ✅ **Safe** - No issues found
|
| 322 |
+
- ⚠️ **Low** - Minor concerns, allowed
|
| 323 |
+
- ⚠️ **Medium** - Review suggested, allowed
|
| 324 |
+
- 🚫 **High** - Serious issues, deployment blocked
|
| 325 |
+
- 🚫 **Critical** - Severe threats, deployment blocked
|
| 326 |
+
|
| 327 |
+
---
|
| 328 |
+
|
| 329 |
+
### Analytics & Statistics
|
| 330 |
+
|
| 331 |
+
#### `get_deployment_stats`
|
| 332 |
+
**Get comprehensive usage statistics**
|
| 333 |
+
|
| 334 |
+
```python
|
| 335 |
+
{
|
| 336 |
+
"deployment_id": "deploy-mcp-weather-abc123",
|
| 337 |
+
"days": 30
|
| 338 |
+
}
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
- Total requests and success rate
|
| 343 |
+
- Average response time
|
| 344 |
+
- Peak usage periods
|
| 345 |
+
- Error rate analysis
|
| 346 |
+
- Client distribution
|
| 347 |
+
|
| 348 |
+
---
|
| 349 |
+
|
| 350 |
+
#### `get_tool_usage`
|
| 351 |
+
**See which tools are used most**
|
| 352 |
+
|
| 353 |
+
```python
|
| 354 |
+
{
|
| 355 |
+
"deployment_id": "deploy-mcp-weather-abc123",
|
| 356 |
+
"days": 30,
|
| 357 |
+
"limit": 10
|
| 358 |
+
}
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
**Insights:**
|
| 362 |
+
- Most popular tools
|
| 363 |
+
- Request counts per tool
|
| 364 |
+
- Success rates by tool
|
| 365 |
+
- Performance comparison
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
#### `get_all_stats_summary`
|
| 370 |
+
**Quick overview of all deployments**
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
- Total deployments count
|
| 374 |
+
- Total requests across all servers
|
| 375 |
+
- Average success rate
|
| 376 |
+
- Active vs. idle deployments
|
| 377 |
+
- Resource utilization
|
| 378 |
+
|
| 379 |
+
---
|
| 380 |
+
|
| 381 |
+
## 📸 Screenshots
|
| 382 |
+
|
| 383 |
+
### Main Dashboard
|
| 384 |
+

|
| 385 |
+
|
| 386 |
+
*Deploy, manage, and monitor all your MCP servers from one unified interface*
|
| 387 |
+
|
| 388 |
+
---
|
| 389 |
+
|
| 390 |
+
### AI Assistant Chat (Gradio v6 Agentic Chatbot)
|
| 391 |
+

|
| 392 |
+
|
| 393 |
+
*Chat with Claude or SambaNova to create MCP servers using natural language*
|
| 394 |
+
|
| 395 |
+
---
|
| 396 |
+
|
| 397 |
+
### Code Editor
|
| 398 |
+

|
| 399 |
+
|
| 400 |
+
*Edit deployment code with syntax highlighting and live preview*
|
| 401 |
+
|
| 402 |
+
---
|
| 403 |
+
|
| 404 |
+
### Analytics Dashboard
|
| 405 |
+

|
| 406 |
+
|
| 407 |
+
*Real-time analytics showing usage patterns, performance metrics, and cost tracking*
|
| 408 |
+
|
| 409 |
+
---
|
| 410 |
+
|
| 411 |
+
### Security Scan Results
|
| 412 |
+

|
| 413 |
+
|
| 414 |
+
*AI-powered security scanning with detailed vulnerability reports*
|
| 415 |
+
|
| 416 |
+
---
|
| 417 |
+
|
| 418 |
+
## 🚀 Quick Start
|
| 419 |
+
|
| 420 |
+
### Prerequisites
|
| 421 |
+
|
| 422 |
+
```bash
|
| 423 |
+
# Required
|
| 424 |
+
- Python 3.10+
|
| 425 |
+
- Modal account (free tier works)
|
| 426 |
+
- PostgreSQL database (Neon, Supabase, or local)
|
| 427 |
+
|
| 428 |
+
# Optional (for AI features)
|
| 429 |
+
- Anthropic API key (Claude)
|
| 430 |
+
- SambaNova API key (Llama)
|
| 431 |
+
- Nebius API key (Security scanning)
|
| 432 |
+
```
|
| 433 |
+
|
| 434 |
+
### Installation
|
| 435 |
+
|
| 436 |
+
```bash
|
| 437 |
+
# 1. Clone the repository
|
| 438 |
+
git clone https://github.com/yourusername/instant-mcp.git
|
| 439 |
+
cd instant-mcp
|
| 440 |
+
|
| 441 |
+
# 2. Install dependencies
|
| 442 |
+
pip install -r requirements.txt
|
| 443 |
+
|
| 444 |
+
# 3. Set up environment variables
|
| 445 |
+
cp .env.example .env
|
| 446 |
+
# Edit .env with your API keys and database URL
|
| 447 |
+
|
| 448 |
+
# 4. Initialize database
|
| 449 |
+
psql $DATABASE_URL -f tests/init_db.sql
|
| 450 |
+
|
| 451 |
+
# 5. Authenticate with Modal
|
| 452 |
+
modal token new
|
| 453 |
+
```
|
| 454 |
+
|
| 455 |
+
### Running Locally
|
| 456 |
+
|
| 457 |
+
```bash
|
| 458 |
+
# Start the main application
|
| 459 |
+
python app.py
|
| 460 |
+
|
| 461 |
+
# Access at: http://localhost:7860
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
### Environment Variables
|
| 465 |
+
|
| 466 |
+
```bash
|
| 467 |
+
# Database (Required)
|
| 468 |
+
DATABASE_URL=postgresql://user:pass@host:5432/db
|
| 469 |
+
|
| 470 |
+
# AI Providers (Optional - choose at least one)
|
| 471 |
+
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
| 472 |
+
SAMBANOVA_API_KEY=your-key-here
|
| 473 |
+
|
| 474 |
+
# Security Scanning (Recommended)
|
| 475 |
+
NEBIUS_API_KEY=your-nebius-key
|
| 476 |
+
SECURITY_SCANNING_ENABLED=true
|
| 477 |
+
|
| 478 |
+
# Modal (Required for deployment)
|
| 479 |
+
MODAL_TOKEN_ID=your-modal-token
|
| 480 |
+
MODAL_TOKEN_SECRET=your-modal-secret
|
| 481 |
+
|
| 482 |
+
# Application
|
| 483 |
+
PORT=7860
|
| 484 |
+
MCP_BASE_URL=http://localhost:7860
|
| 485 |
+
```
|
| 486 |
+
|
| 487 |
+
---
|
| 488 |
+
|
| 489 |
+
## 🔌 Connecting to Claude Desktop
|
| 490 |
+
|
| 491 |
+
Once you've deployed an MCP server, integrate it with Claude Desktop in 3 simple steps:
|
| 492 |
+
|
| 493 |
+
### Step 1: Get Your Deployment URL
|
| 494 |
+
After deployment, you'll receive a URL like:
|
| 495 |
+
```
|
| 496 |
+
https://your-username--deploy-mcp-weather-abc123.modal.run
|
| 497 |
+
```
|
| 498 |
+
|
| 499 |
+
### Step 2: Add to Claude Desktop Config
|
| 500 |
+
|
| 501 |
+
Open your `claude_desktop_config.json` file and add:
|
| 502 |
+
|
| 503 |
+
```json
|
| 504 |
+
{
|
| 505 |
+
"mcpServers": {
|
| 506 |
+
"your-server-name": {
|
| 507 |
+
"command": "npx",
|
| 508 |
+
"args": [
|
| 509 |
+
"mcp-remote",
|
| 510 |
+
"https://your-deployment-url.modal.run/mcp"
|
| 511 |
+
]
|
| 512 |
+
}
|
| 513 |
+
}
|
| 514 |
+
}
|
| 515 |
+
```
|
| 516 |
+
|
| 517 |
+
**Example:**
|
| 518 |
+
```json
|
| 519 |
+
{
|
| 520 |
+
"mcpServers": {
|
| 521 |
+
"weather-api": {
|
| 522 |
+
"command": "npx",
|
| 523 |
+
"args": [
|
| 524 |
+
"mcp-remote",
|
| 525 |
+
"https://myuser--deploy-mcp-weather-abc123.modal.run/mcp"
|
| 526 |
+
]
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
}
|
| 530 |
+
```
|
| 531 |
+
|
| 532 |
+
### Step 3: Restart Claude Desktop
|
| 533 |
+
|
| 534 |
+
Close and reopen Claude Desktop. Your MCP server tools will now be available!
|
| 535 |
+
|
| 536 |
+
**Config File Locations:**
|
| 537 |
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 538 |
+
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
| 539 |
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
| 540 |
+
|
| 541 |
+
---
|
| 542 |
+
|
| 543 |
+
## 🎓 How to Use
|
| 544 |
+
|
| 545 |
+
### Method 1: AI Assistant (Recommended)
|
| 546 |
+
|
| 547 |
+
1. Navigate to the **🤖 AI Assistant** tab
|
| 548 |
+
2. Choose your AI provider (Claude or SambaNova)
|
| 549 |
+
3. Describe what you want in natural language:
|
| 550 |
+
|
| 551 |
+
```
|
| 552 |
+
"Create an MCP server that fetches current weather
|
| 553 |
+
for any city using the wttr.in API"
|
| 554 |
+
```
|
| 555 |
+
|
| 556 |
+
4. The AI will:
|
| 557 |
+
- Generate the MCP code
|
| 558 |
+
- Scan for security issues
|
| 559 |
+
- Deploy to Modal
|
| 560 |
+
- Return the endpoint URL
|
| 561 |
+
|
| 562 |
+
5. **Connect to Claude Desktop** - Add to your `claude_desktop_config.json`:
|
| 563 |
+
|
| 564 |
+
```json
|
| 565 |
+
{
|
| 566 |
+
"mcpServers": {
|
| 567 |
+
"your-server-name": {
|
| 568 |
+
"command": "npx",
|
| 569 |
+
"args": [
|
| 570 |
+
"mcp-remote",
|
| 571 |
+
"https://your-deployment-url.modal.run/mcp"
|
| 572 |
+
]
|
| 573 |
+
}
|
| 574 |
+
}
|
| 575 |
+
}
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
**Config file location:**
|
| 579 |
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 580 |
+
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
| 581 |
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
| 582 |
+
|
| 583 |
+
After adding the config, restart Claude Desktop to connect to your deployed MCP server!
|
| 584 |
+
|
| 585 |
+
### Method 2: Code Editor
|
| 586 |
+
|
| 587 |
+
1. Go to **💻 Code Editor** tab
|
| 588 |
+
2. Write your MCP code following the FastMCP format:
|
| 589 |
+
|
| 590 |
+
```python
|
| 591 |
+
from fastmcp import FastMCP
|
| 592 |
+
|
| 593 |
+
mcp = FastMCP("my-server")
|
| 594 |
+
|
| 595 |
+
@mcp.tool
|
| 596 |
+
def my_function(param: str) -> str:
|
| 597 |
+
"""Description of what this tool does"""
|
| 598 |
+
return f"Result: {param}"
|
| 599 |
+
```
|
| 600 |
+
|
| 601 |
+
3. Add any required packages
|
| 602 |
+
4. Click **Deploy**
|
| 603 |
+
5. Copy the integration config and add to Claude Desktop (see below)
|
| 604 |
+
|
| 605 |
+
### Method 3: Admin Panel
|
| 606 |
+
|
| 607 |
+
1. Use **⚙️ Admin Panel** for:
|
| 608 |
+
- Quick deployment forms
|
| 609 |
+
- Viewing all deployments
|
| 610 |
+
- Managing existing servers
|
| 611 |
+
- Testing deployed endpoints
|
| 612 |
+
|
| 613 |
+
---
|
| 614 |
+
|
| 615 |
+
## 🏗️ Architecture
|
| 616 |
+
|
| 617 |
+
### Technology Stack Breakdown
|
| 618 |
+
|
| 619 |
+
#### **Frontend: Gradio v6**
|
| 620 |
+
- Tabbed interface for workflows
|
| 621 |
+
- Real-time streaming updates
|
| 622 |
+
- Mobile-responsive design
|
| 623 |
+
- Custom CSS styling
|
| 624 |
+
- Built-in MCP endpoint
|
| 625 |
+
|
| 626 |
+
#### **Backend: FastAPI + SQLAlchemy**
|
| 627 |
+
- RESTful API design
|
| 628 |
+
- PostgreSQL database
|
| 629 |
+
- Connection pooling
|
| 630 |
+
- Transaction management
|
| 631 |
+
- Comprehensive error handling
|
| 632 |
+
|
| 633 |
+
#### **Deployment: Modal**
|
| 634 |
+
- Serverless Python runtime
|
| 635 |
+
- Automatic scaling (including to zero)
|
| 636 |
+
- Cold start optimization
|
| 637 |
+
- HTTPS endpoints
|
| 638 |
+
- Environment variable management
|
| 639 |
+
|
| 640 |
+
#### **AI Integration:**
|
| 641 |
+
|
| 642 |
+
**Claude (Anthropic)** - Primary AI assistant
|
| 643 |
+
- Code generation
|
| 644 |
+
- Natural language processing
|
| 645 |
+
- Tool use for deployment
|
| 646 |
+
- Intelligent debugging
|
| 647 |
+
|
| 648 |
+
**Llama 3.3 70B (SambaNova)** - Cost-effective alternative
|
| 649 |
+
- Same capabilities as Claude
|
| 650 |
+
- Lower cost per token
|
| 651 |
+
- OpenAI-compatible API
|
| 652 |
+
|
| 653 |
+
**Nebius AI** - Security scanning
|
| 654 |
+
- Vulnerability detection
|
| 655 |
+
- Code analysis
|
| 656 |
+
- Threat classification
|
| 657 |
+
- Automated blocking
|
| 658 |
+
|
| 659 |
+
---
|
| 660 |
+
|
| 661 |
+
## 📊 Database Schema
|
| 662 |
+
|
| 663 |
+
```sql
|
| 664 |
+
-- Main deployments table
|
| 665 |
+
CREATE TABLE deployments (
|
| 666 |
+
id SERIAL PRIMARY KEY,
|
| 667 |
+
deployment_id VARCHAR(255) UNIQUE,
|
| 668 |
+
app_name VARCHAR(255),
|
| 669 |
+
server_name VARCHAR(255),
|
| 670 |
+
url TEXT,
|
| 671 |
+
mcp_endpoint TEXT,
|
| 672 |
+
status VARCHAR(50),
|
| 673 |
+
created_at TIMESTAMP,
|
| 674 |
+
-- ... usage stats cached
|
| 675 |
+
);
|
| 676 |
+
|
| 677 |
+
-- Package dependencies
|
| 678 |
+
CREATE TABLE deployment_packages (
|
| 679 |
+
id SERIAL PRIMARY KEY,
|
| 680 |
+
deployment_id VARCHAR(255),
|
| 681 |
+
package_name VARCHAR(255)
|
| 682 |
+
);
|
| 683 |
+
|
| 684 |
+
-- Code storage
|
| 685 |
+
CREATE TABLE deployment_files (
|
| 686 |
+
id SERIAL PRIMARY KEY,
|
| 687 |
+
deployment_id VARCHAR(255),
|
| 688 |
+
file_type VARCHAR(50),
|
| 689 |
+
file_content TEXT
|
| 690 |
+
);
|
| 691 |
+
|
| 692 |
+
-- Audit log
|
| 693 |
+
CREATE TABLE deployment_history (
|
| 694 |
+
id SERIAL PRIMARY KEY,
|
| 695 |
+
deployment_id VARCHAR(255),
|
| 696 |
+
action VARCHAR(100),
|
| 697 |
+
timestamp TIMESTAMP,
|
| 698 |
+
details JSONB
|
| 699 |
+
);
|
| 700 |
+
|
| 701 |
+
-- Detailed usage tracking
|
| 702 |
+
CREATE TABLE usage_events (
|
| 703 |
+
id SERIAL PRIMARY KEY,
|
| 704 |
+
deployment_id VARCHAR(255),
|
| 705 |
+
tool_name VARCHAR(255),
|
| 706 |
+
timestamp TIMESTAMP,
|
| 707 |
+
duration_ms INTEGER,
|
| 708 |
+
success BOOLEAN,
|
| 709 |
+
client_id VARCHAR(255)
|
| 710 |
+
);
|
| 711 |
+
```
|
| 712 |
+
|
| 713 |
+
---
|
| 714 |
+
|
| 715 |
+
## 🎯 Hackathon Highlights
|
| 716 |
+
|
| 717 |
+
### Innovation
|
| 718 |
+
|
| 719 |
+
1. **First MCP-as-a-Service Platform**
|
| 720 |
+
- Deploy MCP servers without infrastructure
|
| 721 |
+
- Managed analytics and monitoring
|
| 722 |
+
- One-click deployment
|
| 723 |
+
|
| 724 |
+
2. **AI-Powered Development Workflow**
|
| 725 |
+
- Natural language to MCP server
|
| 726 |
+
- Automated testing and security
|
| 727 |
+
- Intelligent code optimization
|
| 728 |
+
|
| 729 |
+
3. **Cost Optimization Strategy**
|
| 730 |
+
- Scale to zero (no idle costs)
|
| 731 |
+
- Minimal resource allocation
|
| 732 |
+
- External API integration for token savings
|
| 733 |
+
|
| 734 |
+
### Gradio v6 Features Showcased
|
| 735 |
+
|
| 736 |
+
- ✅ Native MCP server support
|
| 737 |
+
- ✅ Explicit tool registration
|
| 738 |
+
- ✅ Streaming responses
|
| 739 |
+
- ✅ Tabbed interfaces
|
| 740 |
+
- ✅ Mobile responsiveness
|
| 741 |
+
- ✅ FastAPI integration
|
| 742 |
+
- ✅ Custom webhook endpoints
|
| 743 |
+
|
| 744 |
+
### Multi-Sponsor Integration
|
| 745 |
+
|
| 746 |
+
| Sponsor | Integration | Impact |
|
| 747 |
+
|---------|-------------|--------|
|
| 748 |
+
| **Modal** | Deployment platform | Zero infrastructure management |
|
| 749 |
+
| **Anthropic** | Claude AI | Intelligent code generation |
|
| 750 |
+
| **Gradio** | UI framework | Beautiful, functional interface |
|
| 751 |
+
| **Nebius** | Security scanning | Enterprise-grade safety |
|
| 752 |
+
| **SambaNova** | Alternative LLM | Cost-effective AI |
|
| 753 |
+
| **Hugging Face** | Hosting | Easy sharing and deployment |
|
| 754 |
+
|
| 755 |
+
---
|
| 756 |
+
|
| 757 |
+
## 💰 Cost Optimization
|
| 758 |
+
|
| 759 |
+
### Modal Pricing Strategy
|
| 760 |
+
|
| 761 |
+
Our deployments use **minimal resources** to maximize free tier usage:
|
| 762 |
+
|
| 763 |
+
```python
|
| 764 |
+
# Each deployment configured with:
|
| 765 |
+
cpu=0.25 # 1/4 CPU core (cheapest tier)
|
| 766 |
+
memory=256 # 256 MB RAM (minimal)
|
| 767 |
+
scaledown_window=2 # Scale to zero after 2s idle
|
| 768 |
+
timeout=300 # 5 min max execution
|
| 769 |
+
```
|
| 770 |
+
|
| 771 |
+
**Result:** Most users stay within Modal's **$30/month free tier**
|
| 772 |
+
|
| 773 |
+
### Token Cost Savings
|
| 774 |
+
|
| 775 |
+
By deploying specialized MCP servers:
|
| 776 |
+
|
| 777 |
+
| Traditional Approach | With Instant MCP | Savings |
|
| 778 |
+
|---------------------|------------------|---------|
|
| 779 |
+
| Ask Claude for weather | Call weather MCP server | 95% |
|
| 780 |
+
| Claude web search (many tokens) | Perplexity MCP server | 80% |
|
| 781 |
+
| Claude generates frontend | Gemini MCP server | 70% |
|
| 782 |
+
| Repeated API calls via Claude | Cached MCP responses | 90% |
|
| 783 |
+
|
| 784 |
+
**Average savings: 60-80% on token costs**
|
| 785 |
+
|
| 786 |
+
---
|
| 787 |
+
|
| 788 |
+
## 🔒 Security Features
|
| 789 |
+
|
| 790 |
+
### Multi-Layer Protection
|
| 791 |
+
|
| 792 |
+
1. **Pre-Deployment Scanning** (Nebius AI)
|
| 793 |
+
- Analyzes code before deployment
|
| 794 |
+
- Blocks high/critical vulnerabilities
|
| 795 |
+
- Provides detailed explanations
|
| 796 |
+
|
| 797 |
+
2. **Input Validation**
|
| 798 |
+
- Python syntax checking
|
| 799 |
+
- Package name validation
|
| 800 |
+
- Server name sanitization
|
| 801 |
+
|
| 802 |
+
3. **Audit Logging**
|
| 803 |
+
- All actions tracked
|
| 804 |
+
- Security scan results stored
|
| 805 |
+
- Deployment history preserved
|
| 806 |
+
|
| 807 |
+
4. **Safe Defaults**
|
| 808 |
+
- No arbitrary code execution
|
| 809 |
+
- Sandboxed Modal runtime
|
| 810 |
+
- Environment variable isolation
|
| 811 |
+
|
| 812 |
+
---
|
| 813 |
+
|
| 814 |
+
## 🚧 Roadmap
|
| 815 |
+
|
| 816 |
+
### Upcoming Features
|
| 817 |
+
|
| 818 |
+
- [ ] Real-time webhook tracking
|
| 819 |
+
- [ ] Cost tracking dashboard
|
| 820 |
+
- [ ] Export functionality (CSV/JSON)
|
| 821 |
+
- [ ] Multi-user collaboration
|
| 822 |
+
- [ ] Template marketplace
|
| 823 |
+
- [ ] GitHub integration
|
| 824 |
+
- [ ] Automated testing framework
|
| 825 |
+
- [ ] Performance benchmarking
|
| 826 |
+
|
| 827 |
+
---
|
| 828 |
+
|
| 829 |
+
## 📚 Documentation
|
| 830 |
+
|
| 831 |
+
- **Quick Start:** See above
|
| 832 |
+
- **API Reference:** [API.md](./API.md) (coming soon)
|
| 833 |
+
- **Migration Guide:** [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md)
|
| 834 |
+
- **Security Best Practices:** [SECURITY.md](./SECURITY.md) (coming soon)
|
| 835 |
+
|
| 836 |
+
---
|
| 837 |
+
|
| 838 |
+
## 🤝 Contributing
|
| 839 |
+
|
| 840 |
+
We welcome contributions! Areas of interest:
|
| 841 |
+
|
| 842 |
+
- Additional AI provider integrations
|
| 843 |
+
- More visualization options
|
| 844 |
+
- Enhanced security scanning
|
| 845 |
+
- Cost optimization algorithms
|
| 846 |
+
- Template library expansion
|
| 847 |
+
|
| 848 |
+
---
|
| 849 |
+
|
| 850 |
+
## 📄 License
|
| 851 |
+
|
| 852 |
+
MIT License - See [LICENSE](./LICENSE) for details
|
| 853 |
+
|
| 854 |
+
---
|
| 855 |
+
|
| 856 |
+
## 🙏 Acknowledgments
|
| 857 |
+
|
| 858 |
+
Special thanks to:
|
| 859 |
+
|
| 860 |
+
- **Anthropic & Gradio** - For hosting this amazing hackathon
|
| 861 |
+
- **Modal** - For serverless infrastructure
|
| 862 |
+
- **Nebius** - For AI-powered security
|
| 863 |
+
- **SambaNova** - For cost-effective LLM access
|
| 864 |
+
- **Hugging Face** - For hosting and community
|
| 865 |
+
- **FastMCP** - For the excellent MCP framework
|
| 866 |
+
- The entire **MCP community** - For pushing the boundaries of AI tooling
|
| 867 |
+
|
| 868 |
+
---
|
| 869 |
+
|
| 870 |
+
## 🎉 Get Started Now!
|
| 871 |
+
|
| 872 |
+
```bash
|
| 873 |
+
# Install
|
| 874 |
+
git clone https://github.com/yourusername/instant-mcp.git
|
| 875 |
+
cd instant-mcp
|
| 876 |
+
pip install -r requirements.txt
|
| 877 |
+
|
| 878 |
+
# Configure
|
| 879 |
+
cp .env.example .env
|
| 880 |
+
# Add your API keys
|
| 881 |
+
|
| 882 |
+
# Run
|
| 883 |
+
python app.py
|
| 884 |
+
|
| 885 |
+
# Deploy your first MCP server in under 60 seconds! ⚡
|
| 886 |
+
```
|
| 887 |
+
|
| 888 |
+
---
|
| 889 |
+
|
| 890 |
+
<div align="center">
|
| 891 |
+
|
| 892 |
+
**Built with ❤️ for MCP's 1st Birthday Hackathon**
|
| 893 |
+
|
| 894 |
+
[Live Demo](https://huggingface.co/spaces/yourspace/instant-mcp) • [Documentation](./docs) • [Report Bug](https://github.com/yourrepo/issues) • [Request Feature](https://github.com/yourrepo/issues)
|
| 895 |
+
|
| 896 |
+
⭐ **Star this repo if you find it useful!** ⭐
|
| 897 |
+
|
| 898 |
+
</div>
|
app.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Gradio MCP Deployment Platform
|
| 4 |
+
|
| 5 |
+
A unified Gradio application that serves both:
|
| 6 |
+
1. MCP tools via SSE endpoint at /gradio_api/mcp/
|
| 7 |
+
2. Interactive web UI for deployment management and analytics
|
| 8 |
+
|
| 9 |
+
For Gradio Hackathon
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import os
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
from fastapi import FastAPI
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
|
| 18 |
+
# Load environment variables
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
# Import MCP tool functions
|
| 22 |
+
from mcp_tools.deployment_tools import (
|
| 23 |
+
deploy_mcp_server,
|
| 24 |
+
list_deployments,
|
| 25 |
+
get_deployment_status,
|
| 26 |
+
delete_deployment,
|
| 27 |
+
get_deployment_code
|
| 28 |
+
)
|
| 29 |
+
from mcp_tools.stats_tools import (
|
| 30 |
+
get_deployment_stats,
|
| 31 |
+
get_tool_usage,
|
| 32 |
+
get_all_stats_summary
|
| 33 |
+
)
|
| 34 |
+
from mcp_tools.security_tools import scan_deployment_security
|
| 35 |
+
|
| 36 |
+
# Import webhook configuration
|
| 37 |
+
from utils.webhook_receiver import get_webhook_url, is_webhook_enabled
|
| 38 |
+
|
| 39 |
+
# Import all UI components
|
| 40 |
+
from ui_components.admin_panel import create_admin_panel
|
| 41 |
+
from ui_components.code_editor import create_code_editor
|
| 42 |
+
from ui_components.ai_chat_deployment import create_ai_chat_deployment
|
| 43 |
+
from ui_components.stats_dashboard import create_stats_dashboard
|
| 44 |
+
from ui_components.log_viewer import create_log_viewer
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ============================================================================
|
| 48 |
+
# CUSTOM PROFESSIONAL THEME
|
| 49 |
+
# ============================================================================
|
| 50 |
+
class MCPTheme(gr.themes.Base):
|
| 51 |
+
"""Professional theme for MCP Deployment Platform"""
|
| 52 |
+
def __init__(
|
| 53 |
+
self,
|
| 54 |
+
*,
|
| 55 |
+
primary_hue=gr.themes.colors.cyan,
|
| 56 |
+
secondary_hue=gr.themes.colors.emerald,
|
| 57 |
+
neutral_hue=gr.themes.colors.slate,
|
| 58 |
+
spacing_size=gr.themes.sizes.spacing_lg,
|
| 59 |
+
radius_size=gr.themes.sizes.radius_md,
|
| 60 |
+
text_size=gr.themes.sizes.text_md,
|
| 61 |
+
font=(
|
| 62 |
+
gr.themes.GoogleFont("Inter"),
|
| 63 |
+
"ui-sans-serif",
|
| 64 |
+
"system-ui",
|
| 65 |
+
"sans-serif",
|
| 66 |
+
),
|
| 67 |
+
font_mono=(
|
| 68 |
+
gr.themes.GoogleFont("JetBrains Mono"),
|
| 69 |
+
"ui-monospace",
|
| 70 |
+
"monospace",
|
| 71 |
+
),
|
| 72 |
+
):
|
| 73 |
+
super().__init__(
|
| 74 |
+
primary_hue=primary_hue,
|
| 75 |
+
secondary_hue=secondary_hue,
|
| 76 |
+
neutral_hue=neutral_hue,
|
| 77 |
+
spacing_size=spacing_size,
|
| 78 |
+
radius_size=radius_size,
|
| 79 |
+
text_size=text_size,
|
| 80 |
+
font=font,
|
| 81 |
+
font_mono=font_mono,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Create theme instance with professional styling
|
| 85 |
+
mcp_theme = MCPTheme().set(
|
| 86 |
+
# Clean background
|
| 87 |
+
body_background_fill="*neutral_50",
|
| 88 |
+
body_background_fill_dark="*neutral_900",
|
| 89 |
+
# Modern buttons with cyan-to-emerald gradient
|
| 90 |
+
button_primary_background_fill="linear-gradient(135deg, *primary_600, *secondary_600)",
|
| 91 |
+
button_primary_background_fill_hover="linear-gradient(135deg, *primary_500, *secondary_500)",
|
| 92 |
+
button_primary_text_color="white",
|
| 93 |
+
button_primary_background_fill_dark="linear-gradient(135deg, *primary_700, *secondary_700)",
|
| 94 |
+
# Clean blocks
|
| 95 |
+
block_background_fill="white",
|
| 96 |
+
block_background_fill_dark="*neutral_800",
|
| 97 |
+
block_border_width="1px",
|
| 98 |
+
block_label_text_weight="600",
|
| 99 |
+
# Input styling
|
| 100 |
+
input_background_fill="*neutral_50",
|
| 101 |
+
input_background_fill_dark="*neutral_900",
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Custom CSS for better styling
|
| 105 |
+
custom_css = """
|
| 106 |
+
/* Main container */
|
| 107 |
+
.gradio-container {
|
| 108 |
+
max-width: 1400px !important;
|
| 109 |
+
margin: 0 auto !important;
|
| 110 |
+
padding: 2rem 1rem !important;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Header styling */
|
| 114 |
+
.header-section {
|
| 115 |
+
text-align: center;
|
| 116 |
+
margin-bottom: 2.5rem;
|
| 117 |
+
padding: 2rem 1rem;
|
| 118 |
+
background: linear-gradient(135deg, #06b6d4 0%, #10b981 100%);
|
| 119 |
+
border-radius: 16px;
|
| 120 |
+
color: white;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.header-title {
|
| 124 |
+
font-size: 2.75rem;
|
| 125 |
+
font-weight: 800;
|
| 126 |
+
margin-bottom: 0.75rem;
|
| 127 |
+
letter-spacing: -0.02em;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.header-subtitle {
|
| 131 |
+
font-size: 1.125rem;
|
| 132 |
+
opacity: 0.95;
|
| 133 |
+
max-width: 700px;
|
| 134 |
+
margin: 0 auto;
|
| 135 |
+
line-height: 1.6;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* Tab styling */
|
| 139 |
+
.tab-nav {
|
| 140 |
+
border-bottom: 2px solid #e5e7eb;
|
| 141 |
+
margin-bottom: 1.5rem;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.tab-nav button {
|
| 145 |
+
font-weight: 600;
|
| 146 |
+
font-size: 0.95rem;
|
| 147 |
+
padding: 0.75rem 1.5rem;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* Footer styling */
|
| 151 |
+
.footer-section {
|
| 152 |
+
margin-top: 3rem;
|
| 153 |
+
padding-top: 2rem;
|
| 154 |
+
border-top: 1px solid #e5e7eb;
|
| 155 |
+
text-align: center;
|
| 156 |
+
color: #6b7280;
|
| 157 |
+
font-size: 0.875rem;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.footer-section code {
|
| 161 |
+
background: #f3f4f6;
|
| 162 |
+
padding: 0.25rem 0.5rem;
|
| 163 |
+
border-radius: 4px;
|
| 164 |
+
font-size: 0.875rem;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Improve spacing */
|
| 168 |
+
.block {
|
| 169 |
+
margin-bottom: 1rem;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Button improvements */
|
| 173 |
+
button {
|
| 174 |
+
transition: all 0.2s ease;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
button:hover {
|
| 178 |
+
transform: translateY(-1px);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* ============================================
|
| 182 |
+
TAB CONTENT CONSISTENCY FIX
|
| 183 |
+
============================================ */
|
| 184 |
+
|
| 185 |
+
/* Ensure all tab panels have consistent sizing */
|
| 186 |
+
.tabitem {
|
| 187 |
+
width: 100% !important;
|
| 188 |
+
max-width: 100% !important;
|
| 189 |
+
min-height: 600px !important;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* Ensure tab content fills the space properly */
|
| 193 |
+
.tabitem > .block,
|
| 194 |
+
.tabitem > div {
|
| 195 |
+
width: 100% !important;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Fix for nested Blocks within tabs */
|
| 199 |
+
.tabitem .gradio-container {
|
| 200 |
+
max-width: 100% !important;
|
| 201 |
+
padding: 1rem !important;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* Ensure rows stay full width */
|
| 205 |
+
.tabitem .row {
|
| 206 |
+
width: 100% !important;
|
| 207 |
+
gap: 1rem !important;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* Ensure columns don't collapse */
|
| 211 |
+
.tabitem .column {
|
| 212 |
+
min-width: 0 !important;
|
| 213 |
+
flex: 1 1 auto !important;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Prevent content from shrinking */
|
| 217 |
+
.tabitem .wrap {
|
| 218 |
+
width: 100% !important;
|
| 219 |
+
}
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
# Create main application with tabs
|
| 223 |
+
with gr.Blocks(title="Instant MCP - AI-Powered MCP Deployment") as gradio_app:
|
| 224 |
+
# Modern Header
|
| 225 |
+
with gr.Row(elem_classes="header-section"):
|
| 226 |
+
with gr.Column():
|
| 227 |
+
gr.Markdown(
|
| 228 |
+
'<div class="header-title">⚡ Instant MCP</div>',
|
| 229 |
+
elem_classes="header-title"
|
| 230 |
+
)
|
| 231 |
+
gr.Markdown(
|
| 232 |
+
'<div class="header-subtitle">From Idea to Production in Seconds • AI-powered deployment platform to build, deploy, and scale your MCP servers instantly</div>',
|
| 233 |
+
elem_classes="header-subtitle"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Tabbed interface
|
| 237 |
+
with gr.Tabs():
|
| 238 |
+
# Admin Panel Tab - Main deployment management
|
| 239 |
+
with gr.Tab("⚙️ Admin Panel"):
|
| 240 |
+
admin_panel = create_admin_panel()
|
| 241 |
+
|
| 242 |
+
# Code Editor Tab - Edit deployment code
|
| 243 |
+
with gr.Tab("💻 Code Editor"):
|
| 244 |
+
code_editor = create_code_editor()
|
| 245 |
+
|
| 246 |
+
# AI Assistant Tab - Chat interface for AI-powered deployment
|
| 247 |
+
with gr.Tab("🤖 AI Assistant"):
|
| 248 |
+
ai_chat = create_ai_chat_deployment()
|
| 249 |
+
|
| 250 |
+
# Stats Dashboard Tab - Analytics and visualizations
|
| 251 |
+
with gr.Tab("📊 Statistics"):
|
| 252 |
+
stats_dashboard = create_stats_dashboard()
|
| 253 |
+
|
| 254 |
+
# Log Viewer Tab - Deployment history and events
|
| 255 |
+
with gr.Tab("📝 Logs"):
|
| 256 |
+
log_viewer = create_log_viewer()
|
| 257 |
+
|
| 258 |
+
# Professional Footer
|
| 259 |
+
with gr.Row(elem_classes="footer-section"):
|
| 260 |
+
with gr.Column():
|
| 261 |
+
gr.Markdown(
|
| 262 |
+
"""
|
| 263 |
+
**MCP SSE Endpoint**: `/gradio_api/mcp/` •
|
| 264 |
+
**Documentation**: [Model Context Protocol](https://github.com/modelcontextprotocol) •
|
| 265 |
+
Built with [Gradio](https://gradio.app)
|
| 266 |
+
"""
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# ============================================================================
|
| 270 |
+
# EXPLICIT MCP TOOL REGISTRATION
|
| 271 |
+
# ============================================================================
|
| 272 |
+
# Explicitly register ONLY the intended MCP tools (prevents UI handlers from being exposed)
|
| 273 |
+
# All UI event handlers now use show_api=False to prevent exposure
|
| 274 |
+
|
| 275 |
+
# Deployment Management Tools
|
| 276 |
+
gr.api(deploy_mcp_server, api_name="deploy_mcp_server")
|
| 277 |
+
gr.api(list_deployments, api_name="list_deployments")
|
| 278 |
+
gr.api(get_deployment_status, api_name="get_deployment_status")
|
| 279 |
+
gr.api(delete_deployment, api_name="delete_deployment")
|
| 280 |
+
gr.api(get_deployment_code, api_name="get_deployment_code")
|
| 281 |
+
|
| 282 |
+
# Statistics Tools
|
| 283 |
+
gr.api(get_deployment_stats, api_name="get_deployment_stats")
|
| 284 |
+
gr.api(get_tool_usage, api_name="get_tool_usage")
|
| 285 |
+
gr.api(get_all_stats_summary, api_name="get_all_stats_summary")
|
| 286 |
+
|
| 287 |
+
# Security Tools
|
| 288 |
+
gr.api(scan_deployment_security, api_name="scan_deployment_security")
|
| 289 |
+
|
| 290 |
+
# Total tools registered (for logging)
|
| 291 |
+
total_tools = 9
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
# ============================================================================
|
| 295 |
+
# WEBHOOK ENDPOINT SETUP
|
| 296 |
+
# ============================================================================
|
| 297 |
+
|
| 298 |
+
# Create a custom FastAPI app for webhook routes
|
| 299 |
+
from fastapi import Request
|
| 300 |
+
from starlette.responses import JSONResponse
|
| 301 |
+
|
| 302 |
+
fastapi_app = FastAPI(title="MCP Deployment Platform API")
|
| 303 |
+
|
| 304 |
+
@fastapi_app.post("/api/webhook/usage")
|
| 305 |
+
async def webhook_usage(request: Request):
|
| 306 |
+
"""Simple webhook endpoint - just stores the data"""
|
| 307 |
+
try:
|
| 308 |
+
data = await request.json()
|
| 309 |
+
|
| 310 |
+
from utils.database import db_transaction
|
| 311 |
+
from utils.models import UsageEvent
|
| 312 |
+
|
| 313 |
+
with db_transaction() as db:
|
| 314 |
+
UsageEvent.record_usage(
|
| 315 |
+
db=db,
|
| 316 |
+
deployment_id=data.get('deployment_id'),
|
| 317 |
+
tool_name=data.get('tool_name'),
|
| 318 |
+
duration_ms=data.get('duration_ms'),
|
| 319 |
+
success=data.get('success', True),
|
| 320 |
+
error_message=data.get('error'),
|
| 321 |
+
metadata={'source': 'webhook'}
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
return JSONResponse({"success": True})
|
| 325 |
+
except Exception as e:
|
| 326 |
+
return JSONResponse({"success": False, "error": str(e)})
|
| 327 |
+
|
| 328 |
+
@fastapi_app.get("/api/webhook/status")
|
| 329 |
+
async def webhook_status():
|
| 330 |
+
"""Get webhook endpoint status and configuration"""
|
| 331 |
+
return JSONResponse({
|
| 332 |
+
"webhook_enabled": is_webhook_enabled(),
|
| 333 |
+
"webhook_url": get_webhook_url(),
|
| 334 |
+
"message": "Webhook endpoint is active" if is_webhook_enabled() else "Webhook endpoint is disabled"
|
| 335 |
+
})
|
| 336 |
+
|
| 337 |
+
# Mount Gradio app onto FastAPI with MCP server enabled
|
| 338 |
+
app = gr.mount_gradio_app(
|
| 339 |
+
fastapi_app,
|
| 340 |
+
gradio_app,
|
| 341 |
+
path="/",
|
| 342 |
+
mcp_server=True,
|
| 343 |
+
theme=mcp_theme,
|
| 344 |
+
css=custom_css
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# Launch configuration
|
| 349 |
+
if __name__ == "__main__":
|
| 350 |
+
import uvicorn
|
| 351 |
+
|
| 352 |
+
# Get port from environment (HF Spaces uses PORT=7860)
|
| 353 |
+
port = int(os.environ.get("PORT", 7860))
|
| 354 |
+
|
| 355 |
+
# Startup banner
|
| 356 |
+
print("=" * 70)
|
| 357 |
+
print("🚀 MCP Deployment Platform")
|
| 358 |
+
print("=" * 70)
|
| 359 |
+
print(f"📊 Web UI: http://0.0.0.0:{port}")
|
| 360 |
+
print(f"📡 MCP Endpoint: http://0.0.0.0:{port}/gradio_api/mcp")
|
| 361 |
+
print(f"📚 API Docs: http://0.0.0.0:{port}/docs")
|
| 362 |
+
print(f"🔗 Webhook Endpoint: http://0.0.0.0:{port}/api/webhook/usage")
|
| 363 |
+
print("=" * 70)
|
| 364 |
+
print(f"\n✅ Registered {total_tools} MCP tool endpoints")
|
| 365 |
+
print("\n🎯 MCP Tools Available:")
|
| 366 |
+
print(" • deploy_mcp_server")
|
| 367 |
+
print(" • list_deployments")
|
| 368 |
+
print(" • get_deployment_status")
|
| 369 |
+
print(" • delete_deployment")
|
| 370 |
+
print(" • get_deployment_code")
|
| 371 |
+
print(" • get_deployment_stats")
|
| 372 |
+
print(" • get_tool_usage")
|
| 373 |
+
print(" • get_all_stats_summary")
|
| 374 |
+
print(" • scan_deployment_security")
|
| 375 |
+
print("\n💡 Connect via MCP client:")
|
| 376 |
+
print(f' Claude Desktop: {{"url": "http://localhost:{port}/gradio_api/mcp"}}')
|
| 377 |
+
print("\n🎨 Web UI Features:")
|
| 378 |
+
print(" • Admin Panel: Deploy & manage servers")
|
| 379 |
+
print(" • Code Editor: View & edit deployment code")
|
| 380 |
+
print(" • AI Assistant: Chat with Claude to create/modify MCPs")
|
| 381 |
+
print(" • Statistics: Analytics & visualizations")
|
| 382 |
+
print(" • Logs: Deployment history & events")
|
| 383 |
+
print("=" * 70)
|
| 384 |
+
|
| 385 |
+
# Run with uvicorn for production deployment
|
| 386 |
+
# 'app' is the FastAPI app with Gradio mounted
|
| 387 |
+
uvicorn.run(
|
| 388 |
+
app,
|
| 389 |
+
host="0.0.0.0",
|
| 390 |
+
port=port,
|
| 391 |
+
log_level="info"
|
| 392 |
+
)
|
| 393 |
+
|
claude.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
If at anytime context7 does not work, use this command to reininstall gradio : claude mcp add --transport http context7 https://mcp.context7.com/mcp --header "CONTEXT7_API_KEY: ctx7sk-4964416d-5670-454c-99fe-6e086b8af715"
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
Do not create markdown files unnecessarily. Only create them when explicitly asked to do so.
|
mcp_tools/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Tools Module
|
| 3 |
+
|
| 4 |
+
This module contains all MCP tool definitions converted to Gradio format.
|
| 5 |
+
Each tool is exposed via the Gradio MCP endpoint at /gradio_api/mcp/
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .deployment_tools import _create_deployment_tools
|
| 9 |
+
from .stats_tools import _create_stats_tools
|
| 10 |
+
from .security_tools import _create_security_tools
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
'_create_deployment_tools',
|
| 14 |
+
'_create_stats_tools',
|
| 15 |
+
'_create_security_tools',
|
| 16 |
+
]
|
mcp_tools/ai_assistant.py
ADDED
|
@@ -0,0 +1,1084 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI Assistant Helper Module (Enhanced with Tool Use)
|
| 3 |
+
|
| 4 |
+
Handles Claude API interactions for MCP code generation, modification, and debugging.
|
| 5 |
+
Now includes tool-use capability to actually deploy, manage, and interact with MCP servers.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
from typing import List, Dict, Optional, Generator, Any, Callable
|
| 11 |
+
from anthropic import Anthropic
|
| 12 |
+
import openai
|
| 13 |
+
|
| 14 |
+
# Import MCP deployment tools for tool execution
|
| 15 |
+
from mcp_tools.deployment_tools import (
|
| 16 |
+
deploy_mcp_server,
|
| 17 |
+
list_deployments,
|
| 18 |
+
get_deployment_status,
|
| 19 |
+
delete_deployment,
|
| 20 |
+
get_deployment_code,
|
| 21 |
+
update_deployment_code,
|
| 22 |
+
)
|
| 23 |
+
from mcp_tools.stats_tools import (
|
| 24 |
+
get_deployment_stats,
|
| 25 |
+
get_tool_usage,
|
| 26 |
+
get_all_stats_summary,
|
| 27 |
+
)
|
| 28 |
+
from mcp_tools.security_tools import scan_deployment_security
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# =============================================================================
|
| 32 |
+
# TOOL DEFINITIONS FOR CLAUDE
|
| 33 |
+
# =============================================================================
|
| 34 |
+
# These match the MCP tools available in the platform
|
| 35 |
+
|
| 36 |
+
TOOL_DEFINITIONS = [
|
| 37 |
+
{
|
| 38 |
+
"name": "deploy_mcp_server",
|
| 39 |
+
"description": """Deploy an MCP server with custom tools to Modal.com.
|
| 40 |
+
|
| 41 |
+
The deployed server will:
|
| 42 |
+
- Use minimal CPU (0.25 cores) and memory (256MB)
|
| 43 |
+
- Scale to zero when not in use (no billing when idle)
|
| 44 |
+
- Allow cold starts (2-5 second startup time)
|
| 45 |
+
- Be accessible via a public URL
|
| 46 |
+
|
| 47 |
+
IMPORTANT CODE FORMAT REQUIREMENTS:
|
| 48 |
+
Your mcp_tools_code MUST include:
|
| 49 |
+
✅ `from fastmcp import FastMCP` import
|
| 50 |
+
✅ `mcp = FastMCP("server-name")` initialization
|
| 51 |
+
✅ One or more `@mcp.tool()` decorated functions
|
| 52 |
+
✅ Docstrings for each tool (used as descriptions)
|
| 53 |
+
✅ Type hints for parameters and return values
|
| 54 |
+
|
| 55 |
+
❌ DO NOT include:
|
| 56 |
+
❌ `mcp.run()` or any server startup code
|
| 57 |
+
❌ `if __name__ == "__main__"` blocks
|
| 58 |
+
❌ Modal-specific imports or setup
|
| 59 |
+
|
| 60 |
+
Example code:
|
| 61 |
+
```python
|
| 62 |
+
from fastmcp import FastMCP
|
| 63 |
+
|
| 64 |
+
mcp = FastMCP("cat-facts")
|
| 65 |
+
|
| 66 |
+
@mcp.tool()
|
| 67 |
+
def get_cat_fact() -> str:
|
| 68 |
+
'''Get a random cat fact from an API'''
|
| 69 |
+
import requests
|
| 70 |
+
response = requests.get("https://catfact.ninja/fact")
|
| 71 |
+
return response.json()["fact"]
|
| 72 |
+
```""",
|
| 73 |
+
"input_schema": {
|
| 74 |
+
"type": "object",
|
| 75 |
+
"properties": {
|
| 76 |
+
"server_name": {
|
| 77 |
+
"type": "string",
|
| 78 |
+
"description": "Unique name for your MCP server (e.g., 'weather-api', 'cat-facts')"
|
| 79 |
+
},
|
| 80 |
+
"mcp_tools_code": {
|
| 81 |
+
"type": "string",
|
| 82 |
+
"description": "Complete MCP server code as a string with FastMCP import, initialization, and @mcp.tool() decorated functions"
|
| 83 |
+
},
|
| 84 |
+
"extra_pip_packages": {
|
| 85 |
+
"type": "string",
|
| 86 |
+
"description": "Comma-separated list of PyPI packages (e.g., 'requests,pandas')"
|
| 87 |
+
},
|
| 88 |
+
"description": {
|
| 89 |
+
"type": "string",
|
| 90 |
+
"description": "Human-readable description of what the server does"
|
| 91 |
+
},
|
| 92 |
+
"category": {
|
| 93 |
+
"type": "string",
|
| 94 |
+
"description": "Category for organizing (e.g., 'Weather', 'Finance', 'Utilities')"
|
| 95 |
+
},
|
| 96 |
+
"tags": {
|
| 97 |
+
"type": "array",
|
| 98 |
+
"items": {"type": "string"},
|
| 99 |
+
"description": "List of tags for filtering and search"
|
| 100 |
+
},
|
| 101 |
+
"author": {
|
| 102 |
+
"type": "string",
|
| 103 |
+
"description": "Author name"
|
| 104 |
+
},
|
| 105 |
+
"version": {
|
| 106 |
+
"type": "string",
|
| 107 |
+
"description": "Semantic version (e.g., '1.0.0')"
|
| 108 |
+
}
|
| 109 |
+
},
|
| 110 |
+
"required": ["server_name", "mcp_tools_code"]
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"name": "list_deployments",
|
| 115 |
+
"description": "List all deployed MCP servers with their details including URLs, status, and usage statistics.",
|
| 116 |
+
"input_schema": {
|
| 117 |
+
"type": "object",
|
| 118 |
+
"properties": {},
|
| 119 |
+
"required": []
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"name": "get_deployment_status",
|
| 124 |
+
"description": "Get detailed status of a deployed MCP server including live status check.",
|
| 125 |
+
"input_schema": {
|
| 126 |
+
"type": "object",
|
| 127 |
+
"properties": {
|
| 128 |
+
"deployment_id": {
|
| 129 |
+
"type": "string",
|
| 130 |
+
"description": "The deployment ID (e.g., 'deploy-mcp-weather-abc123')"
|
| 131 |
+
},
|
| 132 |
+
"app_name": {
|
| 133 |
+
"type": "string",
|
| 134 |
+
"description": "Or the Modal app name"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"required": []
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"name": "delete_deployment",
|
| 142 |
+
"description": "Delete a deployed MCP server from Modal. Requires confirmation.",
|
| 143 |
+
"input_schema": {
|
| 144 |
+
"type": "object",
|
| 145 |
+
"properties": {
|
| 146 |
+
"deployment_id": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "The deployment ID to delete"
|
| 149 |
+
},
|
| 150 |
+
"app_name": {
|
| 151 |
+
"type": "string",
|
| 152 |
+
"description": "Or the Modal app name to delete"
|
| 153 |
+
},
|
| 154 |
+
"confirm": {
|
| 155 |
+
"type": "boolean",
|
| 156 |
+
"description": "Must be true to confirm deletion"
|
| 157 |
+
}
|
| 158 |
+
},
|
| 159 |
+
"required": ["confirm"]
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"name": "get_deployment_code",
|
| 164 |
+
"description": "Get the current MCP tools code for a deployment. Use this to view existing code before modifying it.",
|
| 165 |
+
"input_schema": {
|
| 166 |
+
"type": "object",
|
| 167 |
+
"properties": {
|
| 168 |
+
"deployment_id": {
|
| 169 |
+
"type": "string",
|
| 170 |
+
"description": "The deployment ID"
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"required": ["deployment_id"]
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"name": "update_deployment_code",
|
| 178 |
+
"description": """Update deployment code and/or packages with redeployment to Modal.
|
| 179 |
+
|
| 180 |
+
This will redeploy the MCP server with new code/packages while preserving the same URL.
|
| 181 |
+
The deployment will experience brief downtime (5-10 seconds) during the update.
|
| 182 |
+
|
| 183 |
+
Workflow:
|
| 184 |
+
1. First use get_deployment_code() to get the current code
|
| 185 |
+
2. Make your modifications
|
| 186 |
+
3. Use this function to deploy the changes""",
|
| 187 |
+
"input_schema": {
|
| 188 |
+
"type": "object",
|
| 189 |
+
"properties": {
|
| 190 |
+
"deployment_id": {
|
| 191 |
+
"type": "string",
|
| 192 |
+
"description": "The deployment ID to update"
|
| 193 |
+
},
|
| 194 |
+
"mcp_tools_code": {
|
| 195 |
+
"type": "string",
|
| 196 |
+
"description": "New MCP tools code (triggers redeployment)"
|
| 197 |
+
},
|
| 198 |
+
"extra_pip_packages": {
|
| 199 |
+
"type": "array",
|
| 200 |
+
"items": {"type": "string"},
|
| 201 |
+
"description": "New package list (triggers redeployment)"
|
| 202 |
+
},
|
| 203 |
+
"server_name": {
|
| 204 |
+
"type": "string",
|
| 205 |
+
"description": "New server name"
|
| 206 |
+
},
|
| 207 |
+
"description": {
|
| 208 |
+
"type": "string",
|
| 209 |
+
"description": "New description"
|
| 210 |
+
}
|
| 211 |
+
},
|
| 212 |
+
"required": ["deployment_id"]
|
| 213 |
+
}
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"name": "get_deployment_stats",
|
| 217 |
+
"description": "Get usage statistics for a specific deployment including request counts and response times.",
|
| 218 |
+
"input_schema": {
|
| 219 |
+
"type": "object",
|
| 220 |
+
"properties": {
|
| 221 |
+
"deployment_id": {
|
| 222 |
+
"type": "string",
|
| 223 |
+
"description": "The deployment ID to get stats for"
|
| 224 |
+
},
|
| 225 |
+
"days": {
|
| 226 |
+
"type": "integer",
|
| 227 |
+
"description": "Number of days to look back (default: 30)"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"required": ["deployment_id"]
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"name": "get_tool_usage",
|
| 235 |
+
"description": "Get breakdown of tool usage for a deployment - which tools are being called most.",
|
| 236 |
+
"input_schema": {
|
| 237 |
+
"type": "object",
|
| 238 |
+
"properties": {
|
| 239 |
+
"deployment_id": {
|
| 240 |
+
"type": "string",
|
| 241 |
+
"description": "The deployment ID"
|
| 242 |
+
},
|
| 243 |
+
"days": {
|
| 244 |
+
"type": "integer",
|
| 245 |
+
"description": "Number of days to look back (default: 30)"
|
| 246 |
+
},
|
| 247 |
+
"limit": {
|
| 248 |
+
"type": "integer",
|
| 249 |
+
"description": "Maximum number of tools to return (default: 10)"
|
| 250 |
+
}
|
| 251 |
+
},
|
| 252 |
+
"required": ["deployment_id"]
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"name": "get_all_stats_summary",
|
| 257 |
+
"description": "Get quick statistics summary for all deployments.",
|
| 258 |
+
"input_schema": {
|
| 259 |
+
"type": "object",
|
| 260 |
+
"properties": {},
|
| 261 |
+
"required": []
|
| 262 |
+
}
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"name": "scan_deployment_security",
|
| 266 |
+
"description": """Manually scan MCP code for security vulnerabilities WITHOUT deploying.
|
| 267 |
+
|
| 268 |
+
Use this to check code for security issues before deploying. The scan detects:
|
| 269 |
+
- Code injection vulnerabilities (SQL, command, etc.)
|
| 270 |
+
- Malicious network behavior
|
| 271 |
+
- Resource abuse patterns
|
| 272 |
+
- Destructive operations
|
| 273 |
+
- Known malicious packages""",
|
| 274 |
+
"input_schema": {
|
| 275 |
+
"type": "object",
|
| 276 |
+
"properties": {
|
| 277 |
+
"mcp_tools_code": {
|
| 278 |
+
"type": "string",
|
| 279 |
+
"description": "Python code defining your MCP tools"
|
| 280 |
+
},
|
| 281 |
+
"server_name": {
|
| 282 |
+
"type": "string",
|
| 283 |
+
"description": "Name for context"
|
| 284 |
+
},
|
| 285 |
+
"extra_pip_packages": {
|
| 286 |
+
"type": "array",
|
| 287 |
+
"items": {"type": "string"},
|
| 288 |
+
"description": "Additional pip packages to check"
|
| 289 |
+
},
|
| 290 |
+
"description": {
|
| 291 |
+
"type": "string",
|
| 292 |
+
"description": "Optional description for context"
|
| 293 |
+
}
|
| 294 |
+
},
|
| 295 |
+
"required": ["mcp_tools_code"]
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
]
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
# =============================================================================
|
| 302 |
+
# TOOL EXECUTION MAPPING
|
| 303 |
+
# =============================================================================
|
| 304 |
+
|
| 305 |
+
def execute_tool(tool_name: str, tool_input: Dict[str, Any]) -> Dict[str, Any]:
|
| 306 |
+
"""
|
| 307 |
+
Execute a tool by name with given input parameters.
|
| 308 |
+
|
| 309 |
+
Args:
|
| 310 |
+
tool_name: Name of the tool to execute
|
| 311 |
+
tool_input: Dictionary of input parameters
|
| 312 |
+
|
| 313 |
+
Returns:
|
| 314 |
+
Tool execution result as a dictionary
|
| 315 |
+
"""
|
| 316 |
+
tool_map: Dict[str, Callable] = {
|
| 317 |
+
"deploy_mcp_server": _execute_deploy_mcp_server,
|
| 318 |
+
"list_deployments": _execute_list_deployments,
|
| 319 |
+
"get_deployment_status": _execute_get_deployment_status,
|
| 320 |
+
"delete_deployment": _execute_delete_deployment,
|
| 321 |
+
"get_deployment_code": _execute_get_deployment_code,
|
| 322 |
+
"update_deployment_code": _execute_update_deployment_code,
|
| 323 |
+
"get_deployment_stats": _execute_get_deployment_stats,
|
| 324 |
+
"get_tool_usage": _execute_get_tool_usage,
|
| 325 |
+
"get_all_stats_summary": _execute_get_all_stats_summary,
|
| 326 |
+
"scan_deployment_security": _execute_scan_deployment_security,
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
if tool_name not in tool_map:
|
| 330 |
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
| 331 |
+
|
| 332 |
+
try:
|
| 333 |
+
return tool_map[tool_name](tool_input)
|
| 334 |
+
except Exception as e:
|
| 335 |
+
return {"success": False, "error": f"Tool execution error: {str(e)}"}
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def _execute_deploy_mcp_server(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 339 |
+
"""Execute deploy_mcp_server with parameters"""
|
| 340 |
+
return deploy_mcp_server(
|
| 341 |
+
server_name=params.get("server_name", ""),
|
| 342 |
+
mcp_tools_code=params.get("mcp_tools_code", ""),
|
| 343 |
+
extra_pip_packages=params.get("extra_pip_packages", ""),
|
| 344 |
+
description=params.get("description", ""),
|
| 345 |
+
category=params.get("category", "Uncategorized"),
|
| 346 |
+
tags=params.get("tags", []),
|
| 347 |
+
author=params.get("author", "AI Assistant"),
|
| 348 |
+
version=params.get("version", "1.0.0"),
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _execute_list_deployments(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 353 |
+
"""Execute list_deployments"""
|
| 354 |
+
return list_deployments()
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def _execute_get_deployment_status(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 358 |
+
"""Execute get_deployment_status with parameters"""
|
| 359 |
+
return get_deployment_status(
|
| 360 |
+
deployment_id=params.get("deployment_id", ""),
|
| 361 |
+
app_name=params.get("app_name", ""),
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def _execute_delete_deployment(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 366 |
+
"""Execute delete_deployment with parameters"""
|
| 367 |
+
return delete_deployment(
|
| 368 |
+
deployment_id=params.get("deployment_id", ""),
|
| 369 |
+
app_name=params.get("app_name", ""),
|
| 370 |
+
confirm=params.get("confirm", False),
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def _execute_get_deployment_code(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 375 |
+
"""Execute get_deployment_code with parameters"""
|
| 376 |
+
return get_deployment_code(
|
| 377 |
+
deployment_id=params.get("deployment_id", ""),
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def _execute_update_deployment_code(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 382 |
+
"""Execute update_deployment_code with parameters"""
|
| 383 |
+
return update_deployment_code(
|
| 384 |
+
deployment_id=params.get("deployment_id", ""),
|
| 385 |
+
mcp_tools_code=params.get("mcp_tools_code"),
|
| 386 |
+
extra_pip_packages=params.get("extra_pip_packages"),
|
| 387 |
+
server_name=params.get("server_name"),
|
| 388 |
+
description=params.get("description"),
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _execute_get_deployment_stats(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 393 |
+
"""Execute get_deployment_stats with parameters"""
|
| 394 |
+
return get_deployment_stats(
|
| 395 |
+
deployment_id=params.get("deployment_id", ""),
|
| 396 |
+
days=params.get("days", 30),
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _execute_get_tool_usage(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 401 |
+
"""Execute get_tool_usage with parameters"""
|
| 402 |
+
return get_tool_usage(
|
| 403 |
+
deployment_id=params.get("deployment_id", ""),
|
| 404 |
+
days=params.get("days", 30),
|
| 405 |
+
limit=params.get("limit", 10),
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _execute_get_all_stats_summary(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 410 |
+
"""Execute get_all_stats_summary"""
|
| 411 |
+
return get_all_stats_summary()
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def _execute_scan_deployment_security(params: Dict[str, Any]) -> Dict[str, Any]:
|
| 415 |
+
"""Execute scan_deployment_security with parameters"""
|
| 416 |
+
return scan_deployment_security(
|
| 417 |
+
mcp_tools_code=params.get("mcp_tools_code", ""),
|
| 418 |
+
server_name=params.get("server_name", "Unknown"),
|
| 419 |
+
extra_pip_packages=params.get("extra_pip_packages", []),
|
| 420 |
+
description=params.get("description"),
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
# =============================================================================
|
| 425 |
+
# SYSTEM PROMPT
|
| 426 |
+
# =============================================================================
|
| 427 |
+
|
| 428 |
+
SYSTEM_PROMPT = """You are an MCP server deployment assistant with FULL ACCESS to deployment tools. You can actually deploy, modify, and manage MCP servers - not just generate code.
|
| 429 |
+
|
| 430 |
+
AVAILABLE TOOLS:
|
| 431 |
+
You have access to the following tools that you can call to perform actual operations:
|
| 432 |
+
|
| 433 |
+
1. **deploy_mcp_server** - Deploy new MCP servers to Modal.com
|
| 434 |
+
2. **list_deployments** - List all deployed servers
|
| 435 |
+
3. **get_deployment_status** - Check status of a deployment
|
| 436 |
+
4. **delete_deployment** - Delete a deployment (requires confirmation)
|
| 437 |
+
5. **get_deployment_code** - Get the current code for a deployment
|
| 438 |
+
6. **update_deployment_code** - Update code/packages and redeploy
|
| 439 |
+
7. **get_deployment_stats** - Get usage statistics
|
| 440 |
+
8. **get_tool_usage** - See which tools are being used most
|
| 441 |
+
9. **get_all_stats_summary** - Overview of all deployments
|
| 442 |
+
10. **scan_deployment_security** - Scan code for vulnerabilities before deploying
|
| 443 |
+
|
| 444 |
+
WORKFLOW GUIDELINES:
|
| 445 |
+
|
| 446 |
+
When creating a new MCP server:
|
| 447 |
+
1. Understand the user's requirements
|
| 448 |
+
2. Generate the MCP code following the correct format
|
| 449 |
+
3. Optionally scan for security issues first using scan_deployment_security
|
| 450 |
+
4. Call deploy_mcp_server with the code to actually deploy it
|
| 451 |
+
5. Report the deployment URL back to the user
|
| 452 |
+
|
| 453 |
+
When modifying an existing server:
|
| 454 |
+
1. Call list_deployments to find the deployment
|
| 455 |
+
2. Call get_deployment_code to get the current code
|
| 456 |
+
3. Make the requested modifications
|
| 457 |
+
4. Call update_deployment_code to deploy the changes
|
| 458 |
+
5. Report the results
|
| 459 |
+
|
| 460 |
+
CODE FORMAT REQUIREMENTS:
|
| 461 |
+
When generating MCP code, it MUST include:
|
| 462 |
+
✅ `from fastmcp import FastMCP` import
|
| 463 |
+
✅ `mcp = FastMCP("server-name")` initialization
|
| 464 |
+
✅ One or more `@mcp.tool()` decorated functions
|
| 465 |
+
✅ Docstrings for each tool
|
| 466 |
+
✅ Type hints for parameters and return values
|
| 467 |
+
|
| 468 |
+
❌ DO NOT include:
|
| 469 |
+
❌ `mcp.run()` or any server startup code
|
| 470 |
+
❌ `if __name__ == "__main__"` blocks
|
| 471 |
+
|
| 472 |
+
EXAMPLE MCP CODE:
|
| 473 |
+
```python
|
| 474 |
+
from fastmcp import FastMCP
|
| 475 |
+
import requests
|
| 476 |
+
|
| 477 |
+
mcp = FastMCP("weather-api")
|
| 478 |
+
|
| 479 |
+
@mcp.tool()
|
| 480 |
+
def get_weather(city: str) -> dict:
|
| 481 |
+
'''Get current weather for a city.
|
| 482 |
+
|
| 483 |
+
Args:
|
| 484 |
+
city: Name of the city
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
Weather data including temperature and conditions
|
| 488 |
+
'''
|
| 489 |
+
try:
|
| 490 |
+
response = requests.get(
|
| 491 |
+
f"https://wttr.in/{city}?format=j1",
|
| 492 |
+
timeout=10
|
| 493 |
+
)
|
| 494 |
+
response.raise_for_status()
|
| 495 |
+
data = response.json()
|
| 496 |
+
current = data["current_condition"][0]
|
| 497 |
+
return {
|
| 498 |
+
"city": city,
|
| 499 |
+
"temperature_c": current["temp_C"],
|
| 500 |
+
"description": current["weatherDesc"][0]["value"],
|
| 501 |
+
"humidity": current["humidity"]
|
| 502 |
+
}
|
| 503 |
+
except Exception as e:
|
| 504 |
+
return {"error": str(e), "city": city}
|
| 505 |
+
```
|
| 506 |
+
|
| 507 |
+
SECURITY GUIDELINES:
|
| 508 |
+
- Always validate and sanitize user inputs
|
| 509 |
+
- Use timeouts on HTTP requests
|
| 510 |
+
- Never execute arbitrary code from user input
|
| 511 |
+
- Use environment variables for API keys: `os.getenv('API_KEY', 'default')`
|
| 512 |
+
- Handle exceptions gracefully
|
| 513 |
+
|
| 514 |
+
IMPORTANT: You can and should USE THE TOOLS to actually perform operations. Don't just show code - deploy it when the user asks!"""
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
# =============================================================================
|
| 518 |
+
# MCP ASSISTANT CLASS WITH TOOL USE
|
| 519 |
+
# =============================================================================
|
| 520 |
+
|
| 521 |
+
class MCPAssistant:
|
| 522 |
+
"""Helper class for AI-assisted MCP development with tool-use capability"""
|
| 523 |
+
|
| 524 |
+
def __init__(self, provider: str = "anthropic", model: str = None, api_key: Optional[str] = None):
|
| 525 |
+
"""
|
| 526 |
+
Initialize the MCP Assistant with support for multiple AI providers.
|
| 527 |
+
|
| 528 |
+
Args:
|
| 529 |
+
provider: AI provider ("anthropic" or "sambanova")
|
| 530 |
+
model: Model name (provider-specific)
|
| 531 |
+
api_key: API key (required for Anthropic, ignored for SambaNova which uses env var)
|
| 532 |
+
"""
|
| 533 |
+
self.provider = provider.lower()
|
| 534 |
+
|
| 535 |
+
if self.provider == "anthropic":
|
| 536 |
+
if not api_key:
|
| 537 |
+
raise ValueError("API key is required for Anthropic provider")
|
| 538 |
+
self.client = Anthropic(api_key=api_key)
|
| 539 |
+
self.model = model or "claude-sonnet-4-20250514"
|
| 540 |
+
|
| 541 |
+
elif self.provider == "sambanova":
|
| 542 |
+
sambanova_api_key = os.getenv("SAMBANOVA_API_KEY")
|
| 543 |
+
if not sambanova_api_key:
|
| 544 |
+
raise ValueError("SAMBANOVA_API_KEY not found in environment variables")
|
| 545 |
+
|
| 546 |
+
sambanova_base_url = os.getenv("SAMBANOVA_BASE_URL", "https://api.sambanova.ai/v1")
|
| 547 |
+
|
| 548 |
+
self.client = openai.OpenAI(
|
| 549 |
+
base_url=sambanova_base_url,
|
| 550 |
+
api_key=sambanova_api_key
|
| 551 |
+
)
|
| 552 |
+
self.model = model or "Meta-Llama-3.3-70B-Instruct"
|
| 553 |
+
|
| 554 |
+
else:
|
| 555 |
+
raise ValueError(f"Unsupported provider: {provider}. Use 'anthropic' or 'sambanova'")
|
| 556 |
+
|
| 557 |
+
def _convert_tools_to_openai_format(self, anthropic_tools: List[Dict]) -> List[Dict]:
|
| 558 |
+
"""
|
| 559 |
+
Convert Anthropic tool format to OpenAI tool format.
|
| 560 |
+
|
| 561 |
+
Args:
|
| 562 |
+
anthropic_tools: Tools in Anthropic format
|
| 563 |
+
|
| 564 |
+
Returns:
|
| 565 |
+
Tools in OpenAI format
|
| 566 |
+
"""
|
| 567 |
+
openai_tools = []
|
| 568 |
+
for tool in anthropic_tools:
|
| 569 |
+
openai_tool = {
|
| 570 |
+
"type": "function",
|
| 571 |
+
"function": {
|
| 572 |
+
"name": tool["name"],
|
| 573 |
+
"description": tool["description"],
|
| 574 |
+
"parameters": tool["input_schema"]
|
| 575 |
+
}
|
| 576 |
+
}
|
| 577 |
+
openai_tools.append(openai_tool)
|
| 578 |
+
return openai_tools
|
| 579 |
+
|
| 580 |
+
def chat_stream(
|
| 581 |
+
self,
|
| 582 |
+
message: str,
|
| 583 |
+
history: List[Dict[str, str]] = None,
|
| 584 |
+
max_tokens: int = 4096
|
| 585 |
+
) -> Generator[str, None, None]:
|
| 586 |
+
"""
|
| 587 |
+
Stream chat responses with tool-use support for multiple providers.
|
| 588 |
+
|
| 589 |
+
This method handles the full conversation including tool calls.
|
| 590 |
+
When the AI wants to use a tool, it executes the tool and continues
|
| 591 |
+
the conversation with the results.
|
| 592 |
+
|
| 593 |
+
Args:
|
| 594 |
+
message: User message
|
| 595 |
+
history: Chat history in Gradio format [{role, content}]
|
| 596 |
+
max_tokens: Maximum tokens to generate
|
| 597 |
+
|
| 598 |
+
Yields:
|
| 599 |
+
Streamed response chunks
|
| 600 |
+
"""
|
| 601 |
+
if self.provider == "anthropic":
|
| 602 |
+
yield from self._chat_stream_anthropic(message, history, max_tokens)
|
| 603 |
+
elif self.provider == "sambanova":
|
| 604 |
+
yield from self._chat_stream_sambanova(message, history, max_tokens)
|
| 605 |
+
else:
|
| 606 |
+
yield f"❌ Error: Unsupported provider {self.provider}"
|
| 607 |
+
|
| 608 |
+
def _chat_stream_anthropic(
|
| 609 |
+
self,
|
| 610 |
+
message: str,
|
| 611 |
+
history: List[Dict[str, str]] = None,
|
| 612 |
+
max_tokens: int = 4096
|
| 613 |
+
) -> Generator[str, None, None]:
|
| 614 |
+
"""Anthropic-specific streaming implementation with real-time streaming"""
|
| 615 |
+
# Convert Gradio history to Anthropic format
|
| 616 |
+
messages = []
|
| 617 |
+
if history:
|
| 618 |
+
for msg in history:
|
| 619 |
+
role = msg.get("role")
|
| 620 |
+
content = msg.get("content")
|
| 621 |
+
if role and content:
|
| 622 |
+
messages.append({"role": role, "content": content})
|
| 623 |
+
|
| 624 |
+
# Add current message
|
| 625 |
+
messages.append({"role": "user", "content": message})
|
| 626 |
+
|
| 627 |
+
try:
|
| 628 |
+
# Initial call with tools
|
| 629 |
+
full_response = ""
|
| 630 |
+
tool_calls_made = []
|
| 631 |
+
|
| 632 |
+
while True:
|
| 633 |
+
# Make STREAMING API call with tools
|
| 634 |
+
with self.client.messages.stream(
|
| 635 |
+
model=self.model,
|
| 636 |
+
max_tokens=max_tokens,
|
| 637 |
+
system=SYSTEM_PROMPT,
|
| 638 |
+
tools=TOOL_DEFINITIONS,
|
| 639 |
+
messages=messages,
|
| 640 |
+
) as stream:
|
| 641 |
+
assistant_content = []
|
| 642 |
+
current_text = ""
|
| 643 |
+
tool_uses = []
|
| 644 |
+
|
| 645 |
+
# Stream the response in real-time
|
| 646 |
+
for event in stream:
|
| 647 |
+
# Handle different event types
|
| 648 |
+
if event.type == "content_block_start":
|
| 649 |
+
if hasattr(event, 'content_block') and event.content_block.type == "text":
|
| 650 |
+
current_text = ""
|
| 651 |
+
|
| 652 |
+
elif event.type == "content_block_delta":
|
| 653 |
+
if hasattr(event, 'delta'):
|
| 654 |
+
if event.delta.type == "text_delta":
|
| 655 |
+
# Stream text deltas in real-time!
|
| 656 |
+
text_chunk = event.delta.text
|
| 657 |
+
current_text += text_chunk
|
| 658 |
+
full_response += text_chunk
|
| 659 |
+
yield text_chunk # Real-time streaming!
|
| 660 |
+
|
| 661 |
+
elif event.type == "content_block_stop":
|
| 662 |
+
if event.content_block.type == "text":
|
| 663 |
+
assistant_content.append({
|
| 664 |
+
"type": "text",
|
| 665 |
+
"text": current_text
|
| 666 |
+
})
|
| 667 |
+
elif event.content_block.type == "tool_use":
|
| 668 |
+
tool_uses.append(event.content_block)
|
| 669 |
+
assistant_content.append({
|
| 670 |
+
"type": "tool_use",
|
| 671 |
+
"id": event.content_block.id,
|
| 672 |
+
"name": event.content_block.name,
|
| 673 |
+
"input": event.content_block.input
|
| 674 |
+
})
|
| 675 |
+
|
| 676 |
+
response = stream.get_final_message()
|
| 677 |
+
|
| 678 |
+
# Check stop reason
|
| 679 |
+
if response.stop_reason == "end_turn":
|
| 680 |
+
# Normal completion - we already streamed the text
|
| 681 |
+
break
|
| 682 |
+
|
| 683 |
+
elif response.stop_reason == "tool_use":
|
| 684 |
+
# Claude wants to use tools (already extracted in stream loop above)
|
| 685 |
+
# Add assistant message with tool uses
|
| 686 |
+
messages.append({
|
| 687 |
+
"role": "assistant",
|
| 688 |
+
"content": assistant_content
|
| 689 |
+
})
|
| 690 |
+
|
| 691 |
+
# Execute tools and collect results
|
| 692 |
+
tool_results = []
|
| 693 |
+
for tool_use in tool_uses:
|
| 694 |
+
# Show tool execution status
|
| 695 |
+
tool_status = f"\n\n🔧 **Executing: {tool_use.name}**\n"
|
| 696 |
+
yield tool_status
|
| 697 |
+
full_response += tool_status
|
| 698 |
+
|
| 699 |
+
# Execute the tool
|
| 700 |
+
result = execute_tool(tool_use.name, tool_use.input)
|
| 701 |
+
tool_calls_made.append({
|
| 702 |
+
"tool": tool_use.name,
|
| 703 |
+
"input": tool_use.input,
|
| 704 |
+
"result": result
|
| 705 |
+
})
|
| 706 |
+
|
| 707 |
+
# Show result summary
|
| 708 |
+
if result.get("success"):
|
| 709 |
+
if result.get("url"):
|
| 710 |
+
result_summary = f"✅ Success! URL: {result.get('url')}\n"
|
| 711 |
+
elif result.get("total") is not None:
|
| 712 |
+
result_summary = f"✅ Found {result.get('total')} deployment(s)\n"
|
| 713 |
+
else:
|
| 714 |
+
result_summary = "✅ Success!\n"
|
| 715 |
+
else:
|
| 716 |
+
result_summary = f"❌ Error: {result.get('error', 'Unknown error')}\n"
|
| 717 |
+
|
| 718 |
+
yield result_summary
|
| 719 |
+
full_response += result_summary
|
| 720 |
+
|
| 721 |
+
tool_results.append({
|
| 722 |
+
"type": "tool_result",
|
| 723 |
+
"tool_use_id": tool_use.id,
|
| 724 |
+
"content": json.dumps(result, indent=2)
|
| 725 |
+
})
|
| 726 |
+
|
| 727 |
+
# Add tool results to messages
|
| 728 |
+
messages.append({
|
| 729 |
+
"role": "user",
|
| 730 |
+
"content": tool_results
|
| 731 |
+
})
|
| 732 |
+
|
| 733 |
+
# Continue the conversation
|
| 734 |
+
continue
|
| 735 |
+
|
| 736 |
+
else:
|
| 737 |
+
# Unexpected stop reason
|
| 738 |
+
for block in response.content:
|
| 739 |
+
if hasattr(block, 'text'):
|
| 740 |
+
yield block.text
|
| 741 |
+
break
|
| 742 |
+
|
| 743 |
+
except Exception as e:
|
| 744 |
+
yield f"\n\n❌ Error: {str(e)}\n\nPlease check your API key and try again."
|
| 745 |
+
|
| 746 |
+
def _chat_stream_sambanova(
|
| 747 |
+
self,
|
| 748 |
+
message: str,
|
| 749 |
+
history: List[Dict[str, str]] = None,
|
| 750 |
+
max_tokens: int = 4096
|
| 751 |
+
) -> Generator[str, None, None]:
|
| 752 |
+
"""SambaNova (OpenAI-compatible) streaming implementation with real-time streaming"""
|
| 753 |
+
# Convert Gradio history to OpenAI format
|
| 754 |
+
messages = []
|
| 755 |
+
if history:
|
| 756 |
+
for msg in history:
|
| 757 |
+
role = msg.get("role")
|
| 758 |
+
content = msg.get("content")
|
| 759 |
+
# SambaNova requires content to be a string, not None or list
|
| 760 |
+
if role and content and isinstance(content, str):
|
| 761 |
+
messages.append({"role": role, "content": content})
|
| 762 |
+
|
| 763 |
+
# Add current message
|
| 764 |
+
messages.append({"role": "user", "content": message})
|
| 765 |
+
|
| 766 |
+
# Convert tools to OpenAI format
|
| 767 |
+
openai_tools = self._convert_tools_to_openai_format(TOOL_DEFINITIONS)
|
| 768 |
+
|
| 769 |
+
try:
|
| 770 |
+
full_response = ""
|
| 771 |
+
max_iterations = 10 # Prevent infinite loops
|
| 772 |
+
iteration = 0
|
| 773 |
+
|
| 774 |
+
while iteration < max_iterations:
|
| 775 |
+
iteration += 1
|
| 776 |
+
|
| 777 |
+
# Make STREAMING API call with tools
|
| 778 |
+
stream = self.client.chat.completions.create(
|
| 779 |
+
model=self.model,
|
| 780 |
+
messages=messages,
|
| 781 |
+
tools=openai_tools,
|
| 782 |
+
max_tokens=max_tokens,
|
| 783 |
+
stream=True, # Enable streaming!
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
# Collect streaming response
|
| 787 |
+
assistant_content = ""
|
| 788 |
+
tool_calls_data = []
|
| 789 |
+
current_tool_call = None
|
| 790 |
+
|
| 791 |
+
for chunk in stream:
|
| 792 |
+
delta = chunk.choices[0].delta
|
| 793 |
+
|
| 794 |
+
# Stream text content in real-time
|
| 795 |
+
if delta.content:
|
| 796 |
+
assistant_content += delta.content
|
| 797 |
+
full_response += delta.content
|
| 798 |
+
yield delta.content # Real-time streaming!
|
| 799 |
+
|
| 800 |
+
# Collect tool calls
|
| 801 |
+
if delta.tool_calls:
|
| 802 |
+
for tc_delta in delta.tool_calls:
|
| 803 |
+
if tc_delta.index is not None:
|
| 804 |
+
# Start new tool call or update existing
|
| 805 |
+
while len(tool_calls_data) <= tc_delta.index:
|
| 806 |
+
tool_calls_data.append({
|
| 807 |
+
"id": "",
|
| 808 |
+
"type": "function",
|
| 809 |
+
"function": {"name": "", "arguments": ""}
|
| 810 |
+
})
|
| 811 |
+
|
| 812 |
+
if tc_delta.id:
|
| 813 |
+
tool_calls_data[tc_delta.index]["id"] = tc_delta.id
|
| 814 |
+
if tc_delta.function:
|
| 815 |
+
if tc_delta.function.name:
|
| 816 |
+
tool_calls_data[tc_delta.index]["function"]["name"] = tc_delta.function.name
|
| 817 |
+
if tc_delta.function.arguments:
|
| 818 |
+
tool_calls_data[tc_delta.index]["function"]["arguments"] += tc_delta.function.arguments
|
| 819 |
+
|
| 820 |
+
# Check if there are tool calls
|
| 821 |
+
if tool_calls_data:
|
| 822 |
+
# Add assistant message to history
|
| 823 |
+
messages.append({
|
| 824 |
+
"role": "assistant",
|
| 825 |
+
"content": assistant_content,
|
| 826 |
+
"tool_calls": tool_calls_data
|
| 827 |
+
})
|
| 828 |
+
|
| 829 |
+
# Execute each tool call
|
| 830 |
+
for tool_call_data in tool_calls_data:
|
| 831 |
+
tool_name = tool_call_data["function"]["name"]
|
| 832 |
+
tool_args = json.loads(tool_call_data["function"]["arguments"])
|
| 833 |
+
|
| 834 |
+
# Show tool execution status
|
| 835 |
+
tool_status = f"\n\n🔧 **Executing: {tool_name}**\n"
|
| 836 |
+
yield tool_status
|
| 837 |
+
full_response += tool_status
|
| 838 |
+
|
| 839 |
+
# Execute the tool
|
| 840 |
+
result = execute_tool(tool_name, tool_args)
|
| 841 |
+
|
| 842 |
+
# Show result summary
|
| 843 |
+
if result.get("success"):
|
| 844 |
+
if result.get("url"):
|
| 845 |
+
result_summary = f"✅ Success! URL: {result.get('url')}\n"
|
| 846 |
+
elif result.get("total") is not None:
|
| 847 |
+
result_summary = f"✅ Found {result.get('total')} deployment(s)\n"
|
| 848 |
+
else:
|
| 849 |
+
result_summary = "✅ Success!\n"
|
| 850 |
+
else:
|
| 851 |
+
result_summary = f"❌ Error: {result.get('error', 'Unknown error')}\n"
|
| 852 |
+
|
| 853 |
+
yield result_summary
|
| 854 |
+
full_response += result_summary
|
| 855 |
+
|
| 856 |
+
# Add tool result to messages
|
| 857 |
+
messages.append({
|
| 858 |
+
"role": "tool",
|
| 859 |
+
"tool_call_id": tool_call_data["id"],
|
| 860 |
+
"content": json.dumps(result, indent=2)
|
| 861 |
+
})
|
| 862 |
+
|
| 863 |
+
# Continue the loop to get the next response
|
| 864 |
+
continue
|
| 865 |
+
|
| 866 |
+
else:
|
| 867 |
+
# No tool calls - final response (already streamed above)
|
| 868 |
+
break
|
| 869 |
+
|
| 870 |
+
if iteration >= max_iterations:
|
| 871 |
+
yield f"\n\n⚠️ Warning: Maximum iterations ({max_iterations}) reached. Stopping."
|
| 872 |
+
|
| 873 |
+
except Exception as e:
|
| 874 |
+
yield f"\n\n❌ Error: {str(e)}\n\nPlease check your SambaNova configuration and try again."
|
| 875 |
+
|
| 876 |
+
def chat_with_tools(
|
| 877 |
+
self,
|
| 878 |
+
message: str,
|
| 879 |
+
history: List[Dict[str, str]] = None,
|
| 880 |
+
max_tokens: int = 4096
|
| 881 |
+
) -> Dict[str, Any]:
|
| 882 |
+
"""
|
| 883 |
+
Non-streaming chat with tool-use support.
|
| 884 |
+
|
| 885 |
+
Returns the complete response with tool call information.
|
| 886 |
+
|
| 887 |
+
Args:
|
| 888 |
+
message: User message
|
| 889 |
+
history: Chat history
|
| 890 |
+
max_tokens: Maximum tokens
|
| 891 |
+
|
| 892 |
+
Returns:
|
| 893 |
+
dict with response text, tool calls made, and any deployments created
|
| 894 |
+
"""
|
| 895 |
+
# Collect full response from stream
|
| 896 |
+
full_response = ""
|
| 897 |
+
for chunk in self.chat_stream(message, history, max_tokens):
|
| 898 |
+
full_response += chunk
|
| 899 |
+
|
| 900 |
+
return {
|
| 901 |
+
"success": True,
|
| 902 |
+
"response": full_response
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
def generate_mcp_code(
|
| 906 |
+
self,
|
| 907 |
+
description: str,
|
| 908 |
+
context: Optional[Dict] = None
|
| 909 |
+
) -> Dict[str, Any]:
|
| 910 |
+
"""
|
| 911 |
+
Generate MCP code from a description (legacy method for compatibility).
|
| 912 |
+
|
| 913 |
+
Args:
|
| 914 |
+
description: What the MCP server should do
|
| 915 |
+
context: Optional context (existing code, error logs, etc.)
|
| 916 |
+
|
| 917 |
+
Returns:
|
| 918 |
+
dict with code, packages, category, tags, and explanation
|
| 919 |
+
"""
|
| 920 |
+
prompt = f"Create an MCP server that: {description}"
|
| 921 |
+
|
| 922 |
+
if context:
|
| 923 |
+
if context.get("existing_code"):
|
| 924 |
+
prompt += f"\n\nExisting code to modify:\n```python\n{context['existing_code']}\n```"
|
| 925 |
+
if context.get("error"):
|
| 926 |
+
prompt += f"\n\nError to fix:\n{context['error']}"
|
| 927 |
+
if context.get("packages"):
|
| 928 |
+
prompt += f"\n\nCurrent packages: {', '.join(context['packages'])}"
|
| 929 |
+
|
| 930 |
+
try:
|
| 931 |
+
response = self.client.messages.create(
|
| 932 |
+
model=self.model,
|
| 933 |
+
max_tokens=4096,
|
| 934 |
+
system=SYSTEM_PROMPT,
|
| 935 |
+
messages=[{"role": "user", "content": prompt}],
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
response_text = response.content[0].text
|
| 939 |
+
|
| 940 |
+
# Parse the response
|
| 941 |
+
result = self._parse_response(response_text)
|
| 942 |
+
return {
|
| 943 |
+
"success": True,
|
| 944 |
+
**result
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
except Exception as e:
|
| 948 |
+
return {
|
| 949 |
+
"success": False,
|
| 950 |
+
"error": f"Failed to generate code: {str(e)}"
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
def _parse_response(self, response_text: str) -> Dict[str, Any]:
|
| 954 |
+
"""
|
| 955 |
+
Parse Claude's response to extract code, packages, category, and tags.
|
| 956 |
+
|
| 957 |
+
Args:
|
| 958 |
+
response_text: Raw response from Claude
|
| 959 |
+
|
| 960 |
+
Returns:
|
| 961 |
+
dict with parsed components
|
| 962 |
+
"""
|
| 963 |
+
result = {
|
| 964 |
+
"code": "",
|
| 965 |
+
"packages": [],
|
| 966 |
+
"category": "Uncategorized",
|
| 967 |
+
"tags": [],
|
| 968 |
+
"explanation": ""
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
# Extract code block
|
| 972 |
+
import re
|
| 973 |
+
code_match = re.search(r'```python\n(.*?)\n```', response_text, re.DOTALL)
|
| 974 |
+
if code_match:
|
| 975 |
+
result["code"] = code_match.group(1).strip()
|
| 976 |
+
|
| 977 |
+
# Extract packages
|
| 978 |
+
packages_match = re.search(r'\*\*Packages:\*\*\s*(.+)', response_text, re.IGNORECASE)
|
| 979 |
+
if packages_match:
|
| 980 |
+
packages_str = packages_match.group(1).strip()
|
| 981 |
+
result["packages"] = [p.strip() for p in packages_str.split(",") if p.strip()]
|
| 982 |
+
|
| 983 |
+
# Extract category
|
| 984 |
+
category_match = re.search(r'\*\*Category:\*\*\s*(.+)', response_text, re.IGNORECASE)
|
| 985 |
+
if category_match:
|
| 986 |
+
result["category"] = category_match.group(1).strip()
|
| 987 |
+
|
| 988 |
+
# Extract tags
|
| 989 |
+
tags_match = re.search(r'\*\*Tags:\*\*\s*(.+)', response_text, re.IGNORECASE)
|
| 990 |
+
if tags_match:
|
| 991 |
+
tags_str = tags_match.group(1).strip()
|
| 992 |
+
result["tags"] = [t.strip() for t in tags_str.split(",") if t.strip()]
|
| 993 |
+
|
| 994 |
+
# Explanation is everything before the code block
|
| 995 |
+
if code_match:
|
| 996 |
+
result["explanation"] = response_text[:code_match.start()].strip()
|
| 997 |
+
else:
|
| 998 |
+
result["explanation"] = response_text.strip()
|
| 999 |
+
|
| 1000 |
+
return result
|
| 1001 |
+
|
| 1002 |
+
def review_code(self, code: str) -> Dict[str, Any]:
|
| 1003 |
+
"""
|
| 1004 |
+
Review MCP code for security and best practices.
|
| 1005 |
+
|
| 1006 |
+
Args:
|
| 1007 |
+
code: Python code to review
|
| 1008 |
+
|
| 1009 |
+
Returns:
|
| 1010 |
+
dict with review results
|
| 1011 |
+
"""
|
| 1012 |
+
prompt = f"""Review this MCP server code for:
|
| 1013 |
+
1. Security vulnerabilities
|
| 1014 |
+
2. Error handling
|
| 1015 |
+
3. Best practices
|
| 1016 |
+
4. Potential improvements
|
| 1017 |
+
|
| 1018 |
+
Code:
|
| 1019 |
+
```python
|
| 1020 |
+
{code}
|
| 1021 |
+
```
|
| 1022 |
+
|
| 1023 |
+
Provide a concise review with specific suggestions."""
|
| 1024 |
+
|
| 1025 |
+
try:
|
| 1026 |
+
response = self.client.messages.create(
|
| 1027 |
+
model=self.model,
|
| 1028 |
+
max_tokens=2048,
|
| 1029 |
+
system=SYSTEM_PROMPT,
|
| 1030 |
+
messages=[{"role": "user", "content": prompt}],
|
| 1031 |
+
)
|
| 1032 |
+
|
| 1033 |
+
return {
|
| 1034 |
+
"success": True,
|
| 1035 |
+
"review": response.content[0].text
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
except Exception as e:
|
| 1039 |
+
return {
|
| 1040 |
+
"success": False,
|
| 1041 |
+
"error": f"Failed to review code: {str(e)}"
|
| 1042 |
+
}
|
| 1043 |
+
|
| 1044 |
+
|
| 1045 |
+
def validate_api_key(api_key: str) -> bool:
|
| 1046 |
+
"""
|
| 1047 |
+
Validate Anthropic API key.
|
| 1048 |
+
|
| 1049 |
+
Args:
|
| 1050 |
+
api_key: API key to validate
|
| 1051 |
+
|
| 1052 |
+
Returns:
|
| 1053 |
+
True if valid, False otherwise
|
| 1054 |
+
"""
|
| 1055 |
+
if not api_key or not api_key.startswith("sk-ant-"):
|
| 1056 |
+
return False
|
| 1057 |
+
|
| 1058 |
+
try:
|
| 1059 |
+
client = Anthropic(api_key=api_key)
|
| 1060 |
+
# Try a minimal API call
|
| 1061 |
+
client.messages.create(
|
| 1062 |
+
model="claude-sonnet-4-20250514",
|
| 1063 |
+
max_tokens=10,
|
| 1064 |
+
messages=[{"role": "user", "content": "Hi"}],
|
| 1065 |
+
)
|
| 1066 |
+
return True
|
| 1067 |
+
except Exception:
|
| 1068 |
+
return False
|
| 1069 |
+
|
| 1070 |
+
|
| 1071 |
+
def validate_sambanova_env() -> tuple[bool, str]:
|
| 1072 |
+
"""
|
| 1073 |
+
Validate SambaNova configuration from environment.
|
| 1074 |
+
|
| 1075 |
+
Returns:
|
| 1076 |
+
Tuple of (is_valid, message)
|
| 1077 |
+
"""
|
| 1078 |
+
api_key = os.getenv("SAMBANOVA_API_KEY")
|
| 1079 |
+
if not api_key:
|
| 1080 |
+
return False, "SAMBANOVA_API_KEY not found in environment variables"
|
| 1081 |
+
|
| 1082 |
+
# Optional: Could test the API key here with a minimal call
|
| 1083 |
+
# For now, just check if it exists
|
| 1084 |
+
return True, "SambaNova configuration found"
|
mcp_tools/deployment_tools.py
ADDED
|
@@ -0,0 +1,1855 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deployment Tools Module
|
| 3 |
+
|
| 4 |
+
Gradio-based MCP tools for deployment management.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import subprocess
|
| 12 |
+
import tempfile
|
| 13 |
+
import hashlib
|
| 14 |
+
import shutil
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import List, Optional
|
| 18 |
+
|
| 19 |
+
# Database imports
|
| 20 |
+
from utils.database import db_transaction, get_db
|
| 21 |
+
from utils.models import (
|
| 22 |
+
Deployment,
|
| 23 |
+
DeploymentPackage,
|
| 24 |
+
DeploymentFile,
|
| 25 |
+
DeploymentHistory,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Modal wrapper template (same as server.py)
|
| 29 |
+
# Note: Tracking code removed - will be developed later
|
| 30 |
+
MODAL_WRAPPER_TEMPLATE = '''#!/usr/bin/env python3
|
| 31 |
+
"""
|
| 32 |
+
Auto-generated Modal deployment for MCP Server: {app_name}
|
| 33 |
+
Generated at: {timestamp}
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
import modal
|
| 37 |
+
import os
|
| 38 |
+
|
| 39 |
+
# App configuration with minimal resources and cold starts allowed
|
| 40 |
+
app = modal.App("{app_name}")
|
| 41 |
+
|
| 42 |
+
# Image with required dependencies
|
| 43 |
+
image = modal.Image.debian_slim(python_version="3.12").pip_install(
|
| 44 |
+
"fastapi==0.115.14",
|
| 45 |
+
"fastmcp>=2.10.0",
|
| 46 |
+
"pydantic>=2.0.0",
|
| 47 |
+
"requests>=2.28.0",
|
| 48 |
+
"uvicorn>=0.20.0",
|
| 49 |
+
"python-dotenv>=1.0.0", # For environment variable management
|
| 50 |
+
{extra_deps}
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Create secrets from environment variables
|
| 54 |
+
# This allows deployed functions to access API keys and other secrets
|
| 55 |
+
secrets_dict = {{}}
|
| 56 |
+
{env_vars_setup}
|
| 57 |
+
|
| 58 |
+
# Add webhook configuration to secrets
|
| 59 |
+
{webhook_env_vars}
|
| 60 |
+
|
| 61 |
+
# Create Modal secret from environment variables (if any)
|
| 62 |
+
app_secrets = []
|
| 63 |
+
if secrets_dict:
|
| 64 |
+
app_secrets = [modal.Secret.from_dict(secrets_dict)]
|
| 65 |
+
|
| 66 |
+
def make_mcp_server():
|
| 67 |
+
"""Create the MCP server with user-defined tools"""
|
| 68 |
+
from dotenv import load_dotenv
|
| 69 |
+
import os
|
| 70 |
+
|
| 71 |
+
# Load environment variables from .env file (if present)
|
| 72 |
+
load_dotenv()
|
| 73 |
+
|
| 74 |
+
# ============================================================================
|
| 75 |
+
# ⚠️ USER CODE FORMAT (FastMCP Official Pattern)
|
| 76 |
+
# ============================================================================
|
| 77 |
+
# Your tool code MUST follow the standard FastMCP pattern:
|
| 78 |
+
#
|
| 79 |
+
# from fastmcp import FastMCP
|
| 80 |
+
# mcp = FastMCP("server-name")
|
| 81 |
+
#
|
| 82 |
+
# @mcp.tool # ✅ Preferred (no parentheses)
|
| 83 |
+
# def my_tool(param: str) -> str:
|
| 84 |
+
# \"\"\"Tool description\"\"\"
|
| 85 |
+
# return f"Result: {{param}}"
|
| 86 |
+
#
|
| 87 |
+
# The deployment wrapper:
|
| 88 |
+
# - Uses your code AS-IS (no stripping or modification)
|
| 89 |
+
# - Handles Modal deployment and HTTP transport
|
| 90 |
+
# - Manages environment variables and secrets
|
| 91 |
+
# ============================================================================
|
| 92 |
+
|
| 93 |
+
# ============================================================================
|
| 94 |
+
# CONFIGURATION BEST PRACTICE
|
| 95 |
+
# ============================================================================
|
| 96 |
+
# For API keys and configurable values in your tools, use environment vars:
|
| 97 |
+
#
|
| 98 |
+
# import os
|
| 99 |
+
# API_KEY = os.getenv('YOUR_API_KEY_NAME', 'fallback_default_value')
|
| 100 |
+
# BASE_URL = os.getenv('API_BASE_URL', 'https://api.example.com')
|
| 101 |
+
#
|
| 102 |
+
# Benefits:
|
| 103 |
+
# - Update values in Modal settings/secrets without code changes
|
| 104 |
+
# - Keep secrets out of version control
|
| 105 |
+
# - Safe fallback values for development
|
| 106 |
+
#
|
| 107 |
+
# Example in your @mcp.tool functions:
|
| 108 |
+
#
|
| 109 |
+
# @mcp.tool
|
| 110 |
+
# def fetch_data(query: str) -> dict:
|
| 111 |
+
# import os
|
| 112 |
+
# API_KEY = os.getenv('EXTERNAL_API_KEY', 'demo_key_12345')
|
| 113 |
+
# # Use API_KEY in your code...
|
| 114 |
+
# ============================================================================
|
| 115 |
+
|
| 116 |
+
# === USER-DEFINED TOOLS START ===
|
| 117 |
+
# User's code includes: from fastmcp import FastMCP, mcp = FastMCP(...), and @mcp.tool functions
|
| 118 |
+
{user_code_indented}
|
| 119 |
+
# === USER-DEFINED TOOLS END ===
|
| 120 |
+
|
| 121 |
+
return mcp
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@app.function(
|
| 125 |
+
image=image,
|
| 126 |
+
secrets=app_secrets, # Pass environment variables to deployed function
|
| 127 |
+
# Cost optimization: minimal resources, allow cold starts
|
| 128 |
+
cpu=0.25, # 1/4 CPU core (cheapest)
|
| 129 |
+
memory=256, # 256 MB memory (minimal)
|
| 130 |
+
timeout=300, # 5 min timeout
|
| 131 |
+
# Scale to zero when not in use (no billing when idle)
|
| 132 |
+
scaledown_window=2, # Scale down after 2 seconds of inactivity
|
| 133 |
+
)
|
| 134 |
+
@modal.asgi_app()
|
| 135 |
+
def web():
|
| 136 |
+
"""ASGI web endpoint for the MCP server"""
|
| 137 |
+
from fastapi import FastAPI
|
| 138 |
+
|
| 139 |
+
mcp = make_mcp_server()
|
| 140 |
+
mcp_app = mcp.http_app(transport="streamable-http", stateless_http=True)
|
| 141 |
+
|
| 142 |
+
fastapi_app = FastAPI(
|
| 143 |
+
title="{server_name}",
|
| 144 |
+
description="Auto-deployed MCP Server on Modal.com",
|
| 145 |
+
lifespan=mcp_app.router.lifespan_context
|
| 146 |
+
)
|
| 147 |
+
fastapi_app.mount("/", mcp_app, "mcp")
|
| 148 |
+
|
| 149 |
+
return fastapi_app
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# Test function to verify deployment
|
| 153 |
+
@app.function(image=image, secrets=app_secrets)
|
| 154 |
+
async def test_server():
|
| 155 |
+
"""Test the deployed MCP server"""
|
| 156 |
+
import requests
|
| 157 |
+
|
| 158 |
+
# Simple HTTP GET test to verify the server is responding
|
| 159 |
+
url = web.get_web_url()
|
| 160 |
+
response = requests.get(url, timeout=30)
|
| 161 |
+
|
| 162 |
+
return {{
|
| 163 |
+
"status": "ok" if response.status_code == 200 else "error",
|
| 164 |
+
"status_code": response.status_code,
|
| 165 |
+
"url": url,
|
| 166 |
+
"message": "MCP server is running" if response.status_code == 200 else "Server error"
|
| 167 |
+
}}
|
| 168 |
+
'''
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# Helper functions (from server.py) - prefixed with _ to hide from MCP auto-discovery
|
| 172 |
+
def _generate_app_name(server_name: str) -> str:
|
| 173 |
+
"""Generate a unique Modal app name from server name"""
|
| 174 |
+
sanitized = re.sub(r'[^a-z0-9-]', '-', server_name.lower())
|
| 175 |
+
sanitized = re.sub(r'-+', '-', sanitized).strip('-')
|
| 176 |
+
hash_suffix = hashlib.md5(f"{server_name}{datetime.now().isoformat()}".encode()).hexdigest()[:6]
|
| 177 |
+
return f"mcp-{sanitized[:40]}-{hash_suffix}"
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _extract_imports_and_code(user_code: str) -> tuple[list[str], str]:
|
| 181 |
+
"""Extract import statements and separate from function code"""
|
| 182 |
+
lines = user_code.strip().split('\n')
|
| 183 |
+
imports = []
|
| 184 |
+
code_lines = []
|
| 185 |
+
|
| 186 |
+
for line in lines:
|
| 187 |
+
stripped = line.strip()
|
| 188 |
+
|
| 189 |
+
# Detect imports for dependency installation
|
| 190 |
+
if stripped.startswith('import ') or stripped.startswith('from '):
|
| 191 |
+
if stripped.startswith('from '):
|
| 192 |
+
match = re.match(r'from\s+(\w+)', stripped)
|
| 193 |
+
if match:
|
| 194 |
+
imports.append(match.group(1))
|
| 195 |
+
else:
|
| 196 |
+
match = re.match(r'import\s+(\w+)', stripped)
|
| 197 |
+
if match:
|
| 198 |
+
imports.append(match.group(1))
|
| 199 |
+
|
| 200 |
+
# ⚠️ CRITICAL FIX: Keep FastMCP imports and initialization in user code!
|
| 201 |
+
# The template will use the user's code AS-IS without creating duplicates
|
| 202 |
+
# This ensures @mcp.tool decorators work correctly
|
| 203 |
+
|
| 204 |
+
code_lines.append(line)
|
| 205 |
+
|
| 206 |
+
return imports, '\n'.join(code_lines)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _indent_code(code: str, spaces: int = 4) -> str:
|
| 210 |
+
"""Indent code by specified number of spaces"""
|
| 211 |
+
indent = ' ' * spaces
|
| 212 |
+
return '\n'.join(indent + line if line.strip() else line for line in code.split('\n'))
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _get_env_vars_for_deployment() -> dict:
|
| 216 |
+
"""
|
| 217 |
+
Extract relevant environment variables for Modal deployment.
|
| 218 |
+
|
| 219 |
+
Looks for common API key patterns in the environment and returns
|
| 220 |
+
them as a dictionary to be passed to Modal as secrets.
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
dict: Environment variables to pass to Modal deployment
|
| 224 |
+
"""
|
| 225 |
+
# Common API key patterns to look for
|
| 226 |
+
api_key_patterns = [
|
| 227 |
+
'API_KEY',
|
| 228 |
+
'SECRET_KEY',
|
| 229 |
+
'TOKEN',
|
| 230 |
+
'ACCESS_KEY',
|
| 231 |
+
'CLIENT_SECRET',
|
| 232 |
+
'NEBIUS',
|
| 233 |
+
'OPENAI',
|
| 234 |
+
'ANTHROPIC',
|
| 235 |
+
'GOOGLE',
|
| 236 |
+
'AWS',
|
| 237 |
+
'AZURE'
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
env_vars = {}
|
| 241 |
+
|
| 242 |
+
# Check all environment variables
|
| 243 |
+
for key, value in os.environ.items():
|
| 244 |
+
# Include if key matches common API key patterns
|
| 245 |
+
if any(pattern in key.upper() for pattern in api_key_patterns):
|
| 246 |
+
# Exclude database URLs and other sensitive non-API-key vars
|
| 247 |
+
if 'DATABASE' not in key.upper() and 'DB_' not in key.upper():
|
| 248 |
+
env_vars[key] = value
|
| 249 |
+
|
| 250 |
+
return env_vars
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _generate_env_vars_setup(env_vars: dict) -> str:
|
| 254 |
+
"""
|
| 255 |
+
Generate Python code to set up environment variables in Modal deployment.
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
env_vars: Dictionary of environment variables
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
str: Python code to add to Modal deployment template
|
| 262 |
+
"""
|
| 263 |
+
if not env_vars:
|
| 264 |
+
return "# No environment variables to pass"
|
| 265 |
+
|
| 266 |
+
lines = []
|
| 267 |
+
for key, value in env_vars.items():
|
| 268 |
+
# Escape the value properly for Python string
|
| 269 |
+
escaped_value = value.replace('\\', '\\\\').replace('"', '\\"')
|
| 270 |
+
lines.append(f'secrets_dict["{key}"] = "{escaped_value}"')
|
| 271 |
+
|
| 272 |
+
return '\n'.join(lines)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _extract_tool_definitions(code: str) -> list[dict]:
|
| 276 |
+
"""Extract MCP tool definitions from Python code"""
|
| 277 |
+
import ast
|
| 278 |
+
|
| 279 |
+
tools = []
|
| 280 |
+
try:
|
| 281 |
+
tree = ast.parse(code)
|
| 282 |
+
for node in ast.walk(tree):
|
| 283 |
+
if isinstance(node, ast.FunctionDef):
|
| 284 |
+
has_mcp_decorator = False
|
| 285 |
+
for decorator in node.decorator_list:
|
| 286 |
+
if isinstance(decorator, ast.Call):
|
| 287 |
+
if isinstance(decorator.func, ast.Attribute):
|
| 288 |
+
if (decorator.func.attr == 'tool' and
|
| 289 |
+
isinstance(decorator.func.value, ast.Name) and
|
| 290 |
+
decorator.func.value.id == 'mcp'):
|
| 291 |
+
has_mcp_decorator = True
|
| 292 |
+
break
|
| 293 |
+
elif isinstance(decorator, ast.Attribute):
|
| 294 |
+
if (decorator.attr == 'tool' and
|
| 295 |
+
isinstance(decorator.value, ast.Name) and
|
| 296 |
+
decorator.value.id == 'mcp'):
|
| 297 |
+
has_mcp_decorator = True
|
| 298 |
+
break
|
| 299 |
+
|
| 300 |
+
if has_mcp_decorator:
|
| 301 |
+
tool_name = node.name
|
| 302 |
+
docstring = ast.get_docstring(node) or "No description"
|
| 303 |
+
parameters = []
|
| 304 |
+
for arg in node.args.args:
|
| 305 |
+
param_info = {
|
| 306 |
+
"name": arg.arg,
|
| 307 |
+
"annotation": None
|
| 308 |
+
}
|
| 309 |
+
if arg.annotation:
|
| 310 |
+
if isinstance(arg.annotation, ast.Name):
|
| 311 |
+
param_info["annotation"] = arg.annotation.id
|
| 312 |
+
elif isinstance(arg.annotation, ast.Constant):
|
| 313 |
+
param_info["annotation"] = str(arg.annotation.value)
|
| 314 |
+
else:
|
| 315 |
+
param_info["annotation"] = ast.unparse(arg.annotation)
|
| 316 |
+
parameters.append(param_info)
|
| 317 |
+
|
| 318 |
+
return_type = None
|
| 319 |
+
if node.returns:
|
| 320 |
+
if isinstance(node.returns, ast.Name):
|
| 321 |
+
return_type = node.returns.id
|
| 322 |
+
elif isinstance(node.returns, ast.Constant):
|
| 323 |
+
return_type = str(node.returns.value)
|
| 324 |
+
else:
|
| 325 |
+
return_type = ast.unparse(node.returns)
|
| 326 |
+
|
| 327 |
+
tools.append({
|
| 328 |
+
"name": tool_name,
|
| 329 |
+
"description": docstring.split('\n')[0] if docstring else "No description",
|
| 330 |
+
"full_description": docstring,
|
| 331 |
+
"parameters": parameters,
|
| 332 |
+
"return_type": return_type
|
| 333 |
+
})
|
| 334 |
+
except:
|
| 335 |
+
pass
|
| 336 |
+
|
| 337 |
+
return tools
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
# =============================================================================
|
| 341 |
+
# MCP TOOL IMPLEMENTATIONS (converted from server.py)
|
| 342 |
+
# =============================================================================
|
| 343 |
+
|
| 344 |
+
def deploy_mcp_server(
|
| 345 |
+
server_name: str,
|
| 346 |
+
mcp_tools_code: str,
|
| 347 |
+
extra_pip_packages: str = "",
|
| 348 |
+
description: str = "",
|
| 349 |
+
category: str = "Uncategorized",
|
| 350 |
+
tags: List[str] = None,
|
| 351 |
+
author: str = "Anonymous",
|
| 352 |
+
version: str = "1.0.0",
|
| 353 |
+
documentation: str = ""
|
| 354 |
+
) -> dict:
|
| 355 |
+
"""
|
| 356 |
+
Deploy an MCP server with custom tools to Modal.com.
|
| 357 |
+
|
| 358 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 359 |
+
🚨 FOR AI ASSISTANTS: CRITICAL CODE FORMAT REQUIREMENTS 🚨
|
| 360 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 361 |
+
|
| 362 |
+
WHEN GENERATING CODE FOR THIS TOOL, YOU **MUST** FOLLOW THIS EXACT FORMAT:
|
| 363 |
+
|
| 364 |
+
**REQUIRED STRUCTURE - COPY THIS TEMPLATE:**
|
| 365 |
+
```python
|
| 366 |
+
from fastmcp import FastMCP
|
| 367 |
+
|
| 368 |
+
mcp = FastMCP("server-name")
|
| 369 |
+
|
| 370 |
+
@mcp.tool
|
| 371 |
+
def your_function_name(param: str) -> str:
|
| 372 |
+
\"\"\"Clear description of what this tool does\"\"\"
|
| 373 |
+
# Your implementation here
|
| 374 |
+
return "result"
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
**✅ CRITICAL RULES:**
|
| 378 |
+
1. ✅ MUST start with: `from fastmcp import FastMCP`
|
| 379 |
+
2. ✅ MUST have: `mcp = FastMCP("server-name")`
|
| 380 |
+
3. ✅ MUST use: `@mcp.tool` (NO parentheses unless passing arguments!)
|
| 381 |
+
4. ✅ MUST have: Type hints on ALL parameters and return type
|
| 382 |
+
5. ✅ MUST have: Docstring (triple quotes) for each function
|
| 383 |
+
6. ❌ NEVER include: `mcp.run()` or `if __name__ == "__main__"`
|
| 384 |
+
|
| 385 |
+
**🎯 COMPLETE WORKING EXAMPLE:**
|
| 386 |
+
```python
|
| 387 |
+
from fastmcp import FastMCP
|
| 388 |
+
|
| 389 |
+
mcp = FastMCP("weather-api")
|
| 390 |
+
|
| 391 |
+
@mcp.tool
|
| 392 |
+
def get_weather(city: str) -> str:
|
| 393 |
+
\"\"\"Get current weather for any city using wttr.in\"\"\"
|
| 394 |
+
import requests
|
| 395 |
+
response = requests.get(f"https://wttr.in/{city}?format=3")
|
| 396 |
+
return response.text
|
| 397 |
+
|
| 398 |
+
@mcp.tool
|
| 399 |
+
def get_temperature(city: str, unit: str = "celsius") -> dict:
|
| 400 |
+
\"\"\"Get temperature in celsius or fahrenheit\"\"\"
|
| 401 |
+
import requests
|
| 402 |
+
response = requests.get(f"https://wttr.in/{city}?format=j1")
|
| 403 |
+
data = response.json()
|
| 404 |
+
temp_c = int(data['current_condition'][0]['temp_C'])
|
| 405 |
+
if unit == "fahrenheit":
|
| 406 |
+
return {"temperature": temp_c * 9/5 + 32, "unit": "F"}
|
| 407 |
+
return {"temperature": temp_c, "unit": "C"}
|
| 408 |
+
```
|
| 409 |
+
|
| 410 |
+
**⚠️ COMMON MISTAKES TO AVOID:**
|
| 411 |
+
- ❌ Using `@mcp.tool()` with empty parentheses (use `@mcp.tool` instead)
|
| 412 |
+
- ❌ Forgetting type hints: `def my_tool(x)` → `def my_tool(x: str) -> str`
|
| 413 |
+
- ❌ Missing docstrings
|
| 414 |
+
- ❌ Including `mcp.run()` at the end
|
| 415 |
+
- ❌ Forgetting to import FastMCP
|
| 416 |
+
|
| 417 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 418 |
+
|
| 419 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 420 |
+
📋 KEY REQUIREMENTS (from FastMCP docs)
|
| 421 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 422 |
+
|
| 423 |
+
✅ MUST HAVE:
|
| 424 |
+
1. `from fastmcp import FastMCP` at the top
|
| 425 |
+
2. `mcp = FastMCP("server-name")` to create the server
|
| 426 |
+
3. `@mcp.tool` or `@mcp.tool()` decorator on each function
|
| 427 |
+
4. Docstring for each tool function
|
| 428 |
+
5. Type hints for all parameters and return type
|
| 429 |
+
|
| 430 |
+
💡 Decorator Syntax (both work):
|
| 431 |
+
- `@mcp.tool` - Preferred syntax (cleaner, used in FastMCP docs)
|
| 432 |
+
- `@mcp.tool()` - Also valid (required when passing options like name, enabled, etc.)
|
| 433 |
+
|
| 434 |
+
❌ DO NOT INCLUDE:
|
| 435 |
+
- `mcp.run()` or server startup code
|
| 436 |
+
- `if __name__ == "__main__"` blocks
|
| 437 |
+
|
| 438 |
+
💡 The deployment wrapper will handle FastMCP setup, so you can optionally
|
| 439 |
+
omit the import and initialization, but including them is fine too.
|
| 440 |
+
|
| 441 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 442 |
+
|
| 443 |
+
⚡ QUICK START - COPY THIS EXAMPLE:
|
| 444 |
+
================================
|
| 445 |
+
|
| 446 |
+
Step 1: Write your MCP tools code (mcp_tools_code parameter):
|
| 447 |
+
```python
|
| 448 |
+
from fastmcp import FastMCP
|
| 449 |
+
|
| 450 |
+
mcp = FastMCP("cat-facts")
|
| 451 |
+
|
| 452 |
+
@mcp.tool
|
| 453 |
+
def get_cat_fact() -> str:
|
| 454 |
+
'''Get a random cat fact from an API'''
|
| 455 |
+
import requests
|
| 456 |
+
response = requests.get("https://catfact.ninja/fact")
|
| 457 |
+
return response.json()["fact"]
|
| 458 |
+
|
| 459 |
+
@mcp.tool
|
| 460 |
+
def add_numbers(a: int, b: int) -> int:
|
| 461 |
+
'''Add two numbers together'''
|
| 462 |
+
return a + b
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
Step 2: Call this tool with your code:
|
| 466 |
+
```python
|
| 467 |
+
result = deploy_mcp_server(
|
| 468 |
+
server_name="cat-facts",
|
| 469 |
+
mcp_tools_code='''
|
| 470 |
+
from fastmcp import FastMCP
|
| 471 |
+
|
| 472 |
+
mcp = FastMCP("cat-facts")
|
| 473 |
+
|
| 474 |
+
@mcp.tool
|
| 475 |
+
def get_cat_fact() -> str:
|
| 476 |
+
import requests
|
| 477 |
+
response = requests.get("https://catfact.ninja/fact")
|
| 478 |
+
return response.json()["fact"]
|
| 479 |
+
''',
|
| 480 |
+
extra_pip_packages="requests",
|
| 481 |
+
description="Get random cat facts",
|
| 482 |
+
category="Fun",
|
| 483 |
+
tags=["api", "animals"],
|
| 484 |
+
author="Your Name",
|
| 485 |
+
version="1.0.0"
|
| 486 |
+
)
|
| 487 |
+
```
|
| 488 |
+
|
| 489 |
+
Step 3: Use the deployed URL:
|
| 490 |
+
```
|
| 491 |
+
Your MCP endpoint will be at: https://xxx.modal.run/mcp/
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 495 |
+
📝 CODE STRUCTURE - DETAILED EXPLANATION
|
| 496 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 497 |
+
|
| 498 |
+
⚠️ IMPORTANT - READ THIS CAREFULLY:
|
| 499 |
+
|
| 500 |
+
The deployment wrapper already creates the MCP server instance for you.
|
| 501 |
+
Your code must include `from fastmcp import FastMCP`, create an MCP instance,
|
| 502 |
+
and decorate your functions with `@mcp.tool` (no parentheses).
|
| 503 |
+
|
| 504 |
+
✅ CORRECT FORMAT (from FastMCP official docs):
|
| 505 |
+
─────────────────────────────────────────────────
|
| 506 |
+
```python
|
| 507 |
+
from fastmcp import FastMCP
|
| 508 |
+
|
| 509 |
+
mcp = FastMCP("server-name")
|
| 510 |
+
|
| 511 |
+
@mcp.tool
|
| 512 |
+
def my_tool(param: str) -> str:
|
| 513 |
+
'''Tool description'''
|
| 514 |
+
return f"Result: {param}"
|
| 515 |
+
```
|
| 516 |
+
|
| 517 |
+
💡 Note: Both `@mcp.tool` and `@mcp.tool()` work!
|
| 518 |
+
- `@mcp.tool` - Cleaner (preferred in FastMCP docs)
|
| 519 |
+
- `@mcp.tool()` - Also valid, required when passing options:
|
| 520 |
+
```python
|
| 521 |
+
@mcp.tool(name="custom_name", description="Custom description", enabled=True)
|
| 522 |
+
def my_function():
|
| 523 |
+
pass
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
🔧 WHAT THE WRAPPER PROVIDES AUTOMATICALLY:
|
| 527 |
+
────────────────────────────────────────────
|
| 528 |
+
✅ from fastmcp import FastMCP
|
| 529 |
+
✅ mcp = FastMCP("{server_name}")
|
| 530 |
+
✅ Server initialization and configuration
|
| 531 |
+
✅ Modal deployment wrapper
|
| 532 |
+
✅ HTTP transport setup
|
| 533 |
+
✅ Environment variable loading
|
| 534 |
+
|
| 535 |
+
📋 WHAT YOU MUST PROVIDE (required by FastMCP):
|
| 536 |
+
────────────────────────────────────────────────
|
| 537 |
+
✅ `from fastmcp import FastMCP` import statement
|
| 538 |
+
✅ `mcp = FastMCP("server-name")` initialization
|
| 539 |
+
✅ One or more functions decorated with `@mcp.tool` or `@mcp.tool()`
|
| 540 |
+
✅ Docstrings for each tool (becomes tool description in MCP)
|
| 541 |
+
✅ Type hints for all parameters (str, int, bool, dict, list, etc.)
|
| 542 |
+
✅ Type hint for return value
|
| 543 |
+
✅ Any Python imports your code needs (can go at top or inside functions)
|
| 544 |
+
|
| 545 |
+
❌ DO NOT INCLUDE THESE:
|
| 546 |
+
────────────────────────
|
| 547 |
+
❌ mcp.run() ← Wrapper handles server startup
|
| 548 |
+
❌ if __name__ == "__main__" ← Not needed in deployment
|
| 549 |
+
❌ Modal imports (modal.App, etc.) ← Wrapper handles Modal setup
|
| 550 |
+
|
| 551 |
+
💡 The wrapper will auto-strip duplicate FastMCP imports if present
|
| 552 |
+
|
| 553 |
+
═══════════════════════════════════════════════════════════════════════════
|
| 554 |
+
|
| 555 |
+
💡 MORE EXAMPLES:
|
| 556 |
+
================
|
| 557 |
+
|
| 558 |
+
Example 1 - Simple Calculator:
|
| 559 |
+
```python
|
| 560 |
+
from fastmcp import FastMCP
|
| 561 |
+
|
| 562 |
+
mcp = FastMCP("calculator")
|
| 563 |
+
|
| 564 |
+
@mcp.tool
|
| 565 |
+
def calculate(expression: str) -> float:
|
| 566 |
+
'''Safely evaluate a math expression'''
|
| 567 |
+
import ast
|
| 568 |
+
import operator
|
| 569 |
+
|
| 570 |
+
ops = {
|
| 571 |
+
ast.Add: operator.add,
|
| 572 |
+
ast.Sub: operator.sub,
|
| 573 |
+
ast.Mult: operator.mul,
|
| 574 |
+
ast.Div: operator.truediv,
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
def eval_expr(node):
|
| 578 |
+
if isinstance(node, ast.Num):
|
| 579 |
+
return node.n
|
| 580 |
+
elif isinstance(node, ast.BinOp):
|
| 581 |
+
return ops[type(node.op)](eval_expr(node.left), eval_expr(node.right))
|
| 582 |
+
else:
|
| 583 |
+
raise ValueError("Invalid expression")
|
| 584 |
+
|
| 585 |
+
return eval_expr(ast.parse(expression, mode='eval').body)
|
| 586 |
+
```
|
| 587 |
+
|
| 588 |
+
Example 2 - Weather API with Error Handling (requires API key):
|
| 589 |
+
```python
|
| 590 |
+
from fastmcp import FastMCP
|
| 591 |
+
|
| 592 |
+
mcp = FastMCP("weather")
|
| 593 |
+
|
| 594 |
+
@mcp.tool
|
| 595 |
+
def get_weather(city: str) -> dict:
|
| 596 |
+
'''Get current weather for a city.
|
| 597 |
+
|
| 598 |
+
IMPORTANT: Always returns dict (never None) to match return type!
|
| 599 |
+
Returns error dict if request fails.
|
| 600 |
+
'''
|
| 601 |
+
import requests
|
| 602 |
+
import os
|
| 603 |
+
|
| 604 |
+
api_key = os.environ.get("OPENWEATHER_API_KEY", "demo")
|
| 605 |
+
url = f"https://api.openweathermap.org/data/2.5/weather"
|
| 606 |
+
params = {"q": city, "appid": api_key, "units": "metric"}
|
| 607 |
+
|
| 608 |
+
try:
|
| 609 |
+
response = requests.get(url, params=params, timeout=10)
|
| 610 |
+
response.raise_for_status()
|
| 611 |
+
data = response.json()
|
| 612 |
+
|
| 613 |
+
return {
|
| 614 |
+
"city": city,
|
| 615 |
+
"temperature": data["main"]["temp"],
|
| 616 |
+
"description": data["weather"][0]["description"],
|
| 617 |
+
"humidity": data["main"]["humidity"]
|
| 618 |
+
}
|
| 619 |
+
except Exception as e:
|
| 620 |
+
# Return error dict (not None!) to match return type
|
| 621 |
+
return {"error": str(e), "city": city}
|
| 622 |
+
```
|
| 623 |
+
|
| 624 |
+
Example 3 - Using Optional for Nullable Returns:
|
| 625 |
+
```python
|
| 626 |
+
from fastmcp import FastMCP
|
| 627 |
+
from typing import Optional
|
| 628 |
+
|
| 629 |
+
mcp = FastMCP("data-tools")
|
| 630 |
+
|
| 631 |
+
@mcp.tool
|
| 632 |
+
def find_user(user_id: int) -> Optional[dict]:
|
| 633 |
+
'''Find a user by ID. Returns None if not found.
|
| 634 |
+
|
| 635 |
+
Using Optional[dict] allows returning None!
|
| 636 |
+
'''
|
| 637 |
+
users = {
|
| 638 |
+
1: {"name": "Alice", "email": "alice@example.com"},
|
| 639 |
+
2: {"name": "Bob", "email": "bob@example.com"}
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
# Can return None because of Optional
|
| 643 |
+
return users.get(user_id) # Returns None if not found
|
| 644 |
+
```
|
| 645 |
+
|
| 646 |
+
Example 4 - Multiple Tools in One Server:
|
| 647 |
+
```python
|
| 648 |
+
from fastmcp import FastMCP
|
| 649 |
+
|
| 650 |
+
mcp = FastMCP("text-tools")
|
| 651 |
+
|
| 652 |
+
@mcp.tool
|
| 653 |
+
def count_words(text: str) -> int:
|
| 654 |
+
'''Count words in text'''
|
| 655 |
+
return len(text.split())
|
| 656 |
+
|
| 657 |
+
@mcp.tool
|
| 658 |
+
def reverse_text(text: str) -> str:
|
| 659 |
+
'''Reverse the text'''
|
| 660 |
+
return text[::-1]
|
| 661 |
+
|
| 662 |
+
@mcp.tool
|
| 663 |
+
def to_uppercase(text: str) -> str:
|
| 664 |
+
'''Convert text to uppercase'''
|
| 665 |
+
return text.upper()
|
| 666 |
+
```
|
| 667 |
+
|
| 668 |
+
📦 PARAMETERS EXPLAINED:
|
| 669 |
+
========================
|
| 670 |
+
|
| 671 |
+
Args:
|
| 672 |
+
server_name (str, REQUIRED):
|
| 673 |
+
Unique name for your MCP server. Use lowercase with hyphens.
|
| 674 |
+
Examples: "weather-api", "cat-facts", "calculator-tool"
|
| 675 |
+
|
| 676 |
+
mcp_tools_code (str, REQUIRED):
|
| 677 |
+
⚠️ IMPORTANT: Must be complete FastMCP server code!
|
| 678 |
+
|
| 679 |
+
✅ MUST include (per FastMCP docs):
|
| 680 |
+
- from fastmcp import FastMCP
|
| 681 |
+
- mcp = FastMCP("server-name")
|
| 682 |
+
- @mcp.tool decorated functions (NO parentheses!)
|
| 683 |
+
- Function docstrings
|
| 684 |
+
- Type hints for all parameters and return values
|
| 685 |
+
|
| 686 |
+
❌ DO NOT include:
|
| 687 |
+
- mcp.run() or server startup code
|
| 688 |
+
- if __name__ == "__main__" blocks
|
| 689 |
+
|
| 690 |
+
See "SIMPLE EXAMPLE - COPY THIS EXACTLY" section above for template.
|
| 691 |
+
The wrapper will handle Modal deployment and auto-strip duplicate imports.
|
| 692 |
+
|
| 693 |
+
extra_pip_packages (str, optional):
|
| 694 |
+
Comma-separated list of PyPI packages your code needs.
|
| 695 |
+
Examples: "requests", "pandas,numpy", "beautifulsoup4,requests"
|
| 696 |
+
The system auto-detects some imports, but always specify to be safe!
|
| 697 |
+
|
| 698 |
+
description (str, optional):
|
| 699 |
+
Human-readable description of what your server does.
|
| 700 |
+
Example: "Provides weather data and forecasts for any city"
|
| 701 |
+
|
| 702 |
+
category (str, optional):
|
| 703 |
+
Category for organizing your servers.
|
| 704 |
+
Examples: "Weather", "Finance", "Utilities", "Fun", "Data"
|
| 705 |
+
Default: "Uncategorized"
|
| 706 |
+
|
| 707 |
+
tags (List[str], optional):
|
| 708 |
+
List of tags for filtering and search.
|
| 709 |
+
Examples: ["api", "weather"], ["finance", "stocks", "data"]
|
| 710 |
+
Default: []
|
| 711 |
+
|
| 712 |
+
author (str, optional):
|
| 713 |
+
Your name or organization.
|
| 714 |
+
Default: "Anonymous"
|
| 715 |
+
|
| 716 |
+
version (str, optional):
|
| 717 |
+
Semantic version for your server.
|
| 718 |
+
Examples: "1.0.0", "2.1.0", "0.0.1"
|
| 719 |
+
Default: "1.0.0"
|
| 720 |
+
|
| 721 |
+
documentation (str, optional):
|
| 722 |
+
Markdown documentation for your server.
|
| 723 |
+
Default: ""
|
| 724 |
+
|
| 725 |
+
Returns:
|
| 726 |
+
dict: Deployment result with the following structure:
|
| 727 |
+
{
|
| 728 |
+
"success": bool, # True if deployment succeeded
|
| 729 |
+
"app_name": str, # Modal app name (e.g., "mcp-weather-abc123")
|
| 730 |
+
"url": str, # Base URL (e.g., "https://xxx.modal.run")
|
| 731 |
+
"mcp_endpoint": str, # Full MCP endpoint URL (url + "/mcp/")
|
| 732 |
+
"deployment_id": str, # Unique ID for this deployment
|
| 733 |
+
"detected_packages": list, # Auto-detected Python packages
|
| 734 |
+
"security_scan": dict, # Security scan results
|
| 735 |
+
"message": str # Human-readable success/error message
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
On error:
|
| 739 |
+
{
|
| 740 |
+
"success": False,
|
| 741 |
+
"error": str, # Error message
|
| 742 |
+
"security_scan": dict, # If security issues found
|
| 743 |
+
"severity": str, # "low", "medium", "high", or "critical"
|
| 744 |
+
"issues": list, # List of security issues
|
| 745 |
+
"explanation": str # Detailed explanation
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
🔒 SECURITY:
|
| 749 |
+
===========
|
| 750 |
+
All code is automatically scanned for security vulnerabilities before deployment.
|
| 751 |
+
Deployments with HIGH or CRITICAL severity issues will be blocked.
|
| 752 |
+
|
| 753 |
+
⚠️ COMMON ERRORS & FIXES:
|
| 754 |
+
==========================
|
| 755 |
+
|
| 756 |
+
Error: "Invalid Python code"
|
| 757 |
+
Fix: Check your code syntax. Test it locally first!
|
| 758 |
+
|
| 759 |
+
Error: "No @mcp.tool decorators found"
|
| 760 |
+
Fix: Make sure you have at least one function with @mcp.tool (no parentheses!)
|
| 761 |
+
|
| 762 |
+
Error: "Module 'xyz' not found"
|
| 763 |
+
Fix: Add the package to extra_pip_packages parameter
|
| 764 |
+
|
| 765 |
+
Error: "Security vulnerabilities detected"
|
| 766 |
+
Fix: Review the security scan output and fix the issues
|
| 767 |
+
|
| 768 |
+
Error: "Input validation error: None is not of type 'array'"
|
| 769 |
+
Fix: TYPE HINT MISMATCH! Your function's return type doesn't match what it actually returns.
|
| 770 |
+
|
| 771 |
+
Common causes:
|
| 772 |
+
- Function returns None but type hint says list: `-> list`
|
| 773 |
+
- Function returns None but type hint says dict: `-> dict`
|
| 774 |
+
- Function can return None but type hint doesn't allow it
|
| 775 |
+
|
| 776 |
+
Solutions:
|
| 777 |
+
✅ If function can return None, use Optional:
|
| 778 |
+
from typing import Optional
|
| 779 |
+
def my_tool() -> Optional[list]: # Can return list or None
|
| 780 |
+
if error:
|
| 781 |
+
return None
|
| 782 |
+
return [1, 2, 3]
|
| 783 |
+
|
| 784 |
+
✅ If function always returns a value, ensure it does:
|
| 785 |
+
def my_tool() -> list:
|
| 786 |
+
if error:
|
| 787 |
+
return [] # Return empty list, not None
|
| 788 |
+
return [1, 2, 3]
|
| 789 |
+
|
| 790 |
+
✅ Match your return type to what you actually return:
|
| 791 |
+
def my_tool() -> str: # Says returns string
|
| 792 |
+
return "result" # Actually returns string ✅
|
| 793 |
+
|
| 794 |
+
def my_tool() -> dict: # Says returns dict
|
| 795 |
+
return None # Actually returns None ❌ WRONG!
|
| 796 |
+
|
| 797 |
+
def my_tool() -> dict: # Says returns dict
|
| 798 |
+
return {"key": "value"} # Actually returns dict ✅
|
| 799 |
+
|
| 800 |
+
💰 COST & PERFORMANCE:
|
| 801 |
+
=====================
|
| 802 |
+
|
| 803 |
+
Your deployed server will:
|
| 804 |
+
- Use 0.25 CPU cores (1/4 core) - cheapest tier
|
| 805 |
+
- Use 256 MB RAM - minimal memory
|
| 806 |
+
- Scale to ZERO when not in use (NO BILLING when idle!)
|
| 807 |
+
- Cold start in 2-5 seconds when first called
|
| 808 |
+
- Auto-scale up based on traffic
|
| 809 |
+
- Timeout after 5 minutes of processing
|
| 810 |
+
|
| 811 |
+
🚀 AFTER DEPLOYMENT:
|
| 812 |
+
===================
|
| 813 |
+
|
| 814 |
+
1. Your server will be available at: https://xxx.modal.run/mcp/
|
| 815 |
+
2. Add it to Claude Desktop config:
|
| 816 |
+
{
|
| 817 |
+
"mcpServers": {
|
| 818 |
+
"your-server": {
|
| 819 |
+
"url": "https://xxx.modal.run/mcp/"
|
| 820 |
+
}
|
| 821 |
+
}
|
| 822 |
+
}
|
| 823 |
+
3. Test it using MCP Inspector:
|
| 824 |
+
npx @modelcontextprotocol/inspector https://xxx.modal.run/mcp/
|
| 825 |
+
"""
|
| 826 |
+
try:
|
| 827 |
+
# === VALIDATION PHASE ===
|
| 828 |
+
# Validate required parameters
|
| 829 |
+
if not server_name or not server_name.strip():
|
| 830 |
+
return {
|
| 831 |
+
"success": False,
|
| 832 |
+
"error": "server_name is required and cannot be empty",
|
| 833 |
+
"message": "❌ Please provide a server name (e.g., 'weather-api', 'cat-facts')"
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
if not mcp_tools_code or not mcp_tools_code.strip():
|
| 837 |
+
return {
|
| 838 |
+
"success": False,
|
| 839 |
+
"error": "mcp_tools_code is required and cannot be empty",
|
| 840 |
+
"message": "❌ Please provide your MCP tools code. See the tool description for examples!"
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
# Validate code contains at least one @mcp.tool or @mcp.tool() decorator
|
| 844 |
+
if "@mcp.tool" not in mcp_tools_code:
|
| 845 |
+
return {
|
| 846 |
+
"success": False,
|
| 847 |
+
"error": "Code must have at least one @mcp.tool decorator",
|
| 848 |
+
"message": "❌ Your code must include at least one tool with @mcp.tool decorator\n\n"
|
| 849 |
+
"Example:\n"
|
| 850 |
+
"from fastmcp import FastMCP\n"
|
| 851 |
+
"mcp = FastMCP('server-name')\n\n"
|
| 852 |
+
"@mcp.tool\n"
|
| 853 |
+
"def my_tool(param: str) -> str:\n"
|
| 854 |
+
" '''Tool description'''\n"
|
| 855 |
+
" return f'Result: {param}'\n\n"
|
| 856 |
+
"See the tool description for complete examples!"
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
# ⚠️ CRITICAL FIX: Do NOT strip FastMCP imports/initialization from user code!
|
| 860 |
+
# The _extract_imports_and_code() function will handle this properly
|
| 861 |
+
# by keeping the code intact and only extracting import info for pip packages
|
| 862 |
+
import re
|
| 863 |
+
|
| 864 |
+
# Convert comma-separated packages to list
|
| 865 |
+
extra_pip_packages_list = [p.strip() for p in extra_pip_packages.split(",")] if extra_pip_packages else []
|
| 866 |
+
|
| 867 |
+
# Handle tags parameter
|
| 868 |
+
tags_list = tags if tags is not None else []
|
| 869 |
+
|
| 870 |
+
# Generate unique app name
|
| 871 |
+
app_name = _generate_app_name(server_name)
|
| 872 |
+
|
| 873 |
+
# Generate deployment_id early so it can be used in webhook configuration
|
| 874 |
+
deployment_id = f"deploy-{app_name}"
|
| 875 |
+
|
| 876 |
+
# Extract imports and prepare extra dependencies
|
| 877 |
+
detected_imports, cleaned_code = _extract_imports_and_code(mcp_tools_code)
|
| 878 |
+
all_packages = list(set(detected_imports + extra_pip_packages_list))
|
| 879 |
+
|
| 880 |
+
# Filter out standard library packages
|
| 881 |
+
stdlib = {'os', 'sys', 'json', 're', 'datetime', 'time', 'typing', 'pathlib',
|
| 882 |
+
'collections', 'functools', 'itertools', 'math', 'random', 'string',
|
| 883 |
+
'hashlib', 'base64', 'urllib', 'zoneinfo', 'asyncio'}
|
| 884 |
+
extra_deps = [pkg for pkg in all_packages if pkg.lower() not in stdlib]
|
| 885 |
+
|
| 886 |
+
# === SECURITY SCAN PHASE ===
|
| 887 |
+
from utils.security_scanner import scan_code_for_security
|
| 888 |
+
|
| 889 |
+
scan_result = scan_code_for_security(
|
| 890 |
+
code=cleaned_code,
|
| 891 |
+
context={
|
| 892 |
+
"server_name": server_name,
|
| 893 |
+
"packages": extra_deps,
|
| 894 |
+
"description": description
|
| 895 |
+
}
|
| 896 |
+
)
|
| 897 |
+
|
| 898 |
+
if scan_result["severity"] in ["high", "critical"]:
|
| 899 |
+
return {
|
| 900 |
+
"success": False,
|
| 901 |
+
"error": "Security vulnerabilities detected - deployment blocked",
|
| 902 |
+
"security_scan": scan_result,
|
| 903 |
+
"severity": scan_result["severity"],
|
| 904 |
+
"issues": scan_result["issues"],
|
| 905 |
+
"explanation": scan_result["explanation"],
|
| 906 |
+
"message": f"🚫 Deployment blocked due to {scan_result['severity']} severity security issues"
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
# Format extra dependencies for template
|
| 910 |
+
extra_deps_str = ',\n '.join(f'"{pkg}"' for pkg in extra_deps) if extra_deps else ''
|
| 911 |
+
user_code_indented = _indent_code(cleaned_code, spaces=4)
|
| 912 |
+
|
| 913 |
+
# Get environment variables for Modal deployment
|
| 914 |
+
env_vars = _get_env_vars_for_deployment()
|
| 915 |
+
env_vars_setup = _generate_env_vars_setup(env_vars)
|
| 916 |
+
|
| 917 |
+
# Generate webhook configuration
|
| 918 |
+
webhook_url = os.getenv('MCP_WEBHOOK_URL', '')
|
| 919 |
+
if not webhook_url:
|
| 920 |
+
base_url = os.getenv('MCP_BASE_URL', 'http://localhost:7860')
|
| 921 |
+
webhook_url = f"{base_url}/api/webhook/usage"
|
| 922 |
+
|
| 923 |
+
webhook_env_vars_code = f'''
|
| 924 |
+
secrets_dict["MCP_WEBHOOK_URL"] = "{webhook_url}"
|
| 925 |
+
secrets_dict["MCP_DEPLOYMENT_ID"] = "{deployment_id}"
|
| 926 |
+
'''
|
| 927 |
+
|
| 928 |
+
# Generate Modal wrapper code (tracking removed - will be developed later)
|
| 929 |
+
modal_code = MODAL_WRAPPER_TEMPLATE.format(
|
| 930 |
+
app_name=app_name,
|
| 931 |
+
server_name=server_name,
|
| 932 |
+
timestamp=datetime.now().isoformat(),
|
| 933 |
+
extra_deps=extra_deps_str,
|
| 934 |
+
user_code_indented=user_code_indented,
|
| 935 |
+
env_vars_setup=env_vars_setup,
|
| 936 |
+
webhook_env_vars=webhook_env_vars_code
|
| 937 |
+
)
|
| 938 |
+
|
| 939 |
+
# Create temporary deployment directory (will be cleaned up after deployment)
|
| 940 |
+
temp_deploy_dir = tempfile.mkdtemp(prefix=f"mcp_deploy_{app_name}_")
|
| 941 |
+
try:
|
| 942 |
+
deploy_dir_path = Path(temp_deploy_dir)
|
| 943 |
+
deploy_file = deploy_dir_path / "app.py"
|
| 944 |
+
deploy_file.write_text(modal_code)
|
| 945 |
+
(deploy_dir_path / "original_tools.py").write_text(mcp_tools_code)
|
| 946 |
+
|
| 947 |
+
# Deploy to Modal
|
| 948 |
+
result = subprocess.run(
|
| 949 |
+
["modal", "deploy", str(deploy_file)],
|
| 950 |
+
capture_output=True,
|
| 951 |
+
text=True,
|
| 952 |
+
timeout=300
|
| 953 |
+
)
|
| 954 |
+
finally:
|
| 955 |
+
# Clean up temporary deployment directory
|
| 956 |
+
try:
|
| 957 |
+
shutil.rmtree(temp_deploy_dir)
|
| 958 |
+
except Exception:
|
| 959 |
+
pass # Ignore cleanup errors
|
| 960 |
+
|
| 961 |
+
if result.returncode != 0:
|
| 962 |
+
return {
|
| 963 |
+
"success": False,
|
| 964 |
+
"error": "Deployment failed",
|
| 965 |
+
"stdout": result.stdout,
|
| 966 |
+
"stderr": result.stderr
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
# Extract URL from deployment output
|
| 970 |
+
url_match = re.search(r'https://[a-zA-Z0-9-]+--[a-zA-Z0-9-]+\.modal\.run', result.stdout)
|
| 971 |
+
deployed_url = url_match.group(0) if url_match else None
|
| 972 |
+
|
| 973 |
+
if not deployed_url:
|
| 974 |
+
try:
|
| 975 |
+
import modal
|
| 976 |
+
remote_func = modal.Function.from_name(app_name, "web")
|
| 977 |
+
deployed_url = remote_func.get_web_url()
|
| 978 |
+
except Exception:
|
| 979 |
+
deployed_url = f"https://<workspace>--{app_name}-web.modal.run"
|
| 980 |
+
|
| 981 |
+
# Save to database (deployment_id already created earlier)
|
| 982 |
+
with db_transaction() as db:
|
| 983 |
+
deployment = Deployment(
|
| 984 |
+
deployment_id=deployment_id,
|
| 985 |
+
app_name=app_name,
|
| 986 |
+
server_name=server_name,
|
| 987 |
+
url=deployed_url,
|
| 988 |
+
mcp_endpoint=f"{deployed_url}/mcp/" if deployed_url else None,
|
| 989 |
+
description=description,
|
| 990 |
+
status="deployed",
|
| 991 |
+
category=category,
|
| 992 |
+
tags=tags_list,
|
| 993 |
+
author=author,
|
| 994 |
+
version=version,
|
| 995 |
+
documentation=documentation,
|
| 996 |
+
)
|
| 997 |
+
db.add(deployment)
|
| 998 |
+
db.flush()
|
| 999 |
+
|
| 1000 |
+
for package in extra_deps:
|
| 1001 |
+
pkg = DeploymentPackage(deployment_id=deployment_id, package_name=package)
|
| 1002 |
+
db.add(pkg)
|
| 1003 |
+
|
| 1004 |
+
# Store files in database only (no local file paths)
|
| 1005 |
+
app_file = DeploymentFile(
|
| 1006 |
+
deployment_id=deployment_id,
|
| 1007 |
+
file_type="app",
|
| 1008 |
+
file_path="", # No persistent local file
|
| 1009 |
+
file_content=modal_code,
|
| 1010 |
+
)
|
| 1011 |
+
db.add(app_file)
|
| 1012 |
+
|
| 1013 |
+
original_file = DeploymentFile(
|
| 1014 |
+
deployment_id=deployment_id,
|
| 1015 |
+
file_type="original_tools",
|
| 1016 |
+
file_path="", # No persistent local file
|
| 1017 |
+
file_content=mcp_tools_code,
|
| 1018 |
+
)
|
| 1019 |
+
db.add(original_file)
|
| 1020 |
+
|
| 1021 |
+
tools_list = _extract_tool_definitions(cleaned_code)
|
| 1022 |
+
tools_manifest = DeploymentFile(
|
| 1023 |
+
deployment_id=deployment_id,
|
| 1024 |
+
file_type="tools_manifest",
|
| 1025 |
+
file_path="",
|
| 1026 |
+
file_content=json.dumps(tools_list, indent=2),
|
| 1027 |
+
)
|
| 1028 |
+
db.add(tools_manifest)
|
| 1029 |
+
|
| 1030 |
+
DeploymentHistory.log_event(
|
| 1031 |
+
db=db,
|
| 1032 |
+
deployment_id=deployment_id,
|
| 1033 |
+
action="created",
|
| 1034 |
+
details={
|
| 1035 |
+
"server_name": server_name,
|
| 1036 |
+
"packages": extra_deps,
|
| 1037 |
+
"deployed_url": deployed_url,
|
| 1038 |
+
},
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
+
scan_action = "security_scan_passed" if scan_result["is_safe"] else "security_scan_warning"
|
| 1042 |
+
DeploymentHistory.log_event(
|
| 1043 |
+
db=db,
|
| 1044 |
+
deployment_id=deployment_id,
|
| 1045 |
+
action=scan_action,
|
| 1046 |
+
details={
|
| 1047 |
+
"severity": scan_result["severity"],
|
| 1048 |
+
"is_safe": scan_result["is_safe"],
|
| 1049 |
+
"explanation": scan_result["explanation"],
|
| 1050 |
+
}
|
| 1051 |
+
)
|
| 1052 |
+
|
| 1053 |
+
security_msg = ""
|
| 1054 |
+
if scan_result["severity"] in ["low", "medium"]:
|
| 1055 |
+
security_msg = f"\n⚠️ Security Warning ({scan_result['severity']} severity): {scan_result['explanation']}"
|
| 1056 |
+
elif scan_result["is_safe"]:
|
| 1057 |
+
security_msg = "\n✅ Security scan passed"
|
| 1058 |
+
|
| 1059 |
+
# Generate Claude Desktop integration config
|
| 1060 |
+
mcp_endpoint = f"{deployed_url}/mcp/" if deployed_url else None
|
| 1061 |
+
claude_desktop_config = {
|
| 1062 |
+
server_name: {
|
| 1063 |
+
"command": "npx",
|
| 1064 |
+
"args": [
|
| 1065 |
+
"mcp-remote",
|
| 1066 |
+
mcp_endpoint
|
| 1067 |
+
]
|
| 1068 |
+
}
|
| 1069 |
+
} if mcp_endpoint else {}
|
| 1070 |
+
|
| 1071 |
+
config_locations = {
|
| 1072 |
+
"macOS": "~/Library/Application Support/Claude/claude_desktop_config.json",
|
| 1073 |
+
"Windows": "%APPDATA%/Claude/claude_desktop_config.json",
|
| 1074 |
+
"Linux": "~/.config/Claude/claude_desktop_config.json"
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
return {
|
| 1078 |
+
"success": True,
|
| 1079 |
+
"app_name": app_name,
|
| 1080 |
+
"url": deployed_url,
|
| 1081 |
+
"mcp_endpoint": mcp_endpoint,
|
| 1082 |
+
"deployment_id": deployment_id,
|
| 1083 |
+
"security_scan": scan_result,
|
| 1084 |
+
"claude_desktop_config": claude_desktop_config,
|
| 1085 |
+
"config_locations": config_locations,
|
| 1086 |
+
"message": f"✅ Successfully deployed '{server_name}'\n🔗 URL: {deployed_url}\n📡 MCP: {mcp_endpoint}{security_msg}\n\n🔌 **Connect to Claude Desktop:**\nAdd this to your claude_desktop_config.json:\n```json\n{json.dumps(claude_desktop_config, indent=2)}\n```"
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
except subprocess.TimeoutExpired:
|
| 1090 |
+
return {"success": False, "error": "Deployment timed out after 5 minutes"}
|
| 1091 |
+
except Exception as e:
|
| 1092 |
+
return {"success": False, "error": str(e)}
|
| 1093 |
+
|
| 1094 |
+
|
| 1095 |
+
def list_deployments() -> dict:
|
| 1096 |
+
"""
|
| 1097 |
+
List all deployed MCP servers.
|
| 1098 |
+
|
| 1099 |
+
Returns:
|
| 1100 |
+
dict with deployment list and statistics
|
| 1101 |
+
"""
|
| 1102 |
+
try:
|
| 1103 |
+
with get_db() as db:
|
| 1104 |
+
deployments = Deployment.get_active_deployments(db)
|
| 1105 |
+
deployment_list = []
|
| 1106 |
+
for dep in deployments:
|
| 1107 |
+
deployment_list.append({
|
| 1108 |
+
"deployment_id": dep.deployment_id,
|
| 1109 |
+
"app_name": dep.app_name,
|
| 1110 |
+
"server_name": dep.server_name,
|
| 1111 |
+
"url": dep.url,
|
| 1112 |
+
"mcp_endpoint": dep.mcp_endpoint,
|
| 1113 |
+
"status": dep.status,
|
| 1114 |
+
"created_at": dep.created_at.isoformat() if dep.created_at else None,
|
| 1115 |
+
"description": dep.description,
|
| 1116 |
+
})
|
| 1117 |
+
|
| 1118 |
+
return {
|
| 1119 |
+
"success": True,
|
| 1120 |
+
"total": len(deployment_list),
|
| 1121 |
+
"deployments": deployment_list
|
| 1122 |
+
}
|
| 1123 |
+
except Exception as e:
|
| 1124 |
+
return {"success": False, "error": str(e)}
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def get_deployment_status(deployment_id: str = "", app_name: str = "") -> dict:
|
| 1128 |
+
"""
|
| 1129 |
+
Get detailed status of a deployed MCP server.
|
| 1130 |
+
|
| 1131 |
+
Args:
|
| 1132 |
+
deployment_id: The deployment ID
|
| 1133 |
+
app_name: Or the Modal app name
|
| 1134 |
+
|
| 1135 |
+
Returns:
|
| 1136 |
+
dict with deployment details and status
|
| 1137 |
+
"""
|
| 1138 |
+
try:
|
| 1139 |
+
with db_transaction() as db:
|
| 1140 |
+
deployment = None
|
| 1141 |
+
if deployment_id:
|
| 1142 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 1143 |
+
elif app_name:
|
| 1144 |
+
deployment = Deployment.get_by_app_name(db, app_name)
|
| 1145 |
+
|
| 1146 |
+
if not deployment:
|
| 1147 |
+
return {
|
| 1148 |
+
"success": False,
|
| 1149 |
+
"error": f"Deployment not found: {deployment_id or app_name}"
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
live = False
|
| 1153 |
+
try:
|
| 1154 |
+
import modal
|
| 1155 |
+
remote_func = modal.Function.from_name(deployment.app_name, "web")
|
| 1156 |
+
current_url = remote_func.get_web_url()
|
| 1157 |
+
if current_url != deployment.url:
|
| 1158 |
+
deployment.url = current_url
|
| 1159 |
+
deployment.mcp_endpoint = f"{current_url}/mcp/"
|
| 1160 |
+
live = True
|
| 1161 |
+
except Exception:
|
| 1162 |
+
live = False
|
| 1163 |
+
|
| 1164 |
+
# Return only deployment-related info (no usage metrics)
|
| 1165 |
+
return {
|
| 1166 |
+
"success": True,
|
| 1167 |
+
"live": live,
|
| 1168 |
+
"deployment_id": deployment.deployment_id,
|
| 1169 |
+
"app_name": deployment.app_name,
|
| 1170 |
+
"server_name": deployment.server_name,
|
| 1171 |
+
"url": deployment.url,
|
| 1172 |
+
"mcp_endpoint": deployment.mcp_endpoint,
|
| 1173 |
+
"description": deployment.description,
|
| 1174 |
+
"status": deployment.status,
|
| 1175 |
+
"category": deployment.category,
|
| 1176 |
+
"tags": deployment.tags or [],
|
| 1177 |
+
"author": deployment.author,
|
| 1178 |
+
"version": deployment.version,
|
| 1179 |
+
"documentation": deployment.documentation,
|
| 1180 |
+
"created_at": deployment.created_at.isoformat() if deployment.created_at else None,
|
| 1181 |
+
"updated_at": deployment.updated_at.isoformat() if deployment.updated_at else None,
|
| 1182 |
+
"packages": [pkg.package_name for pkg in deployment.packages],
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
except Exception as e:
|
| 1186 |
+
return {"success": False, "error": str(e)}
|
| 1187 |
+
|
| 1188 |
+
|
| 1189 |
+
def delete_deployment(deployment_id: str = "", app_name: str = "", confirm: bool = False) -> dict:
|
| 1190 |
+
"""
|
| 1191 |
+
Delete a deployed MCP server from Modal.
|
| 1192 |
+
|
| 1193 |
+
Args:
|
| 1194 |
+
deployment_id: The deployment ID to delete
|
| 1195 |
+
app_name: Or the Modal app name
|
| 1196 |
+
confirm: Must be True to confirm deletion
|
| 1197 |
+
|
| 1198 |
+
Returns:
|
| 1199 |
+
dict with deletion status
|
| 1200 |
+
"""
|
| 1201 |
+
if not confirm:
|
| 1202 |
+
return {"success": False, "error": "Must set confirm=True to delete deployment"}
|
| 1203 |
+
|
| 1204 |
+
try:
|
| 1205 |
+
with db_transaction() as db:
|
| 1206 |
+
deployment = None
|
| 1207 |
+
if deployment_id:
|
| 1208 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 1209 |
+
elif app_name:
|
| 1210 |
+
deployment = Deployment.get_by_app_name(db, app_name)
|
| 1211 |
+
|
| 1212 |
+
if not deployment:
|
| 1213 |
+
return {"success": False, "error": f"Deployment not found: {deployment_id or app_name}"}
|
| 1214 |
+
|
| 1215 |
+
target_app_name = deployment.app_name
|
| 1216 |
+
found_id = deployment.deployment_id
|
| 1217 |
+
|
| 1218 |
+
# Stop the Modal app
|
| 1219 |
+
try:
|
| 1220 |
+
subprocess.run(
|
| 1221 |
+
["modal", "app", "stop", target_app_name],
|
| 1222 |
+
capture_output=True,
|
| 1223 |
+
text=True,
|
| 1224 |
+
timeout=60
|
| 1225 |
+
)
|
| 1226 |
+
except subprocess.TimeoutExpired:
|
| 1227 |
+
return {"success": False, "error": "Modal app stop timed out"}
|
| 1228 |
+
|
| 1229 |
+
# Soft delete in database (no local files to clean up)
|
| 1230 |
+
deployment.soft_delete()
|
| 1231 |
+
DeploymentHistory.log_event(
|
| 1232 |
+
db=db,
|
| 1233 |
+
deployment_id=found_id,
|
| 1234 |
+
action="deleted",
|
| 1235 |
+
details={"app_name": target_app_name},
|
| 1236 |
+
)
|
| 1237 |
+
|
| 1238 |
+
return {
|
| 1239 |
+
"success": True,
|
| 1240 |
+
"app_name": target_app_name,
|
| 1241 |
+
"deployment_id": found_id,
|
| 1242 |
+
"message": f"✅ Deleted deployment '{target_app_name}'"
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
except Exception as e:
|
| 1246 |
+
return {"success": False, "error": str(e)}
|
| 1247 |
+
|
| 1248 |
+
|
| 1249 |
+
def get_deployment_code(deployment_id: str) -> dict:
|
| 1250 |
+
"""
|
| 1251 |
+
Get the current MCP tools code for a deployment.
|
| 1252 |
+
|
| 1253 |
+
Args:
|
| 1254 |
+
deployment_id: The deployment ID
|
| 1255 |
+
|
| 1256 |
+
Returns:
|
| 1257 |
+
dict with code, packages, and tool information
|
| 1258 |
+
"""
|
| 1259 |
+
try:
|
| 1260 |
+
with get_db() as db:
|
| 1261 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 1262 |
+
if not deployment:
|
| 1263 |
+
return {"success": False, "error": f"Deployment not found: {deployment_id}"}
|
| 1264 |
+
|
| 1265 |
+
original_file = DeploymentFile.get_file(db, deployment_id, "original_tools")
|
| 1266 |
+
if not original_file:
|
| 1267 |
+
return {"success": False, "error": f"No code found for deployment: {deployment_id}"}
|
| 1268 |
+
|
| 1269 |
+
# Get packages using the relationship or direct query
|
| 1270 |
+
packages = db.query(DeploymentPackage).filter_by(deployment_id=deployment_id).all()
|
| 1271 |
+
package_list = [pkg.package_name for pkg in packages]
|
| 1272 |
+
|
| 1273 |
+
tools_manifest = DeploymentFile.get_file(db, deployment_id, "tools_manifest")
|
| 1274 |
+
tools_list = []
|
| 1275 |
+
if tools_manifest and tools_manifest.file_content:
|
| 1276 |
+
try:
|
| 1277 |
+
tools_list = json.loads(tools_manifest.file_content)
|
| 1278 |
+
except json.JSONDecodeError:
|
| 1279 |
+
tools_list = []
|
| 1280 |
+
|
| 1281 |
+
return {
|
| 1282 |
+
"success": True,
|
| 1283 |
+
"deployment_id": deployment.deployment_id,
|
| 1284 |
+
"server_name": deployment.server_name,
|
| 1285 |
+
"description": deployment.description or "",
|
| 1286 |
+
"url": deployment.url or "",
|
| 1287 |
+
"mcp_endpoint": deployment.mcp_endpoint or "",
|
| 1288 |
+
"code": original_file.file_content or "",
|
| 1289 |
+
"packages": package_list,
|
| 1290 |
+
"tools": tools_list,
|
| 1291 |
+
"message": f"✅ Retrieved code for '{deployment.server_name}'"
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
except Exception as e:
|
| 1295 |
+
return {"success": False, "error": str(e)}
|
| 1296 |
+
|
| 1297 |
+
|
| 1298 |
+
def _validate_python_syntax(code: str) -> dict:
|
| 1299 |
+
"""
|
| 1300 |
+
Validate Python code syntax.
|
| 1301 |
+
|
| 1302 |
+
Args:
|
| 1303 |
+
code: Python code to validate
|
| 1304 |
+
|
| 1305 |
+
Returns:
|
| 1306 |
+
dict with success status and error message if invalid
|
| 1307 |
+
"""
|
| 1308 |
+
try:
|
| 1309 |
+
compile(code, '<string>', 'exec')
|
| 1310 |
+
return {"valid": True}
|
| 1311 |
+
except SyntaxError as e:
|
| 1312 |
+
return {
|
| 1313 |
+
"valid": False,
|
| 1314 |
+
"error": f"Syntax error at line {e.lineno}: {e.msg}"
|
| 1315 |
+
}
|
| 1316 |
+
except Exception as e:
|
| 1317 |
+
return {
|
| 1318 |
+
"valid": False,
|
| 1319 |
+
"error": f"Validation error: {str(e)}"
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
|
| 1323 |
+
def _validate_packages(packages: list[str]) -> dict:
|
| 1324 |
+
"""
|
| 1325 |
+
Validate package names.
|
| 1326 |
+
|
| 1327 |
+
Args:
|
| 1328 |
+
packages: List of package names to validate
|
| 1329 |
+
|
| 1330 |
+
Returns:
|
| 1331 |
+
dict with validation results
|
| 1332 |
+
"""
|
| 1333 |
+
if not packages:
|
| 1334 |
+
return {"valid": True, "packages": []}
|
| 1335 |
+
|
| 1336 |
+
# Basic validation: check for valid package name format
|
| 1337 |
+
package_pattern = re.compile(r'^[a-zA-Z0-9_\-\.]+$')
|
| 1338 |
+
|
| 1339 |
+
invalid_packages = []
|
| 1340 |
+
valid_packages = []
|
| 1341 |
+
|
| 1342 |
+
for pkg in packages:
|
| 1343 |
+
if not pkg or not package_pattern.match(pkg):
|
| 1344 |
+
invalid_packages.append(pkg)
|
| 1345 |
+
else:
|
| 1346 |
+
valid_packages.append(pkg)
|
| 1347 |
+
|
| 1348 |
+
if invalid_packages:
|
| 1349 |
+
return {
|
| 1350 |
+
"valid": False,
|
| 1351 |
+
"error": f"Invalid package names: {', '.join(invalid_packages)}",
|
| 1352 |
+
"invalid_packages": invalid_packages
|
| 1353 |
+
}
|
| 1354 |
+
|
| 1355 |
+
return {
|
| 1356 |
+
"valid": True,
|
| 1357 |
+
"packages": valid_packages
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
|
| 1361 |
+
def _backup_deployment_state(db, deployment: Deployment) -> dict:
|
| 1362 |
+
"""
|
| 1363 |
+
Backup current deployment state to deployment_history.
|
| 1364 |
+
|
| 1365 |
+
Args:
|
| 1366 |
+
db: Database session
|
| 1367 |
+
deployment: Deployment object to backup
|
| 1368 |
+
|
| 1369 |
+
Returns:
|
| 1370 |
+
dict with backup details
|
| 1371 |
+
"""
|
| 1372 |
+
try:
|
| 1373 |
+
# Get current packages using direct query
|
| 1374 |
+
packages = db.query(DeploymentPackage).filter_by(deployment_id=deployment.deployment_id).all()
|
| 1375 |
+
package_list = [pkg.package_name for pkg in packages]
|
| 1376 |
+
|
| 1377 |
+
# Get current files
|
| 1378 |
+
original_file = DeploymentFile.get_file(db, deployment.deployment_id, "original_tools")
|
| 1379 |
+
app_file = DeploymentFile.get_file(db, deployment.deployment_id, "app")
|
| 1380 |
+
|
| 1381 |
+
backup_details = {
|
| 1382 |
+
"server_name": deployment.server_name,
|
| 1383 |
+
"description": deployment.description,
|
| 1384 |
+
"url": deployment.url,
|
| 1385 |
+
"mcp_endpoint": deployment.mcp_endpoint,
|
| 1386 |
+
"status": deployment.status,
|
| 1387 |
+
"packages": package_list,
|
| 1388 |
+
"original_tools_code": original_file.file_content if original_file else None,
|
| 1389 |
+
"app_code": app_file.file_content if app_file else None,
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
# Log backup event
|
| 1393 |
+
DeploymentHistory.log_event(
|
| 1394 |
+
db=db,
|
| 1395 |
+
deployment_id=deployment.deployment_id,
|
| 1396 |
+
action="pre_update_backup",
|
| 1397 |
+
details=backup_details
|
| 1398 |
+
)
|
| 1399 |
+
|
| 1400 |
+
return {
|
| 1401 |
+
"success": True,
|
| 1402 |
+
"backup_details": backup_details
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
except Exception as e:
|
| 1406 |
+
return {
|
| 1407 |
+
"success": False,
|
| 1408 |
+
"error": f"Backup failed: {str(e)}"
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
|
| 1412 |
+
def _test_updated_deployment(url: str, timeout: int = 30) -> dict:
|
| 1413 |
+
"""
|
| 1414 |
+
Test if an updated deployment is responsive.
|
| 1415 |
+
|
| 1416 |
+
Args:
|
| 1417 |
+
url: Deployment URL to test
|
| 1418 |
+
timeout: Request timeout in seconds
|
| 1419 |
+
|
| 1420 |
+
Returns:
|
| 1421 |
+
dict with test results
|
| 1422 |
+
"""
|
| 1423 |
+
try:
|
| 1424 |
+
import requests
|
| 1425 |
+
|
| 1426 |
+
response = requests.get(url, timeout=timeout)
|
| 1427 |
+
|
| 1428 |
+
if response.status_code == 200:
|
| 1429 |
+
return {
|
| 1430 |
+
"success": True,
|
| 1431 |
+
"responsive": True,
|
| 1432 |
+
"status_code": response.status_code
|
| 1433 |
+
}
|
| 1434 |
+
else:
|
| 1435 |
+
return {
|
| 1436 |
+
"success": False,
|
| 1437 |
+
"responsive": False,
|
| 1438 |
+
"status_code": response.status_code,
|
| 1439 |
+
"error": f"Server returned status {response.status_code}"
|
| 1440 |
+
}
|
| 1441 |
+
|
| 1442 |
+
except Exception as e:
|
| 1443 |
+
return {
|
| 1444 |
+
"success": False,
|
| 1445 |
+
"responsive": False,
|
| 1446 |
+
"error": f"Test failed: {str(e)}"
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
|
| 1450 |
+
def update_deployment_code(
|
| 1451 |
+
deployment_id: str,
|
| 1452 |
+
mcp_tools_code: str = None,
|
| 1453 |
+
extra_pip_packages: list[str] = None,
|
| 1454 |
+
server_name: str = None,
|
| 1455 |
+
description: str = None
|
| 1456 |
+
) -> dict:
|
| 1457 |
+
"""
|
| 1458 |
+
Update deployment code and/or packages with redeployment to Modal.
|
| 1459 |
+
|
| 1460 |
+
This will redeploy the MCP server with new code/packages while preserving
|
| 1461 |
+
the same URL (by reusing the same Modal app_name). The deployment will
|
| 1462 |
+
experience brief downtime (5-10 seconds) during the update.
|
| 1463 |
+
|
| 1464 |
+
Args:
|
| 1465 |
+
deployment_id: The deployment ID to update (e.g., "deploy-mcp-xxx-xxxxxx")
|
| 1466 |
+
mcp_tools_code: New MCP tools code (optional - triggers redeployment)
|
| 1467 |
+
extra_pip_packages: New package list (optional - triggers redeployment)
|
| 1468 |
+
server_name: New server name (optional)
|
| 1469 |
+
description: New description (optional)
|
| 1470 |
+
|
| 1471 |
+
Returns:
|
| 1472 |
+
dict with update status, URL, and deployment info
|
| 1473 |
+
"""
|
| 1474 |
+
try:
|
| 1475 |
+
# Validate at least one field is provided
|
| 1476 |
+
if not any([mcp_tools_code, extra_pip_packages is not None, server_name, description is not None]):
|
| 1477 |
+
return {
|
| 1478 |
+
"success": False,
|
| 1479 |
+
"error": "Must provide at least one field to update"
|
| 1480 |
+
}
|
| 1481 |
+
|
| 1482 |
+
with db_transaction() as db:
|
| 1483 |
+
# Find deployment
|
| 1484 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 1485 |
+
|
| 1486 |
+
if not deployment:
|
| 1487 |
+
return {
|
| 1488 |
+
"success": False,
|
| 1489 |
+
"error": f"Deployment not found: {deployment_id}"
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
+
if deployment.is_deleted:
|
| 1493 |
+
return {
|
| 1494 |
+
"success": False,
|
| 1495 |
+
"error": "Cannot update deleted deployment"
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
# Track what we're updating
|
| 1499 |
+
updated_fields = []
|
| 1500 |
+
requires_redeployment = False
|
| 1501 |
+
|
| 1502 |
+
# === VALIDATION PHASE ===
|
| 1503 |
+
|
| 1504 |
+
# Validate new code syntax if provided
|
| 1505 |
+
if mcp_tools_code:
|
| 1506 |
+
validation = _validate_python_syntax(mcp_tools_code)
|
| 1507 |
+
if not validation["valid"]:
|
| 1508 |
+
return {
|
| 1509 |
+
"success": False,
|
| 1510 |
+
"error": f"Invalid Python code: {validation['error']}"
|
| 1511 |
+
}
|
| 1512 |
+
requires_redeployment = True
|
| 1513 |
+
updated_fields.append("mcp_tools_code")
|
| 1514 |
+
|
| 1515 |
+
# Validate packages if provided
|
| 1516 |
+
if extra_pip_packages is not None:
|
| 1517 |
+
validation = _validate_packages(extra_pip_packages)
|
| 1518 |
+
if not validation["valid"]:
|
| 1519 |
+
return {
|
| 1520 |
+
"success": False,
|
| 1521 |
+
"error": f"Invalid packages: {validation['error']}"
|
| 1522 |
+
}
|
| 1523 |
+
requires_redeployment = True
|
| 1524 |
+
updated_fields.append("packages")
|
| 1525 |
+
|
| 1526 |
+
# Validate metadata
|
| 1527 |
+
if server_name:
|
| 1528 |
+
if not server_name.strip():
|
| 1529 |
+
return {
|
| 1530 |
+
"success": False,
|
| 1531 |
+
"error": "server_name cannot be empty"
|
| 1532 |
+
}
|
| 1533 |
+
updated_fields.append("server_name")
|
| 1534 |
+
|
| 1535 |
+
if description is not None:
|
| 1536 |
+
updated_fields.append("description")
|
| 1537 |
+
|
| 1538 |
+
# === BACKUP PHASE ===
|
| 1539 |
+
|
| 1540 |
+
# Always backup before any update
|
| 1541 |
+
backup_result = _backup_deployment_state(db, deployment)
|
| 1542 |
+
if not backup_result["success"]:
|
| 1543 |
+
return {
|
| 1544 |
+
"success": False,
|
| 1545 |
+
"error": f"Failed to backup deployment: {backup_result['error']}"
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
# === UPDATE PHASE ===
|
| 1549 |
+
|
| 1550 |
+
# If only metadata changed, skip redeployment
|
| 1551 |
+
if not requires_redeployment:
|
| 1552 |
+
# Update metadata only
|
| 1553 |
+
if server_name:
|
| 1554 |
+
deployment.server_name = server_name.strip()
|
| 1555 |
+
if description is not None:
|
| 1556 |
+
deployment.description = description
|
| 1557 |
+
|
| 1558 |
+
# Log metadata-only update
|
| 1559 |
+
DeploymentHistory.log_event(
|
| 1560 |
+
db=db,
|
| 1561 |
+
deployment_id=deployment_id,
|
| 1562 |
+
action="metadata_updated",
|
| 1563 |
+
details={
|
| 1564 |
+
"updated_fields": updated_fields,
|
| 1565 |
+
"redeployed": False
|
| 1566 |
+
}
|
| 1567 |
+
)
|
| 1568 |
+
|
| 1569 |
+
return {
|
| 1570 |
+
"success": True,
|
| 1571 |
+
"redeployed": False,
|
| 1572 |
+
"updated_fields": updated_fields,
|
| 1573 |
+
"deployment": {
|
| 1574 |
+
"deployment_id": deployment.deployment_id,
|
| 1575 |
+
"app_name": deployment.app_name,
|
| 1576 |
+
"server_name": deployment.server_name,
|
| 1577 |
+
"url": deployment.url,
|
| 1578 |
+
"mcp_endpoint": deployment.mcp_endpoint,
|
| 1579 |
+
"description": deployment.description,
|
| 1580 |
+
"status": deployment.status,
|
| 1581 |
+
"category": deployment.category,
|
| 1582 |
+
"tags": deployment.tags or [],
|
| 1583 |
+
"author": deployment.author,
|
| 1584 |
+
"version": deployment.version,
|
| 1585 |
+
"created_at": deployment.created_at.isoformat() if deployment.created_at else None,
|
| 1586 |
+
"updated_at": deployment.updated_at.isoformat() if deployment.updated_at else None,
|
| 1587 |
+
},
|
| 1588 |
+
"message": f"✅ Updated metadata for '{deployment_id}' (no redeployment needed)"
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
# === REDEPLOYMENT PHASE ===
|
| 1592 |
+
|
| 1593 |
+
# Get current or new values
|
| 1594 |
+
final_server_name = server_name.strip() if server_name else deployment.server_name
|
| 1595 |
+
final_tools_code = mcp_tools_code if mcp_tools_code else None
|
| 1596 |
+
final_packages = extra_pip_packages if extra_pip_packages is not None else None
|
| 1597 |
+
|
| 1598 |
+
# If code not provided, get from database
|
| 1599 |
+
if not final_tools_code:
|
| 1600 |
+
original_file = DeploymentFile.get_file(db, deployment_id, "original_tools")
|
| 1601 |
+
if not original_file:
|
| 1602 |
+
return {
|
| 1603 |
+
"success": False,
|
| 1604 |
+
"error": "Cannot find original tools code in database"
|
| 1605 |
+
}
|
| 1606 |
+
final_tools_code = original_file.file_content
|
| 1607 |
+
|
| 1608 |
+
# If packages not provided, get from database
|
| 1609 |
+
if final_packages is None:
|
| 1610 |
+
current_packages = db.query(DeploymentPackage).filter_by(deployment_id=deployment_id).all()
|
| 1611 |
+
final_packages = [pkg.package_name for pkg in current_packages]
|
| 1612 |
+
|
| 1613 |
+
# Extract imports and prepare dependencies
|
| 1614 |
+
detected_imports, cleaned_code = _extract_imports_and_code(final_tools_code)
|
| 1615 |
+
all_packages = list(set(detected_imports + final_packages))
|
| 1616 |
+
|
| 1617 |
+
# Filter out standard library packages
|
| 1618 |
+
stdlib = {'os', 'sys', 'json', 're', 'datetime', 'time', 'typing', 'pathlib',
|
| 1619 |
+
'collections', 'functools', 'itertools', 'math', 'random', 'string',
|
| 1620 |
+
'hashlib', 'base64', 'urllib', 'zoneinfo', 'asyncio'}
|
| 1621 |
+
extra_deps = [pkg for pkg in all_packages if pkg.lower() not in stdlib]
|
| 1622 |
+
|
| 1623 |
+
# === SECURITY SCAN PHASE ===
|
| 1624 |
+
from utils.security_scanner import scan_code_for_security
|
| 1625 |
+
|
| 1626 |
+
scan_result = scan_code_for_security(
|
| 1627 |
+
code=cleaned_code,
|
| 1628 |
+
context={
|
| 1629 |
+
"server_name": final_server_name,
|
| 1630 |
+
"packages": extra_deps,
|
| 1631 |
+
"description": description or deployment.description or "",
|
| 1632 |
+
"deployment_id": deployment_id
|
| 1633 |
+
}
|
| 1634 |
+
)
|
| 1635 |
+
|
| 1636 |
+
# Check if redeployment should be blocked
|
| 1637 |
+
if scan_result["severity"] in ["high", "critical"]:
|
| 1638 |
+
return {
|
| 1639 |
+
"success": False,
|
| 1640 |
+
"error": "Security vulnerabilities detected - update blocked",
|
| 1641 |
+
"security_scan": scan_result,
|
| 1642 |
+
"severity": scan_result["severity"],
|
| 1643 |
+
"message": f"🚫 Update blocked due to {scan_result['severity']} severity security issues"
|
| 1644 |
+
}
|
| 1645 |
+
|
| 1646 |
+
# Format extra dependencies for template
|
| 1647 |
+
extra_deps_str = ',\n '.join(f'"{pkg}"' for pkg in extra_deps) if extra_deps else ''
|
| 1648 |
+
user_code_indented = _indent_code(cleaned_code, spaces=4)
|
| 1649 |
+
|
| 1650 |
+
# Get environment variables for Modal deployment
|
| 1651 |
+
env_vars = _get_env_vars_for_deployment()
|
| 1652 |
+
env_vars_setup = _generate_env_vars_setup(env_vars)
|
| 1653 |
+
|
| 1654 |
+
# Generate webhook configuration
|
| 1655 |
+
webhook_url = os.getenv('MCP_WEBHOOK_URL', '')
|
| 1656 |
+
if not webhook_url:
|
| 1657 |
+
base_url = os.getenv('MCP_BASE_URL', 'http://localhost:7860')
|
| 1658 |
+
webhook_url = f"{base_url}/api/webhook/usage"
|
| 1659 |
+
|
| 1660 |
+
webhook_env_vars_code = f'''
|
| 1661 |
+
secrets_dict["MCP_WEBHOOK_URL"] = "{webhook_url}"
|
| 1662 |
+
secrets_dict["MCP_DEPLOYMENT_ID"] = "{deployment_id}"
|
| 1663 |
+
'''
|
| 1664 |
+
|
| 1665 |
+
# Generate Modal wrapper code (reuse same app_name to preserve URL)
|
| 1666 |
+
# Note: Tracking removed - will be developed later
|
| 1667 |
+
modal_code = MODAL_WRAPPER_TEMPLATE.format(
|
| 1668 |
+
app_name=deployment.app_name, # IMPORTANT: Reuse existing app_name
|
| 1669 |
+
server_name=final_server_name,
|
| 1670 |
+
timestamp=datetime.now().isoformat(),
|
| 1671 |
+
extra_deps=extra_deps_str,
|
| 1672 |
+
user_code_indented=user_code_indented,
|
| 1673 |
+
env_vars_setup=env_vars_setup,
|
| 1674 |
+
webhook_env_vars=webhook_env_vars_code
|
| 1675 |
+
)
|
| 1676 |
+
|
| 1677 |
+
# Create temporary deployment directory (will be cleaned up after deployment)
|
| 1678 |
+
temp_deploy_dir = tempfile.mkdtemp(prefix=f"mcp_update_{deployment.app_name}_")
|
| 1679 |
+
try:
|
| 1680 |
+
deploy_dir_path = Path(temp_deploy_dir)
|
| 1681 |
+
deploy_file = deploy_dir_path / "app.py"
|
| 1682 |
+
deploy_file.write_text(modal_code)
|
| 1683 |
+
(deploy_dir_path / "original_tools.py").write_text(final_tools_code)
|
| 1684 |
+
|
| 1685 |
+
# Deploy to Modal (reusing same app_name)
|
| 1686 |
+
result = subprocess.run(
|
| 1687 |
+
["modal", "deploy", str(deploy_file)],
|
| 1688 |
+
capture_output=True,
|
| 1689 |
+
text=True,
|
| 1690 |
+
timeout=300
|
| 1691 |
+
)
|
| 1692 |
+
finally:
|
| 1693 |
+
# Clean up temporary deployment directory
|
| 1694 |
+
try:
|
| 1695 |
+
shutil.rmtree(temp_deploy_dir)
|
| 1696 |
+
except Exception:
|
| 1697 |
+
pass # Ignore cleanup errors
|
| 1698 |
+
|
| 1699 |
+
if result.returncode != 0:
|
| 1700 |
+
return {
|
| 1701 |
+
"success": False,
|
| 1702 |
+
"error": "Redeployment failed",
|
| 1703 |
+
"stdout": result.stdout,
|
| 1704 |
+
"stderr": result.stderr
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
# Extract URL from deployment output
|
| 1708 |
+
url_match = re.search(r'https://[a-zA-Z0-9-]+--[a-zA-Z0-9-]+\.modal\.run', result.stdout)
|
| 1709 |
+
deployed_url = url_match.group(0) if url_match else None
|
| 1710 |
+
|
| 1711 |
+
if not deployed_url:
|
| 1712 |
+
try:
|
| 1713 |
+
import modal
|
| 1714 |
+
remote_func = modal.Function.from_name(deployment.app_name, "web")
|
| 1715 |
+
deployed_url = remote_func.get_web_url()
|
| 1716 |
+
except Exception:
|
| 1717 |
+
deployed_url = deployment.url
|
| 1718 |
+
|
| 1719 |
+
# === DATABASE UPDATE PHASE ===
|
| 1720 |
+
|
| 1721 |
+
# Update deployment record
|
| 1722 |
+
if server_name:
|
| 1723 |
+
deployment.server_name = server_name.strip()
|
| 1724 |
+
if description is not None:
|
| 1725 |
+
deployment.description = description
|
| 1726 |
+
deployment.url = deployed_url
|
| 1727 |
+
deployment.mcp_endpoint = f"{deployed_url}/mcp/" if deployed_url else None
|
| 1728 |
+
|
| 1729 |
+
# Update packages
|
| 1730 |
+
db.query(DeploymentPackage).filter(
|
| 1731 |
+
DeploymentPackage.deployment_id == deployment_id
|
| 1732 |
+
).delete(synchronize_session=False)
|
| 1733 |
+
|
| 1734 |
+
for package in extra_deps:
|
| 1735 |
+
pkg = DeploymentPackage(
|
| 1736 |
+
deployment_id=deployment_id,
|
| 1737 |
+
package_name=package,
|
| 1738 |
+
)
|
| 1739 |
+
db.add(pkg)
|
| 1740 |
+
|
| 1741 |
+
# Update deployment files in database (no local file paths)
|
| 1742 |
+
app_file_record = DeploymentFile.get_file(db, deployment_id, "app")
|
| 1743 |
+
if app_file_record:
|
| 1744 |
+
app_file_record.file_content = modal_code
|
| 1745 |
+
app_file_record.file_path = "" # No persistent local file
|
| 1746 |
+
else:
|
| 1747 |
+
db.add(DeploymentFile(
|
| 1748 |
+
deployment_id=deployment_id,
|
| 1749 |
+
file_type="app",
|
| 1750 |
+
file_path="", # No persistent local file
|
| 1751 |
+
file_content=modal_code,
|
| 1752 |
+
))
|
| 1753 |
+
|
| 1754 |
+
original_file_record = DeploymentFile.get_file(db, deployment_id, "original_tools")
|
| 1755 |
+
if original_file_record:
|
| 1756 |
+
original_file_record.file_content = final_tools_code
|
| 1757 |
+
original_file_record.file_path = "" # No persistent local file
|
| 1758 |
+
else:
|
| 1759 |
+
db.add(DeploymentFile(
|
| 1760 |
+
deployment_id=deployment_id,
|
| 1761 |
+
file_type="original_tools",
|
| 1762 |
+
file_path="", # No persistent local file
|
| 1763 |
+
file_content=final_tools_code,
|
| 1764 |
+
))
|
| 1765 |
+
|
| 1766 |
+
# Update tools manifest
|
| 1767 |
+
tools_list = _extract_tool_definitions(cleaned_code)
|
| 1768 |
+
tools_manifest_record = DeploymentFile.get_file(db, deployment_id, "tools_manifest")
|
| 1769 |
+
if tools_manifest_record:
|
| 1770 |
+
tools_manifest_record.file_content = json.dumps(tools_list, indent=2)
|
| 1771 |
+
else:
|
| 1772 |
+
db.add(DeploymentFile(
|
| 1773 |
+
deployment_id=deployment_id,
|
| 1774 |
+
file_type="tools_manifest",
|
| 1775 |
+
file_path="",
|
| 1776 |
+
file_content=json.dumps(tools_list, indent=2),
|
| 1777 |
+
))
|
| 1778 |
+
|
| 1779 |
+
# Log code update event
|
| 1780 |
+
DeploymentHistory.log_event(
|
| 1781 |
+
db=db,
|
| 1782 |
+
deployment_id=deployment_id,
|
| 1783 |
+
action="code_updated",
|
| 1784 |
+
details={
|
| 1785 |
+
"updated_fields": updated_fields,
|
| 1786 |
+
"redeployed": True,
|
| 1787 |
+
"new_url": deployed_url,
|
| 1788 |
+
"packages": extra_deps,
|
| 1789 |
+
}
|
| 1790 |
+
)
|
| 1791 |
+
|
| 1792 |
+
security_msg = ""
|
| 1793 |
+
if scan_result["severity"] in ["low", "medium"]:
|
| 1794 |
+
security_msg = f"\n⚠️ Security Warning ({scan_result['severity']}): {scan_result['explanation']}"
|
| 1795 |
+
|
| 1796 |
+
# Generate Claude Desktop integration config
|
| 1797 |
+
mcp_endpoint = f"{deployed_url}/mcp/" if deployed_url else None
|
| 1798 |
+
claude_desktop_config = {
|
| 1799 |
+
final_server_name: {
|
| 1800 |
+
"command": "npx",
|
| 1801 |
+
"args": [
|
| 1802 |
+
"mcp-remote",
|
| 1803 |
+
mcp_endpoint
|
| 1804 |
+
]
|
| 1805 |
+
}
|
| 1806 |
+
} if mcp_endpoint else {}
|
| 1807 |
+
|
| 1808 |
+
config_locations = {
|
| 1809 |
+
"macOS": "~/Library/Application Support/Claude/claude_desktop_config.json",
|
| 1810 |
+
"Windows": "%APPDATA%/Claude/claude_desktop_config.json",
|
| 1811 |
+
"Linux": "~/.config/Claude/claude_desktop_config.json"
|
| 1812 |
+
}
|
| 1813 |
+
|
| 1814 |
+
return {
|
| 1815 |
+
"success": True,
|
| 1816 |
+
"redeployed": True,
|
| 1817 |
+
"url": deployed_url,
|
| 1818 |
+
"mcp_endpoint": mcp_endpoint,
|
| 1819 |
+
"updated_fields": updated_fields,
|
| 1820 |
+
"deployment": {
|
| 1821 |
+
"deployment_id": deployment.deployment_id,
|
| 1822 |
+
"app_name": deployment.app_name,
|
| 1823 |
+
"server_name": deployment.server_name,
|
| 1824 |
+
"url": deployment.url,
|
| 1825 |
+
"mcp_endpoint": deployment.mcp_endpoint,
|
| 1826 |
+
"description": deployment.description,
|
| 1827 |
+
"status": deployment.status,
|
| 1828 |
+
"category": deployment.category,
|
| 1829 |
+
"tags": deployment.tags or [],
|
| 1830 |
+
"author": deployment.author,
|
| 1831 |
+
"version": deployment.version,
|
| 1832 |
+
"created_at": deployment.created_at.isoformat() if deployment.created_at else None,
|
| 1833 |
+
"updated_at": deployment.updated_at.isoformat() if deployment.updated_at else None,
|
| 1834 |
+
},
|
| 1835 |
+
"security_scan": scan_result,
|
| 1836 |
+
"claude_desktop_config": claude_desktop_config,
|
| 1837 |
+
"config_locations": config_locations,
|
| 1838 |
+
"message": f"✅ Successfully updated '{final_server_name}'\n🔗 URL: {deployed_url}{security_msg}\n\n🔌 **Connect to Claude Desktop:**\nAdd this to your claude_desktop_config.json:\n```json\n{json.dumps(claude_desktop_config, indent=2)}\n```"
|
| 1839 |
+
}
|
| 1840 |
+
|
| 1841 |
+
except subprocess.TimeoutExpired:
|
| 1842 |
+
return {"success": False, "error": "Deployment timed out after 5 minutes"}
|
| 1843 |
+
except Exception as e:
|
| 1844 |
+
return {"success": False, "error": str(e)}
|
| 1845 |
+
|
| 1846 |
+
|
| 1847 |
+
def _create_deployment_tools() -> List[gr.Interface]:
|
| 1848 |
+
"""
|
| 1849 |
+
Create and return all deployment-related Gradio interfaces.
|
| 1850 |
+
Most tools are registered via @gr.api() decorator above.
|
| 1851 |
+
|
| 1852 |
+
Returns:
|
| 1853 |
+
List of Gradio interfaces (empty list since tools use @gr.api())
|
| 1854 |
+
"""
|
| 1855 |
+
return []
|
mcp_tools/security_tools.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Security Tools Module
|
| 3 |
+
|
| 4 |
+
Gradio-based MCP tools for security scanning.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from typing import List
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def scan_deployment_security(
|
| 13 |
+
mcp_tools_code: str,
|
| 14 |
+
server_name: str = "Unknown",
|
| 15 |
+
extra_pip_packages: str = "",
|
| 16 |
+
description: str = ""
|
| 17 |
+
) -> dict:
|
| 18 |
+
"""
|
| 19 |
+
Manually scan MCP code for security vulnerabilities without deploying.
|
| 20 |
+
|
| 21 |
+
Use this tool to check code for security issues before deploying or updating.
|
| 22 |
+
The scan uses AI to detect:
|
| 23 |
+
- Code injection vulnerabilities (SQL, command, etc.)
|
| 24 |
+
- Malicious network behavior
|
| 25 |
+
- Resource abuse patterns
|
| 26 |
+
- Destructive operations
|
| 27 |
+
- Known malicious packages
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
mcp_tools_code: Python code defining your MCP tools
|
| 31 |
+
server_name: Name for context (default: "Unknown")
|
| 32 |
+
extra_pip_packages: Comma-separated list of additional packages
|
| 33 |
+
description: Optional description for context
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
dict with scan results and recommendations
|
| 37 |
+
"""
|
| 38 |
+
try:
|
| 39 |
+
# Convert comma-separated packages to list
|
| 40 |
+
extra_pip_packages_list = [p.strip() for p in extra_pip_packages.split(",")] if extra_pip_packages else []
|
| 41 |
+
|
| 42 |
+
# Extract imports
|
| 43 |
+
def _extract_imports_and_code_local(user_code: str) -> tuple[list[str], str]:
|
| 44 |
+
"""Extract import statements"""
|
| 45 |
+
lines = user_code.strip().split('\n')
|
| 46 |
+
imports = []
|
| 47 |
+
code_lines = []
|
| 48 |
+
|
| 49 |
+
for line in lines:
|
| 50 |
+
stripped = line.strip()
|
| 51 |
+
if stripped.startswith('import ') or stripped.startswith('from '):
|
| 52 |
+
if stripped.startswith('from '):
|
| 53 |
+
match = re.match(r'from\s+(\w+)', stripped)
|
| 54 |
+
if match:
|
| 55 |
+
imports.append(match.group(1))
|
| 56 |
+
else:
|
| 57 |
+
match = re.match(r'import\s+(\w+)', stripped)
|
| 58 |
+
if match:
|
| 59 |
+
imports.append(match.group(1))
|
| 60 |
+
code_lines.append(line)
|
| 61 |
+
|
| 62 |
+
return imports, '\n'.join(code_lines)
|
| 63 |
+
|
| 64 |
+
detected_imports, cleaned_code = _extract_imports_and_code_local(mcp_tools_code)
|
| 65 |
+
all_packages = list(set(detected_imports + extra_pip_packages_list))
|
| 66 |
+
|
| 67 |
+
# Filter out standard library packages
|
| 68 |
+
stdlib = {'os', 'sys', 'json', 're', 'datetime', 'time', 'typing', 'pathlib',
|
| 69 |
+
'collections', 'functools', 'itertools', 'math', 'random', 'string',
|
| 70 |
+
'hashlib', 'base64', 'urllib', 'zoneinfo', 'asyncio'}
|
| 71 |
+
extra_deps = [pkg for pkg in all_packages if pkg.lower() not in stdlib]
|
| 72 |
+
|
| 73 |
+
# Perform security scan
|
| 74 |
+
from utils.security_scanner import scan_code_for_security
|
| 75 |
+
|
| 76 |
+
scan_result = scan_code_for_security(
|
| 77 |
+
code=cleaned_code,
|
| 78 |
+
context={
|
| 79 |
+
"server_name": server_name,
|
| 80 |
+
"packages": extra_deps,
|
| 81 |
+
"description": description
|
| 82 |
+
}
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Add helpful interpretation
|
| 86 |
+
if scan_result["is_safe"]:
|
| 87 |
+
scan_result["interpretation"] = "✅ Code appears safe to deploy"
|
| 88 |
+
elif scan_result["severity"] in ["critical", "high"]:
|
| 89 |
+
scan_result["interpretation"] = f"🚫 {scan_result['severity'].upper()} severity issues - deployment would be blocked"
|
| 90 |
+
else:
|
| 91 |
+
scan_result["interpretation"] = f"⚠️ {scan_result['severity'].upper()} severity issues - deployment would proceed with warnings"
|
| 92 |
+
|
| 93 |
+
return scan_result
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
return {
|
| 97 |
+
"success": False,
|
| 98 |
+
"error": f"Security scan failed: {str(e)}",
|
| 99 |
+
"scan_completed": False,
|
| 100 |
+
"is_safe": None
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _create_security_tools() -> List[gr.Interface]:
|
| 105 |
+
"""
|
| 106 |
+
Create and return all security-related Gradio interfaces.
|
| 107 |
+
Tools are registered via @gr.api() decorator above.
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
List of Gradio interfaces (empty - using @gr.api())
|
| 111 |
+
"""
|
| 112 |
+
return []
|
mcp_tools/stats_tools.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Statistics Tools Module
|
| 3 |
+
|
| 4 |
+
Gradio-based MCP tools for statistics and analytics.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from typing import List
|
| 9 |
+
from utils.usage_tracker import (
|
| 10 |
+
get_deployment_statistics,
|
| 11 |
+
get_tool_usage_breakdown,
|
| 12 |
+
get_usage_timeline,
|
| 13 |
+
get_client_statistics,
|
| 14 |
+
get_all_deployments_stats,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_deployment_stats(deployment_id: str, days: int = 30) -> dict:
|
| 19 |
+
"""
|
| 20 |
+
Get usage statistics for a specific deployment.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
deployment_id: The deployment ID to get stats for
|
| 24 |
+
days: Number of days to look back (default: 30)
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
dict with usage statistics
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
stats = get_deployment_statistics(deployment_id, days)
|
| 31 |
+
if stats is None:
|
| 32 |
+
return {
|
| 33 |
+
"success": False,
|
| 34 |
+
"error": f"Failed to retrieve statistics for {deployment_id}"
|
| 35 |
+
}
|
| 36 |
+
return {
|
| 37 |
+
"success": True,
|
| 38 |
+
"deployment_id": deployment_id,
|
| 39 |
+
"stats": stats
|
| 40 |
+
}
|
| 41 |
+
except Exception as e:
|
| 42 |
+
return {"success": False, "error": str(e)}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_tool_usage(deployment_id: str, days: int = 30, limit: int = 10) -> dict:
|
| 46 |
+
"""
|
| 47 |
+
Get breakdown of tool usage for a deployment.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
deployment_id: The deployment ID
|
| 51 |
+
days: Number of days to look back (default: 30)
|
| 52 |
+
limit: Maximum number of tools to return (default: 10)
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
dict with tool usage breakdown
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
tools = get_tool_usage_breakdown(deployment_id, days, limit)
|
| 59 |
+
if tools is None:
|
| 60 |
+
return {
|
| 61 |
+
"success": False,
|
| 62 |
+
"error": f"Failed to retrieve tool usage for {deployment_id}"
|
| 63 |
+
}
|
| 64 |
+
return {
|
| 65 |
+
"success": True,
|
| 66 |
+
"deployment_id": deployment_id,
|
| 67 |
+
"period_days": days,
|
| 68 |
+
"tools": tools
|
| 69 |
+
}
|
| 70 |
+
except Exception as e:
|
| 71 |
+
return {"success": False, "error": str(e)}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_all_stats_summary() -> dict:
|
| 75 |
+
"""
|
| 76 |
+
Get quick statistics summary for all deployments.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
dict with all deployment statistics
|
| 80 |
+
"""
|
| 81 |
+
try:
|
| 82 |
+
all_stats = get_all_deployments_stats()
|
| 83 |
+
if all_stats is None:
|
| 84 |
+
return {
|
| 85 |
+
"success": False,
|
| 86 |
+
"error": "Failed to retrieve deployment statistics"
|
| 87 |
+
}
|
| 88 |
+
return {
|
| 89 |
+
"success": True,
|
| 90 |
+
"total_deployments": len(all_stats),
|
| 91 |
+
"deployments": all_stats
|
| 92 |
+
}
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return {"success": False, "error": str(e)}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _create_stats_tools() -> List[gr.Interface]:
|
| 98 |
+
"""
|
| 99 |
+
Create and return all statistics-related Gradio interfaces.
|
| 100 |
+
Tools are registered via @gr.api() decorator above.
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
List of Gradio interfaces (empty - using @gr.api())
|
| 104 |
+
"""
|
| 105 |
+
return []
|
requirements.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Dependencies - Gradio with MCP Support
|
| 2 |
+
gradio>=5.0.0
|
| 3 |
+
gradio[mcp]>=5.0.0 # Gradio with native MCP server support
|
| 4 |
+
fastapi>=0.115.0
|
| 5 |
+
uvicorn>=0.20.0
|
| 6 |
+
pydantic>=2.0.0
|
| 7 |
+
|
| 8 |
+
# Data Visualization (for stats dashboard)
|
| 9 |
+
plotly>=5.18.0
|
| 10 |
+
pandas>=2.0.0
|
| 11 |
+
|
| 12 |
+
# Database
|
| 13 |
+
psycopg2-binary>=2.9.0
|
| 14 |
+
SQLAlchemy>=2.0.0
|
| 15 |
+
alembic>=1.13.0
|
| 16 |
+
|
| 17 |
+
# Email Provider
|
| 18 |
+
resend>=2.0.0
|
| 19 |
+
|
| 20 |
+
# MCP & Deployment
|
| 21 |
+
modal>=0.60.0
|
| 22 |
+
fastmcp>=2.10.0
|
| 23 |
+
|
| 24 |
+
# Utilities
|
| 25 |
+
requests
|
| 26 |
+
python-dotenv
|
| 27 |
+
|
| 28 |
+
# AI Integrations
|
| 29 |
+
openai
|
| 30 |
+
anthropic
|
ui_components/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Components Module
|
| 3 |
+
|
| 4 |
+
This module contains all Gradio UI components for the web interface.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from .admin_panel import create_admin_panel
|
| 8 |
+
from .code_editor import create_code_editor
|
| 9 |
+
from .stats_dashboard import create_stats_dashboard
|
| 10 |
+
from .log_viewer import create_log_viewer
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
'create_admin_panel',
|
| 14 |
+
'create_code_editor',
|
| 15 |
+
'create_stats_dashboard',
|
| 16 |
+
'create_log_viewer',
|
| 17 |
+
]
|
ui_components/admin_panel.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Admin Panel UI Component
|
| 3 |
+
|
| 4 |
+
Interactive UI for managing MCP server deployments.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from mcp_tools.deployment_tools import (
|
| 9 |
+
deploy_mcp_server,
|
| 10 |
+
list_deployments,
|
| 11 |
+
delete_deployment,
|
| 12 |
+
get_deployment_status
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def create_admin_panel():
|
| 17 |
+
"""
|
| 18 |
+
Create the admin panel UI component.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
gr.Blocks: Admin panel interface
|
| 22 |
+
"""
|
| 23 |
+
with gr.Blocks() as panel:
|
| 24 |
+
gr.Markdown("## ⚙️ Deployment Management")
|
| 25 |
+
gr.Markdown("Deploy and manage your MCP servers")
|
| 26 |
+
|
| 27 |
+
# Quick Deploy Section
|
| 28 |
+
with gr.Accordion("➕ Quick Deploy", open=True):
|
| 29 |
+
with gr.Row():
|
| 30 |
+
with gr.Column(scale=2):
|
| 31 |
+
server_name = gr.Textbox(
|
| 32 |
+
label="Server Name",
|
| 33 |
+
placeholder="my-mcp-server",
|
| 34 |
+
info="Unique name for your MCP server"
|
| 35 |
+
)
|
| 36 |
+
code = gr.Code(
|
| 37 |
+
language="python",
|
| 38 |
+
label="MCP Tools Code",
|
| 39 |
+
lines=12,
|
| 40 |
+
value='''from fastmcp import FastMCP
|
| 41 |
+
|
| 42 |
+
mcp = FastMCP("cat-facts")
|
| 43 |
+
|
| 44 |
+
@mcp.tool()
|
| 45 |
+
def get_cat_fact() -> str:
|
| 46 |
+
"""Get a random cat fact from an API"""
|
| 47 |
+
import requests
|
| 48 |
+
response = requests.get("https://catfact.ninja/fact")
|
| 49 |
+
return response.json()["fact"]
|
| 50 |
+
|
| 51 |
+
@mcp.tool()
|
| 52 |
+
def add_numbers(a: int, b: int) -> int:
|
| 53 |
+
"""Add two numbers together"""
|
| 54 |
+
return a + b
|
| 55 |
+
'''
|
| 56 |
+
)
|
| 57 |
+
with gr.Column(scale=1):
|
| 58 |
+
packages = gr.Textbox(
|
| 59 |
+
label="Extra Packages",
|
| 60 |
+
placeholder="requests, pandas",
|
| 61 |
+
value="requests",
|
| 62 |
+
info="Comma-separated pip packages"
|
| 63 |
+
)
|
| 64 |
+
description = gr.Textbox(
|
| 65 |
+
label="Description",
|
| 66 |
+
lines=2,
|
| 67 |
+
placeholder="Optional description"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# New metadata fields
|
| 71 |
+
with gr.Row():
|
| 72 |
+
category = gr.Textbox(
|
| 73 |
+
label="Category",
|
| 74 |
+
placeholder="e.g., Weather, Finance",
|
| 75 |
+
value="Uncategorized",
|
| 76 |
+
scale=1
|
| 77 |
+
)
|
| 78 |
+
version = gr.Textbox(
|
| 79 |
+
label="Version",
|
| 80 |
+
value="1.0.0",
|
| 81 |
+
scale=1
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
tags = gr.Textbox(
|
| 85 |
+
label="Tags (comma-separated)",
|
| 86 |
+
placeholder="e.g., api, data, utilities"
|
| 87 |
+
)
|
| 88 |
+
author = gr.Textbox(
|
| 89 |
+
label="Author",
|
| 90 |
+
value="Anonymous"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
deploy_btn = gr.Button("🚀 Deploy", variant="primary", size="lg")
|
| 94 |
+
|
| 95 |
+
deploy_output = gr.JSON(label="Deployment Result")
|
| 96 |
+
|
| 97 |
+
# Helper function to parse tags
|
| 98 |
+
def deploy_with_metadata(name, code_val, pkgs, desc, cat, tag_str, auth, ver):
|
| 99 |
+
"""Deploy with metadata, parsing tags from comma-separated string"""
|
| 100 |
+
tag_list = [t.strip() for t in tag_str.split(",") if t.strip()] if tag_str else []
|
| 101 |
+
return deploy_mcp_server(
|
| 102 |
+
server_name=name,
|
| 103 |
+
mcp_tools_code=code_val,
|
| 104 |
+
extra_pip_packages=pkgs,
|
| 105 |
+
description=desc,
|
| 106 |
+
category=cat,
|
| 107 |
+
tags=tag_list,
|
| 108 |
+
author=auth,
|
| 109 |
+
version=ver
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# Wire up deploy button
|
| 113 |
+
deploy_btn.click(
|
| 114 |
+
fn=deploy_with_metadata,
|
| 115 |
+
inputs=[server_name, code, packages, description, category, tags, author, version],
|
| 116 |
+
outputs=deploy_output,
|
| 117 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Deployments Table Section
|
| 121 |
+
gr.Markdown("### 📋 Active Deployments")
|
| 122 |
+
|
| 123 |
+
with gr.Row():
|
| 124 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm", scale=0)
|
| 125 |
+
search_box = gr.Textbox(
|
| 126 |
+
label="Search",
|
| 127 |
+
placeholder="Filter by name...",
|
| 128 |
+
scale=2
|
| 129 |
+
)
|
| 130 |
+
category_filter = gr.Dropdown(
|
| 131 |
+
label="Filter by Category",
|
| 132 |
+
choices=["All Categories"],
|
| 133 |
+
value="All Categories",
|
| 134 |
+
scale=1
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
deployments_df = gr.Dataframe(
|
| 138 |
+
headers=["ID", "Name", "Category", "Tags", "Version", "Author", "Status", "Requests", "Created"],
|
| 139 |
+
datatype=["str", "str", "str", "str", "str", "str", "str", "number", "str"],
|
| 140 |
+
interactive=False,
|
| 141 |
+
wrap=True,
|
| 142 |
+
label="Deployments"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Quick Actions Section
|
| 146 |
+
with gr.Accordion("⚡ Quick Actions", open=False):
|
| 147 |
+
with gr.Row():
|
| 148 |
+
deployment_selector = gr.Dropdown(
|
| 149 |
+
label="Select Deployment",
|
| 150 |
+
choices=[],
|
| 151 |
+
interactive=True
|
| 152 |
+
)
|
| 153 |
+
action_selector = gr.Dropdown(
|
| 154 |
+
label="Action",
|
| 155 |
+
choices=["View Status", "Delete (confirm required)"],
|
| 156 |
+
value="View Status",
|
| 157 |
+
interactive=True
|
| 158 |
+
)
|
| 159 |
+
execute_btn = gr.Button("Execute", variant="secondary")
|
| 160 |
+
|
| 161 |
+
action_output = gr.JSON(label="Action Result")
|
| 162 |
+
|
| 163 |
+
# Functions
|
| 164 |
+
def load_deployments():
|
| 165 |
+
"""Load all deployments into the table"""
|
| 166 |
+
result = list_deployments()
|
| 167 |
+
if result["success"]:
|
| 168 |
+
data = []
|
| 169 |
+
dropdown_choices = []
|
| 170 |
+
categories = set(["All Categories"])
|
| 171 |
+
|
| 172 |
+
for dep in result["deployments"]:
|
| 173 |
+
# Format tags
|
| 174 |
+
tags_str = ", ".join(dep.get("tags", [])) if dep.get("tags") else "—"
|
| 175 |
+
|
| 176 |
+
data.append([
|
| 177 |
+
dep["deployment_id"][:16] + "...", # Shortened ID
|
| 178 |
+
dep["server_name"],
|
| 179 |
+
dep.get("category", "Uncategorized"),
|
| 180 |
+
tags_str,
|
| 181 |
+
dep.get("version", "1.0.0"),
|
| 182 |
+
dep.get("author", "Anonymous"),
|
| 183 |
+
dep["status"],
|
| 184 |
+
dep["total_requests"],
|
| 185 |
+
dep["created_at"][:10] if dep["created_at"] else "N/A" # Date only
|
| 186 |
+
])
|
| 187 |
+
dropdown_choices.append(
|
| 188 |
+
(dep["server_name"], dep["deployment_id"])
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Collect unique categories
|
| 192 |
+
if dep.get("category"):
|
| 193 |
+
categories.add(dep["category"])
|
| 194 |
+
|
| 195 |
+
return data, gr.Dropdown(choices=dropdown_choices), gr.Dropdown(choices=sorted(list(categories)))
|
| 196 |
+
return [], gr.Dropdown(choices=[]), gr.Dropdown(choices=["All Categories"])
|
| 197 |
+
|
| 198 |
+
def execute_action(deployment_id, action):
|
| 199 |
+
"""Execute selected action on deployment"""
|
| 200 |
+
if not deployment_id:
|
| 201 |
+
return {"success": False, "error": "Select a deployment first"}
|
| 202 |
+
|
| 203 |
+
if "Delete" in action:
|
| 204 |
+
return delete_deployment(deployment_id=deployment_id, confirm=True)
|
| 205 |
+
elif "View Status" in action:
|
| 206 |
+
return get_deployment_status(deployment_id=deployment_id)
|
| 207 |
+
|
| 208 |
+
return {"success": False, "error": "Unknown action"}
|
| 209 |
+
|
| 210 |
+
def filter_deployments(search_term, category_val):
|
| 211 |
+
"""Filter deployments by search term and category"""
|
| 212 |
+
result = list_deployments()
|
| 213 |
+
if not result["success"]:
|
| 214 |
+
return []
|
| 215 |
+
|
| 216 |
+
filtered_data = []
|
| 217 |
+
for dep in result["deployments"]:
|
| 218 |
+
# Check search term
|
| 219 |
+
search_match = (not search_term or
|
| 220 |
+
search_term.lower() in dep["server_name"].lower() or
|
| 221 |
+
search_term.lower() in dep["deployment_id"].lower())
|
| 222 |
+
|
| 223 |
+
# Check category filter
|
| 224 |
+
category_match = (category_val == "All Categories" or
|
| 225 |
+
dep.get("category", "Uncategorized") == category_val)
|
| 226 |
+
|
| 227 |
+
if search_match and category_match:
|
| 228 |
+
tags_str = ", ".join(dep.get("tags", [])) if dep.get("tags") else "—"
|
| 229 |
+
filtered_data.append([
|
| 230 |
+
dep["deployment_id"][:16] + "...",
|
| 231 |
+
dep["server_name"],
|
| 232 |
+
dep.get("category", "Uncategorized"),
|
| 233 |
+
tags_str,
|
| 234 |
+
dep.get("version", "1.0.0"),
|
| 235 |
+
dep.get("author", "Anonymous"),
|
| 236 |
+
dep["status"],
|
| 237 |
+
dep["total_requests"],
|
| 238 |
+
dep["created_at"][:10] if dep["created_at"] else "N/A"
|
| 239 |
+
])
|
| 240 |
+
return filtered_data
|
| 241 |
+
|
| 242 |
+
# Wire up events
|
| 243 |
+
refresh_btn.click(
|
| 244 |
+
fn=load_deployments,
|
| 245 |
+
outputs=[deployments_df, deployment_selector, category_filter],
|
| 246 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
search_box.change(
|
| 250 |
+
fn=filter_deployments,
|
| 251 |
+
inputs=[search_box, category_filter],
|
| 252 |
+
outputs=deployments_df,
|
| 253 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
category_filter.change(
|
| 257 |
+
fn=filter_deployments,
|
| 258 |
+
inputs=[search_box, category_filter],
|
| 259 |
+
outputs=deployments_df,
|
| 260 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
execute_btn.click(
|
| 264 |
+
fn=execute_action,
|
| 265 |
+
inputs=[deployment_selector, action_selector],
|
| 266 |
+
outputs=action_output,
|
| 267 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# Load deployments on panel load
|
| 271 |
+
panel.load(
|
| 272 |
+
fn=load_deployments,
|
| 273 |
+
outputs=[deployments_df, deployment_selector, category_filter],
|
| 274 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
return panel
|
ui_components/ai_chat_deployment.py
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI Chat Interface for MCP Deployment (Enhanced with Tool Use)
|
| 3 |
+
|
| 4 |
+
Provides a conversational interface for creating, modifying, and debugging MCP servers.
|
| 5 |
+
The AI assistant can now actually deploy and manage servers using tools.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
from typing import List, Dict, Tuple, Optional
|
| 12 |
+
from mcp_tools.ai_assistant import MCPAssistant, validate_api_key, validate_sambanova_env
|
| 13 |
+
from mcp_tools.deployment_tools import (
|
| 14 |
+
deploy_mcp_server,
|
| 15 |
+
list_deployments,
|
| 16 |
+
get_deployment_code,
|
| 17 |
+
update_deployment_code,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def create_ai_chat_deployment():
|
| 22 |
+
"""
|
| 23 |
+
Create the AI-powered chat interface for MCP deployment.
|
| 24 |
+
|
| 25 |
+
The AI assistant now has access to actual deployment tools and can:
|
| 26 |
+
- Deploy new MCP servers
|
| 27 |
+
- List existing deployments
|
| 28 |
+
- Get and modify deployment code
|
| 29 |
+
- Check deployment status
|
| 30 |
+
- View usage statistics
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
gr.Blocks: AI chat interface
|
| 34 |
+
"""
|
| 35 |
+
with gr.Blocks() as chat_interface:
|
| 36 |
+
gr.Markdown("## 🤖 AI-Powered MCP Creation & Management")
|
| 37 |
+
gr.Markdown("""
|
| 38 |
+
Chat with Claude to create, modify, or debug MCP servers.
|
| 39 |
+
|
| 40 |
+
**🔧 The AI assistant has full access to deployment tools and can actually:**
|
| 41 |
+
- ✅ Deploy new MCP servers to Modal.com
|
| 42 |
+
- ✅ List and check your existing deployments
|
| 43 |
+
- ✅ View and modify deployment code
|
| 44 |
+
- ✅ Update and redeploy servers
|
| 45 |
+
- ✅ Scan code for security vulnerabilities
|
| 46 |
+
- ✅ View usage statistics
|
| 47 |
+
|
| 48 |
+
Just describe what you want to do!
|
| 49 |
+
""")
|
| 50 |
+
|
| 51 |
+
# Session state
|
| 52 |
+
session_state = gr.State({
|
| 53 |
+
"api_key": None,
|
| 54 |
+
"assistant": None,
|
| 55 |
+
"mode": "create",
|
| 56 |
+
"generated_code": None,
|
| 57 |
+
"generated_packages": [],
|
| 58 |
+
"suggested_category": "Uncategorized",
|
| 59 |
+
"suggested_tags": [],
|
| 60 |
+
# Deployment context (pre-loaded when selecting in modify mode)
|
| 61 |
+
"selected_deployment_id": None,
|
| 62 |
+
"selected_deployment_code": None,
|
| 63 |
+
"selected_deployment_metadata": None,
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
with gr.Row():
|
| 67 |
+
# Left column: Chat interface
|
| 68 |
+
with gr.Column(scale=2):
|
| 69 |
+
# Model Selection
|
| 70 |
+
model_selector = gr.Dropdown(
|
| 71 |
+
choices=[
|
| 72 |
+
("Claude Sonnet 4 (Anthropic)", "anthropic:claude-sonnet-4-20250514"),
|
| 73 |
+
("Meta Llama 3.3 70B (SambaNova)", "sambanova:Meta-Llama-3.3-70B-Instruct"),
|
| 74 |
+
("DeepSeek V3 (SambaNova)", "sambanova:DeepSeek-V3-0324"),
|
| 75 |
+
("Llama 4 Maverick 17B (SambaNova)", "sambanova:Llama-4-Maverick-17B-128E-Instruct"),
|
| 76 |
+
("Qwen3 32B (SambaNova)", "sambanova:Qwen3-32B"),
|
| 77 |
+
("GPT OSS 120B (SambaNova)", "sambanova:gpt-oss-120b"),
|
| 78 |
+
("DeepSeek V3.1 (SambaNova)", "sambanova:DeepSeek-V3.1"),
|
| 79 |
+
],
|
| 80 |
+
value="anthropic:claude-sonnet-4-20250514",
|
| 81 |
+
label="🤖 AI Model",
|
| 82 |
+
info="Select which AI model to use for assistance"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# API Key input (conditionally visible for Anthropic)
|
| 86 |
+
with gr.Row(visible=True) as api_key_row:
|
| 87 |
+
api_key_input = gr.Textbox(
|
| 88 |
+
label="Anthropic API Key",
|
| 89 |
+
type="password",
|
| 90 |
+
placeholder="sk-ant-...",
|
| 91 |
+
scale=3
|
| 92 |
+
)
|
| 93 |
+
validate_btn = gr.Button("✓ Validate", size="sm", scale=1, visible=True)
|
| 94 |
+
|
| 95 |
+
api_status = gr.Markdown("*API key not set*")
|
| 96 |
+
|
| 97 |
+
# Mode selection with enhanced options
|
| 98 |
+
mode_selector = gr.Radio(
|
| 99 |
+
choices=[
|
| 100 |
+
("🚀 Create New MCP", "create"),
|
| 101 |
+
("✏️ Modify Existing MCP", "modify"),
|
| 102 |
+
("🔍 Debug & Troubleshoot", "debug"),
|
| 103 |
+
("📊 View Stats & Status", "stats"),
|
| 104 |
+
],
|
| 105 |
+
value="create",
|
| 106 |
+
label="Mode",
|
| 107 |
+
interactive=True
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Deployment selector (only for modify mode)
|
| 111 |
+
deployment_selector = gr.Dropdown(
|
| 112 |
+
label="Select Deployment to Modify",
|
| 113 |
+
choices=[],
|
| 114 |
+
visible=False,
|
| 115 |
+
interactive=True
|
| 116 |
+
)
|
| 117 |
+
refresh_deployments_btn = gr.Button(
|
| 118 |
+
"🔄 Refresh",
|
| 119 |
+
visible=False,
|
| 120 |
+
size="sm"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Chat interface
|
| 124 |
+
chatbot = gr.Chatbot(
|
| 125 |
+
label="Chat with Claude (Tool-Use Enabled)",
|
| 126 |
+
height=500,
|
| 127 |
+
avatar_images=(
|
| 128 |
+
None,
|
| 129 |
+
"https://www.anthropic.com/_next/image?url=%2Fimages%2Ficons%2Ffeature-prompt.svg&w=96&q=75",
|
| 130 |
+
)
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Input box
|
| 134 |
+
with gr.Row():
|
| 135 |
+
msg_input = gr.Textbox(
|
| 136 |
+
label="Your Message",
|
| 137 |
+
placeholder="Describe what you want to do... (e.g., 'Create an MCP that fetches weather data' or 'Show me my deployments')",
|
| 138 |
+
scale=4,
|
| 139 |
+
lines=2
|
| 140 |
+
)
|
| 141 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 142 |
+
|
| 143 |
+
# Quick examples organized by mode
|
| 144 |
+
with gr.Accordion("💡 Example Prompts", open=False):
|
| 145 |
+
gr.Markdown("### Create Mode")
|
| 146 |
+
gr.Examples(
|
| 147 |
+
examples=[
|
| 148 |
+
"Create an MCP that fetches weather data using wttr.in API",
|
| 149 |
+
"Build an MCP server that converts currencies using an exchange rate API",
|
| 150 |
+
"Make an MCP tool that searches books using the Open Library API",
|
| 151 |
+
"Create an MCP with two tools: one to get random cat facts and one to get dog facts",
|
| 152 |
+
],
|
| 153 |
+
inputs=msg_input,
|
| 154 |
+
label="Creation Examples"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
gr.Markdown("### Manage Mode")
|
| 158 |
+
gr.Examples(
|
| 159 |
+
examples=[
|
| 160 |
+
"Show me all my deployed MCP servers",
|
| 161 |
+
"What's the status of my weather-api deployment?",
|
| 162 |
+
"Get the code for my cat-facts deployment",
|
| 163 |
+
"Show me usage statistics for all my deployments",
|
| 164 |
+
],
|
| 165 |
+
inputs=msg_input,
|
| 166 |
+
label="Management Examples"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
gr.Markdown("### Modify Mode")
|
| 170 |
+
gr.Examples(
|
| 171 |
+
examples=[
|
| 172 |
+
"Add error handling to my existing weather MCP",
|
| 173 |
+
"Add a new tool to my cat-facts server that returns multiple facts",
|
| 174 |
+
"Update my deployment to add rate limiting",
|
| 175 |
+
"Fix the security issues in my code",
|
| 176 |
+
],
|
| 177 |
+
inputs=msg_input,
|
| 178 |
+
label="Modification Examples"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Right column: Code preview and info
|
| 182 |
+
with gr.Column(scale=1):
|
| 183 |
+
gr.Markdown("### 📝 Generated Code Preview")
|
| 184 |
+
gr.Markdown("*Code extracted from AI response will appear here*")
|
| 185 |
+
|
| 186 |
+
# Code preview
|
| 187 |
+
code_preview = gr.Code(
|
| 188 |
+
language="python",
|
| 189 |
+
label="",
|
| 190 |
+
lines=20,
|
| 191 |
+
interactive=True,
|
| 192 |
+
value="# Code will appear here after chatting with Claude\n# The AI can also deploy directly using tools!"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
# Metadata inputs for manual deployment
|
| 196 |
+
with gr.Accordion("Manual Deployment Options", open=False):
|
| 197 |
+
gr.Markdown("*Use these only if you want to deploy code manually without asking the AI*")
|
| 198 |
+
|
| 199 |
+
server_name_input = gr.Textbox(
|
| 200 |
+
label="Server Name",
|
| 201 |
+
placeholder="e.g., my-weather-api"
|
| 202 |
+
)
|
| 203 |
+
category_input = gr.Textbox(
|
| 204 |
+
label="Category",
|
| 205 |
+
placeholder="e.g., Weather, Finance, Utilities",
|
| 206 |
+
value="Uncategorized"
|
| 207 |
+
)
|
| 208 |
+
tags_input = gr.Textbox(
|
| 209 |
+
label="Tags (comma-separated)",
|
| 210 |
+
placeholder="e.g., api, weather, data"
|
| 211 |
+
)
|
| 212 |
+
author_input = gr.Textbox(
|
| 213 |
+
label="Author",
|
| 214 |
+
value="Anonymous"
|
| 215 |
+
)
|
| 216 |
+
packages_input = gr.Textbox(
|
| 217 |
+
label="Required Packages (comma-separated)",
|
| 218 |
+
placeholder="e.g., requests, beautifulsoup4"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# Manual deploy button
|
| 222 |
+
manual_deploy_btn = gr.Button(
|
| 223 |
+
"🚀 Manual Deploy",
|
| 224 |
+
variant="secondary",
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Deployment result
|
| 228 |
+
with gr.Accordion("Deployment Results", open=True):
|
| 229 |
+
deployment_result = gr.JSON(label="Latest Result")
|
| 230 |
+
|
| 231 |
+
# Functions
|
| 232 |
+
|
| 233 |
+
def validate_or_create_assistant(model_choice: str, api_key: str = None) -> Tuple[str, dict]:
|
| 234 |
+
"""Validate API key or create assistant based on model selection"""
|
| 235 |
+
if not model_choice:
|
| 236 |
+
return "❌ *Please select a model*", {"api_key": None, "assistant": None}
|
| 237 |
+
|
| 238 |
+
# Parse provider and model from selection
|
| 239 |
+
provider, model = model_choice.split(":")
|
| 240 |
+
|
| 241 |
+
if provider == "anthropic":
|
| 242 |
+
# Anthropic requires API key validation
|
| 243 |
+
if not api_key:
|
| 244 |
+
return "❌ *Please enter an Anthropic API key*", {"api_key": None, "assistant": None}
|
| 245 |
+
|
| 246 |
+
if validate_api_key(api_key):
|
| 247 |
+
try:
|
| 248 |
+
assistant = MCPAssistant(provider="anthropic", model=model, api_key=api_key)
|
| 249 |
+
return f"✅ *Ready with {model}!*", {"api_key": api_key, "assistant": assistant}
|
| 250 |
+
except Exception as e:
|
| 251 |
+
return f"❌ *Error: {str(e)}*", {"api_key": None, "assistant": None}
|
| 252 |
+
else:
|
| 253 |
+
return "❌ *Invalid Anthropic API key*", {"api_key": None, "assistant": None}
|
| 254 |
+
|
| 255 |
+
elif provider == "sambanova":
|
| 256 |
+
# SambaNova uses environment variable
|
| 257 |
+
is_valid, message = validate_sambanova_env()
|
| 258 |
+
if not is_valid:
|
| 259 |
+
return f"❌ *{message}*", {"api_key": None, "assistant": None}
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
assistant = MCPAssistant(provider="sambanova", model=model)
|
| 263 |
+
return f"✅ *Ready with {model}!*", {"api_key": None, "assistant": assistant}
|
| 264 |
+
except Exception as e:
|
| 265 |
+
return f"❌ *Error: {str(e)}*", {"api_key": None, "assistant": None}
|
| 266 |
+
|
| 267 |
+
else:
|
| 268 |
+
return f"❌ *Unknown provider: {provider}*", {"api_key": None, "assistant": None}
|
| 269 |
+
|
| 270 |
+
def update_api_key_visibility(model_choice: str, current_state: dict):
|
| 271 |
+
"""Show/hide API key input based on selected model and auto-create SambaNova assistant"""
|
| 272 |
+
if not model_choice:
|
| 273 |
+
return gr.Row(visible=True), "*Please select a model*", current_state
|
| 274 |
+
|
| 275 |
+
provider = model_choice.split(":")[0]
|
| 276 |
+
|
| 277 |
+
if provider == "anthropic":
|
| 278 |
+
# Clear assistant if switching to Anthropic (requires new API key)
|
| 279 |
+
new_state = {**current_state, "assistant": None, "api_key": None}
|
| 280 |
+
return (
|
| 281 |
+
gr.Row(visible=True), # api_key_row
|
| 282 |
+
"*Enter your Anthropic API key*", # api_status
|
| 283 |
+
new_state # session_state
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
elif provider == "sambanova":
|
| 287 |
+
# Auto-create SambaNova assistant
|
| 288 |
+
is_valid, message = validate_sambanova_env()
|
| 289 |
+
if is_valid:
|
| 290 |
+
try:
|
| 291 |
+
model = model_choice.split(":")[1]
|
| 292 |
+
assistant = MCPAssistant(provider="sambanova", model=model)
|
| 293 |
+
new_state = {**current_state, "assistant": assistant, "api_key": None}
|
| 294 |
+
return (
|
| 295 |
+
gr.Row(visible=False), # api_key_row - hide for SambaNova
|
| 296 |
+
f"✅ *Ready with {model}!*", # api_status
|
| 297 |
+
new_state # session_state
|
| 298 |
+
)
|
| 299 |
+
except Exception as e:
|
| 300 |
+
new_state = {**current_state, "assistant": None, "api_key": None}
|
| 301 |
+
return (
|
| 302 |
+
gr.Row(visible=False), # api_key_row
|
| 303 |
+
f"❌ *Error: {str(e)}*", # api_status
|
| 304 |
+
new_state # session_state
|
| 305 |
+
)
|
| 306 |
+
else:
|
| 307 |
+
new_state = {**current_state, "assistant": None, "api_key": None}
|
| 308 |
+
return (
|
| 309 |
+
gr.Row(visible=False), # api_key_row
|
| 310 |
+
f"❌ *{message}*", # api_status
|
| 311 |
+
new_state # session_state
|
| 312 |
+
)
|
| 313 |
+
else:
|
| 314 |
+
return (
|
| 315 |
+
gr.Row(visible=True), # api_key_row
|
| 316 |
+
"*Unknown provider*", # api_status
|
| 317 |
+
current_state # session_state
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
def load_deployment_choices():
|
| 321 |
+
"""Load available deployments for the dropdown"""
|
| 322 |
+
try:
|
| 323 |
+
result = list_deployments()
|
| 324 |
+
|
| 325 |
+
# Debug: Print result for troubleshooting
|
| 326 |
+
print(f"[DEBUG] list_deployments result: success={result.get('success')}, total={result.get('total', 0)}")
|
| 327 |
+
|
| 328 |
+
if result.get("success"):
|
| 329 |
+
deployments = result.get("deployments", [])
|
| 330 |
+
|
| 331 |
+
if not deployments:
|
| 332 |
+
print("[DEBUG] No deployments found in database")
|
| 333 |
+
return []
|
| 334 |
+
|
| 335 |
+
choices = []
|
| 336 |
+
for dep in deployments:
|
| 337 |
+
# Build a descriptive label
|
| 338 |
+
server_name = dep.get("server_name", "Unknown")
|
| 339 |
+
app_name = dep.get("app_name", "")
|
| 340 |
+
deployment_id = dep.get("deployment_id", "")
|
| 341 |
+
|
| 342 |
+
if not deployment_id:
|
| 343 |
+
print(f"[DEBUG] Skipping deployment without ID: {dep}")
|
| 344 |
+
continue
|
| 345 |
+
|
| 346 |
+
label = f"{server_name}"
|
| 347 |
+
if app_name:
|
| 348 |
+
label += f" ({app_name})"
|
| 349 |
+
|
| 350 |
+
choices.append((label, deployment_id))
|
| 351 |
+
|
| 352 |
+
print(f"[DEBUG] Loaded {len(choices)} deployment choices")
|
| 353 |
+
return choices
|
| 354 |
+
else:
|
| 355 |
+
error = result.get("error", "Unknown error")
|
| 356 |
+
print(f"[DEBUG] list_deployments failed: {error}")
|
| 357 |
+
return []
|
| 358 |
+
|
| 359 |
+
except Exception as e:
|
| 360 |
+
print(f"[ERROR] Exception loading deployments: {e}")
|
| 361 |
+
import traceback
|
| 362 |
+
traceback.print_exc()
|
| 363 |
+
return []
|
| 364 |
+
|
| 365 |
+
def update_mode_visibility(mode: str):
|
| 366 |
+
"""Update UI based on selected mode"""
|
| 367 |
+
show_deployment_selector = mode == "modify"
|
| 368 |
+
|
| 369 |
+
print(f"[DEBUG] Mode changed to: {mode}, show_dropdown: {show_deployment_selector}")
|
| 370 |
+
|
| 371 |
+
# Load deployments if in modify mode
|
| 372 |
+
if show_deployment_selector:
|
| 373 |
+
choices = load_deployment_choices()
|
| 374 |
+
print(f"[DEBUG] Found {len(choices)} choices for dropdown")
|
| 375 |
+
|
| 376 |
+
if choices:
|
| 377 |
+
return (
|
| 378 |
+
gr.update(
|
| 379 |
+
choices=choices,
|
| 380 |
+
visible=True,
|
| 381 |
+
value=None,
|
| 382 |
+
interactive=True,
|
| 383 |
+
label="Select Deployment to Modify",
|
| 384 |
+
info=None
|
| 385 |
+
),
|
| 386 |
+
gr.update(visible=True)
|
| 387 |
+
)
|
| 388 |
+
else:
|
| 389 |
+
return (
|
| 390 |
+
gr.update(
|
| 391 |
+
choices=[],
|
| 392 |
+
visible=True,
|
| 393 |
+
value=None,
|
| 394 |
+
interactive=True,
|
| 395 |
+
label="Select Deployment to Modify",
|
| 396 |
+
info="⚠️ No deployments found. Create one first!"
|
| 397 |
+
),
|
| 398 |
+
gr.update(visible=True)
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# Hide both when not in modify mode
|
| 402 |
+
print("[DEBUG] Hiding dropdown and button")
|
| 403 |
+
return (
|
| 404 |
+
gr.update(visible=False),
|
| 405 |
+
gr.update(visible=False)
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
def refresh_deployments():
|
| 409 |
+
"""Refresh the deployment list"""
|
| 410 |
+
print("[DEBUG] Refreshing deployment list...")
|
| 411 |
+
choices = load_deployment_choices()
|
| 412 |
+
print(f"[DEBUG] Refresh found {len(choices)} deployments")
|
| 413 |
+
|
| 414 |
+
if choices:
|
| 415 |
+
return gr.update(
|
| 416 |
+
choices=choices,
|
| 417 |
+
value=None,
|
| 418 |
+
visible=True,
|
| 419 |
+
interactive=True,
|
| 420 |
+
label="Select Deployment to Modify",
|
| 421 |
+
info=None
|
| 422 |
+
)
|
| 423 |
+
else:
|
| 424 |
+
return gr.update(
|
| 425 |
+
choices=[],
|
| 426 |
+
value=None,
|
| 427 |
+
visible=True,
|
| 428 |
+
interactive=True,
|
| 429 |
+
label="Select Deployment to Modify",
|
| 430 |
+
info="⚠️ No deployments found. Create one first!"
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
def load_deployment_context(deployment_id: Optional[str], state: dict, history: List[Dict]):
|
| 434 |
+
"""
|
| 435 |
+
Load deployment context when a deployment is selected.
|
| 436 |
+
|
| 437 |
+
This fetches the deployment code and metadata, injects it into the session state,
|
| 438 |
+
and adds a context message to the chat so the AI has immediate access.
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
deployment_id: Selected deployment ID
|
| 442 |
+
state: Current session state
|
| 443 |
+
history: Current chat history
|
| 444 |
+
|
| 445 |
+
Returns:
|
| 446 |
+
Tuple of (updated_history, updated_state, code_preview)
|
| 447 |
+
"""
|
| 448 |
+
if not deployment_id:
|
| 449 |
+
# Clear context if no deployment selected
|
| 450 |
+
new_state = {
|
| 451 |
+
**state,
|
| 452 |
+
"selected_deployment_id": None,
|
| 453 |
+
"selected_deployment_code": None,
|
| 454 |
+
"selected_deployment_metadata": None,
|
| 455 |
+
}
|
| 456 |
+
return history, new_state, "# No deployment selected"
|
| 457 |
+
|
| 458 |
+
print(f"[DEBUG] Loading context for deployment: {deployment_id}")
|
| 459 |
+
|
| 460 |
+
try:
|
| 461 |
+
# Fetch deployment code and status
|
| 462 |
+
code_result = get_deployment_code(deployment_id)
|
| 463 |
+
|
| 464 |
+
if not code_result.get("success"):
|
| 465 |
+
error_msg = code_result.get("error", "Unknown error")
|
| 466 |
+
new_history = [
|
| 467 |
+
*history,
|
| 468 |
+
{
|
| 469 |
+
"role": "assistant",
|
| 470 |
+
"content": f"❌ Failed to load deployment: {error_msg}"
|
| 471 |
+
}
|
| 472 |
+
]
|
| 473 |
+
return new_history, state, f"# Error loading deployment\n# {error_msg}"
|
| 474 |
+
|
| 475 |
+
# Extract deployment info (data is directly in code_result, not nested)
|
| 476 |
+
server_name = code_result.get("server_name", "Unknown")
|
| 477 |
+
mcp_code = code_result.get("code", "")
|
| 478 |
+
packages_list = code_result.get("packages", [])
|
| 479 |
+
packages = ", ".join(packages_list) if packages_list else ""
|
| 480 |
+
description = code_result.get("description", "")
|
| 481 |
+
url = code_result.get("url", "")
|
| 482 |
+
|
| 483 |
+
# Get app name from deployment_id (format: deploy-mcp-<name>-<hash>)
|
| 484 |
+
app_name = deployment_id.replace("deploy-", "") if deployment_id.startswith("deploy-") else deployment_id
|
| 485 |
+
|
| 486 |
+
# Update state with deployment context
|
| 487 |
+
new_state = {
|
| 488 |
+
**state,
|
| 489 |
+
"selected_deployment_id": deployment_id,
|
| 490 |
+
"selected_deployment_code": mcp_code,
|
| 491 |
+
"selected_deployment_metadata": {
|
| 492 |
+
"server_name": server_name,
|
| 493 |
+
"app_name": app_name,
|
| 494 |
+
"packages": packages,
|
| 495 |
+
"description": description,
|
| 496 |
+
"url": url,
|
| 497 |
+
"deployment_id": deployment_id,
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
# Add context message to chat
|
| 502 |
+
context_message = f"""✅ **Loaded: {server_name}**
|
| 503 |
+
|
| 504 |
+
**Deployment ID:** `{deployment_id}`
|
| 505 |
+
**App Name:** `{app_name}`
|
| 506 |
+
**URL:** {url}
|
| 507 |
+
**Packages:** {packages if packages else "None"}
|
| 508 |
+
|
| 509 |
+
The current code for this deployment is now loaded in the code preview.
|
| 510 |
+
You can now ask me to modify, enhance, or debug this MCP server!
|
| 511 |
+
|
| 512 |
+
**Example prompts:**
|
| 513 |
+
- "Add error handling to all functions"
|
| 514 |
+
- "Add a new tool that does X"
|
| 515 |
+
- "Fix the security issues"
|
| 516 |
+
- "Add rate limiting"
|
| 517 |
+
"""
|
| 518 |
+
|
| 519 |
+
new_history = [
|
| 520 |
+
*history,
|
| 521 |
+
{
|
| 522 |
+
"role": "assistant",
|
| 523 |
+
"content": context_message
|
| 524 |
+
}
|
| 525 |
+
]
|
| 526 |
+
|
| 527 |
+
print(f"[DEBUG] Successfully loaded deployment context for {server_name}")
|
| 528 |
+
|
| 529 |
+
return new_history, new_state, mcp_code
|
| 530 |
+
|
| 531 |
+
except Exception as e:
|
| 532 |
+
print(f"[ERROR] Exception loading deployment context: {e}")
|
| 533 |
+
import traceback
|
| 534 |
+
traceback.print_exc()
|
| 535 |
+
|
| 536 |
+
error_history = [
|
| 537 |
+
*history,
|
| 538 |
+
{
|
| 539 |
+
"role": "assistant",
|
| 540 |
+
"content": f"❌ Error loading deployment: {str(e)}"
|
| 541 |
+
}
|
| 542 |
+
]
|
| 543 |
+
return error_history, state, f"# Error\n# {str(e)}"
|
| 544 |
+
|
| 545 |
+
def chat_response(
|
| 546 |
+
message: str,
|
| 547 |
+
history: List[Dict],
|
| 548 |
+
state: dict,
|
| 549 |
+
mode: str,
|
| 550 |
+
deployment_id: Optional[str] = None
|
| 551 |
+
):
|
| 552 |
+
"""
|
| 553 |
+
Handle chat messages and generate responses with tool execution.
|
| 554 |
+
|
| 555 |
+
The AI assistant now uses tools to actually perform operations,
|
| 556 |
+
not just generate code.
|
| 557 |
+
|
| 558 |
+
Streams responses in real-time using yield.
|
| 559 |
+
"""
|
| 560 |
+
|
| 561 |
+
# Check if API key is set
|
| 562 |
+
if not state.get("assistant"):
|
| 563 |
+
error_msg = [
|
| 564 |
+
*history,
|
| 565 |
+
{"role": "user", "content": message},
|
| 566 |
+
{"role": "assistant", "content": "❌ Please set and validate your API key first."}
|
| 567 |
+
]
|
| 568 |
+
yield (
|
| 569 |
+
error_msg,
|
| 570 |
+
state,
|
| 571 |
+
code_preview.value,
|
| 572 |
+
None
|
| 573 |
+
)
|
| 574 |
+
return
|
| 575 |
+
|
| 576 |
+
assistant = state["assistant"]
|
| 577 |
+
|
| 578 |
+
# Add user message to history
|
| 579 |
+
history = history or []
|
| 580 |
+
history.append({"role": "user", "content": message})
|
| 581 |
+
|
| 582 |
+
# Immediately show user message
|
| 583 |
+
yield (
|
| 584 |
+
history,
|
| 585 |
+
state,
|
| 586 |
+
code_preview.value,
|
| 587 |
+
None
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
# Build context based on mode and pre-loaded deployment data
|
| 591 |
+
context_message = message
|
| 592 |
+
|
| 593 |
+
if mode == "modify" and state.get("selected_deployment_metadata"):
|
| 594 |
+
# Use pre-loaded deployment context (much more efficient!)
|
| 595 |
+
metadata = state["selected_deployment_metadata"]
|
| 596 |
+
current_code = state.get("selected_deployment_code", "")
|
| 597 |
+
|
| 598 |
+
context_message = f"""[DEPLOYMENT CONTEXT - Pre-loaded for efficiency]
|
| 599 |
+
Deployment ID: {metadata['deployment_id']}
|
| 600 |
+
Server Name: {metadata['server_name']}
|
| 601 |
+
App Name: {metadata['app_name']}
|
| 602 |
+
Current Packages: {metadata['packages']}
|
| 603 |
+
URL: {metadata['url']}
|
| 604 |
+
|
| 605 |
+
CURRENT CODE:
|
| 606 |
+
```python
|
| 607 |
+
{current_code}
|
| 608 |
+
```
|
| 609 |
+
|
| 610 |
+
USER REQUEST: {message}
|
| 611 |
+
|
| 612 |
+
NOTE: The deployment code is already loaded above. You can directly suggest modifications without calling get_deployment_code. When you're ready to update, use the update_deployment_code tool with deployment_id='{metadata['deployment_id']}'.
|
| 613 |
+
"""
|
| 614 |
+
elif mode == "stats":
|
| 615 |
+
context_message = f"[Context: User wants to view statistics or status]\n\n{message}"
|
| 616 |
+
elif mode == "debug":
|
| 617 |
+
context_message = f"[Context: User is debugging/troubleshooting]\n\n{message}"
|
| 618 |
+
|
| 619 |
+
# Add empty assistant message that we'll stream into
|
| 620 |
+
history.append({"role": "assistant", "content": ""})
|
| 621 |
+
|
| 622 |
+
# Stream response with tool execution
|
| 623 |
+
response_text = ""
|
| 624 |
+
try:
|
| 625 |
+
for chunk in assistant.chat_stream(context_message, history[:-2]):
|
| 626 |
+
response_text += chunk
|
| 627 |
+
# Update the last message (assistant's response) with accumulated text
|
| 628 |
+
history[-1] = {"role": "assistant", "content": response_text}
|
| 629 |
+
|
| 630 |
+
# Yield updated history to show streaming in real-time
|
| 631 |
+
yield (
|
| 632 |
+
history,
|
| 633 |
+
state,
|
| 634 |
+
code_preview.value,
|
| 635 |
+
None
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
except Exception as e:
|
| 639 |
+
response_text = f"❌ Error: {str(e)}"
|
| 640 |
+
history[-1] = {"role": "assistant", "content": response_text}
|
| 641 |
+
yield (
|
| 642 |
+
history,
|
| 643 |
+
state,
|
| 644 |
+
code_preview.value,
|
| 645 |
+
None
|
| 646 |
+
)
|
| 647 |
+
return
|
| 648 |
+
|
| 649 |
+
# Try to parse code from response for preview
|
| 650 |
+
parsed = assistant._parse_response(response_text)
|
| 651 |
+
|
| 652 |
+
# Update state and code preview
|
| 653 |
+
new_state = {**state}
|
| 654 |
+
new_code = code_preview.value
|
| 655 |
+
|
| 656 |
+
if parsed["code"]:
|
| 657 |
+
new_state["generated_code"] = parsed["code"]
|
| 658 |
+
new_state["generated_packages"] = parsed["packages"]
|
| 659 |
+
new_state["suggested_category"] = parsed["category"]
|
| 660 |
+
new_state["suggested_tags"] = parsed["tags"]
|
| 661 |
+
new_code = parsed["code"]
|
| 662 |
+
|
| 663 |
+
# Extract any deployment results from response
|
| 664 |
+
deployment_info = None
|
| 665 |
+
if "URL:" in response_text and "modal.run" in response_text:
|
| 666 |
+
# Try to extract URL for display
|
| 667 |
+
import re
|
| 668 |
+
url_match = re.search(r'https://[a-zA-Z0-9-]+--[a-zA-Z0-9-]+\.modal\.run', response_text)
|
| 669 |
+
if url_match:
|
| 670 |
+
deployment_info = {
|
| 671 |
+
"success": True,
|
| 672 |
+
"url": url_match.group(0),
|
| 673 |
+
"mcp_endpoint": url_match.group(0) + "/mcp/",
|
| 674 |
+
"message": "Deployment URL extracted from response"
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
# Final yield with all updates (code preview, deployment info)
|
| 678 |
+
yield (
|
| 679 |
+
history,
|
| 680 |
+
new_state,
|
| 681 |
+
new_code,
|
| 682 |
+
deployment_info
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
def manual_deploy(
|
| 686 |
+
code: str,
|
| 687 |
+
server_name: str,
|
| 688 |
+
category: str,
|
| 689 |
+
tags: str,
|
| 690 |
+
author: str,
|
| 691 |
+
packages: str,
|
| 692 |
+
):
|
| 693 |
+
"""Manually deploy code without AI assistance"""
|
| 694 |
+
|
| 695 |
+
if not code or code.startswith("#"):
|
| 696 |
+
return {"success": False, "error": "No code to deploy"}
|
| 697 |
+
|
| 698 |
+
if not server_name:
|
| 699 |
+
return {"success": False, "error": "Server name is required"}
|
| 700 |
+
|
| 701 |
+
# Parse tags and packages
|
| 702 |
+
tags_list = [t.strip() for t in tags.split(",") if t.strip()]
|
| 703 |
+
packages_str = packages.strip()
|
| 704 |
+
|
| 705 |
+
try:
|
| 706 |
+
result = deploy_mcp_server(
|
| 707 |
+
server_name=server_name,
|
| 708 |
+
mcp_tools_code=code,
|
| 709 |
+
extra_pip_packages=packages_str,
|
| 710 |
+
description=f"Manually deployed - {category}",
|
| 711 |
+
category=category,
|
| 712 |
+
tags=tags_list,
|
| 713 |
+
author=author,
|
| 714 |
+
)
|
| 715 |
+
return result
|
| 716 |
+
|
| 717 |
+
except Exception as e:
|
| 718 |
+
return {"success": False, "error": str(e)}
|
| 719 |
+
|
| 720 |
+
# Event handlers
|
| 721 |
+
# NOTE: All UI event handlers use api_visibility="private" to prevent exposure via MCP endpoint (Gradio 6.x)
|
| 722 |
+
|
| 723 |
+
# Update API key visibility when model changes (auto-create SambaNova assistant)
|
| 724 |
+
model_selector.change(
|
| 725 |
+
fn=update_api_key_visibility,
|
| 726 |
+
inputs=[model_selector, session_state],
|
| 727 |
+
outputs=[api_key_row, api_status, session_state],
|
| 728 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
# Validate API key or create assistant
|
| 732 |
+
validate_btn.click(
|
| 733 |
+
fn=validate_or_create_assistant,
|
| 734 |
+
inputs=[model_selector, api_key_input],
|
| 735 |
+
outputs=[api_status, session_state],
|
| 736 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
mode_selector.change(
|
| 740 |
+
fn=update_mode_visibility,
|
| 741 |
+
inputs=[mode_selector],
|
| 742 |
+
outputs=[deployment_selector, refresh_deployments_btn],
|
| 743 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
# Refresh deployments button
|
| 747 |
+
refresh_deployments_btn.click(
|
| 748 |
+
fn=refresh_deployments,
|
| 749 |
+
outputs=[deployment_selector],
|
| 750 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
# Load deployment context when a deployment is selected
|
| 754 |
+
deployment_selector.change(
|
| 755 |
+
fn=load_deployment_context,
|
| 756 |
+
inputs=[deployment_selector, session_state, chatbot],
|
| 757 |
+
outputs=[chatbot, session_state, code_preview],
|
| 758 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
# Send message on button click
|
| 762 |
+
send_btn.click(
|
| 763 |
+
fn=chat_response,
|
| 764 |
+
inputs=[msg_input, chatbot, session_state, mode_selector, deployment_selector],
|
| 765 |
+
outputs=[
|
| 766 |
+
chatbot,
|
| 767 |
+
session_state,
|
| 768 |
+
code_preview,
|
| 769 |
+
deployment_result
|
| 770 |
+
],
|
| 771 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 772 |
+
).then(
|
| 773 |
+
fn=lambda: "", # Clear input
|
| 774 |
+
outputs=[msg_input],
|
| 775 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
# Send message on Enter
|
| 779 |
+
msg_input.submit(
|
| 780 |
+
fn=chat_response,
|
| 781 |
+
inputs=[msg_input, chatbot, session_state, mode_selector, deployment_selector],
|
| 782 |
+
outputs=[
|
| 783 |
+
chatbot,
|
| 784 |
+
session_state,
|
| 785 |
+
code_preview,
|
| 786 |
+
deployment_result
|
| 787 |
+
],
|
| 788 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 789 |
+
).then(
|
| 790 |
+
fn=lambda: "",
|
| 791 |
+
outputs=[msg_input],
|
| 792 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 793 |
+
)
|
| 794 |
+
|
| 795 |
+
# Manual deploy button
|
| 796 |
+
manual_deploy_btn.click(
|
| 797 |
+
fn=manual_deploy,
|
| 798 |
+
inputs=[
|
| 799 |
+
code_preview,
|
| 800 |
+
server_name_input,
|
| 801 |
+
category_input,
|
| 802 |
+
tags_input,
|
| 803 |
+
author_input,
|
| 804 |
+
packages_input,
|
| 805 |
+
],
|
| 806 |
+
outputs=[deployment_result],
|
| 807 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
# Initialize interface on load
|
| 811 |
+
def initialize_interface(mode: str, model: str, state: dict):
|
| 812 |
+
"""
|
| 813 |
+
Initialize the interface when page loads.
|
| 814 |
+
|
| 815 |
+
This pre-loads deployment choices so they're ready immediately
|
| 816 |
+
when switching to modify mode.
|
| 817 |
+
"""
|
| 818 |
+
# Pre-load deployment choices
|
| 819 |
+
choices = load_deployment_choices()
|
| 820 |
+
print(f"[DEBUG] Page load: Preloaded {len(choices)} deployment choices")
|
| 821 |
+
|
| 822 |
+
# Update dropdown with choices (keep it hidden for now since mode is "create" by default)
|
| 823 |
+
dropdown_update = gr.update(
|
| 824 |
+
choices=choices,
|
| 825 |
+
visible=False # Will be shown when mode changes to "modify"
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
# Get API key visibility
|
| 829 |
+
api_key_row_update, api_status_text, new_state = update_api_key_visibility(model, state)
|
| 830 |
+
|
| 831 |
+
return (
|
| 832 |
+
dropdown_update, # deployment_selector
|
| 833 |
+
gr.update(visible=False), # refresh_deployments_btn
|
| 834 |
+
api_key_row_update, # api_key_row
|
| 835 |
+
api_status_text, # api_status
|
| 836 |
+
new_state # session_state
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
# Single load event that initializes everything
|
| 840 |
+
chat_interface.load(
|
| 841 |
+
fn=initialize_interface,
|
| 842 |
+
inputs=[mode_selector, model_selector, session_state],
|
| 843 |
+
outputs=[deployment_selector, refresh_deployments_btn, api_key_row, api_status, session_state],
|
| 844 |
+
api_visibility="private" # Prevent exposure as MCP tool
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
return chat_interface
|
ui_components/code_editor.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Code Editor UI Component
|
| 3 |
+
|
| 4 |
+
Interactive code editor for MCP tool development.
|
| 5 |
+
|
| 6 |
+
FIXES APPLIED:
|
| 7 |
+
- Added try/except error handling in load_code()
|
| 8 |
+
- Using .get() with defaults for ALL fields to prevent KeyError
|
| 9 |
+
- Better error messages and fallbacks
|
| 10 |
+
- Handles missing url, description, and other fields gracefully
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
from mcp_tools.deployment_tools import (
|
| 15 |
+
get_deployment_code,
|
| 16 |
+
list_deployments,
|
| 17 |
+
update_deployment_code,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def create_code_editor():
|
| 22 |
+
"""
|
| 23 |
+
Create the code editor UI component.
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
gr.Blocks: Code editor interface
|
| 27 |
+
"""
|
| 28 |
+
with gr.Blocks() as editor:
|
| 29 |
+
gr.Markdown("## 💻 Code Editor")
|
| 30 |
+
gr.Markdown("Edit and view your deployment code inline")
|
| 31 |
+
|
| 32 |
+
# Load Deployment Section
|
| 33 |
+
with gr.Row():
|
| 34 |
+
deployment_selector = gr.Dropdown(
|
| 35 |
+
label="Select Deployment",
|
| 36 |
+
choices=[],
|
| 37 |
+
interactive=True,
|
| 38 |
+
scale=3
|
| 39 |
+
)
|
| 40 |
+
load_btn = gr.Button("📥 Load Code", size="sm", scale=1)
|
| 41 |
+
refresh_deployments_btn = gr.Button("🔄", size="sm", scale=0)
|
| 42 |
+
|
| 43 |
+
# Deployment Info Display
|
| 44 |
+
with gr.Row():
|
| 45 |
+
with gr.Column(scale=1):
|
| 46 |
+
deployment_info = gr.Markdown("*Select a deployment to view details*")
|
| 47 |
+
with gr.Column(scale=1):
|
| 48 |
+
packages_display = gr.Textbox(
|
| 49 |
+
label="Current Packages",
|
| 50 |
+
interactive=True, # Allow editing packages
|
| 51 |
+
placeholder="No deployment loaded"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Code Editor Section
|
| 55 |
+
gr.Markdown("### 📝 MCP Tools Code")
|
| 56 |
+
code_editor = gr.Code(
|
| 57 |
+
language="python",
|
| 58 |
+
label="",
|
| 59 |
+
lines=20,
|
| 60 |
+
interactive=True,
|
| 61 |
+
value="# Load a deployment to view and edit code"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Tools Preview
|
| 65 |
+
tools_preview = gr.JSON(
|
| 66 |
+
label="📋 Detected Tools",
|
| 67 |
+
value={}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Action Buttons
|
| 71 |
+
with gr.Row():
|
| 72 |
+
save_btn = gr.Button("💾 Save & Redeploy", variant="primary", interactive=True)
|
| 73 |
+
preview_btn = gr.Button("👁️ Preview", variant="secondary")
|
| 74 |
+
deploy_btn = gr.Button("🚀 Deploy as New (Coming Soon)", interactive=False)
|
| 75 |
+
|
| 76 |
+
# Output/Result Display
|
| 77 |
+
output = gr.JSON(label="Result")
|
| 78 |
+
|
| 79 |
+
# Functions
|
| 80 |
+
def load_deployment_list():
|
| 81 |
+
"""Load list of deployments for dropdown"""
|
| 82 |
+
try:
|
| 83 |
+
result = list_deployments()
|
| 84 |
+
if result.get("success"):
|
| 85 |
+
choices = [
|
| 86 |
+
(dep.get("server_name", "Unknown"), dep.get("deployment_id", ""))
|
| 87 |
+
for dep in result.get("deployments", [])
|
| 88 |
+
if dep.get("deployment_id") # Only include if we have an ID
|
| 89 |
+
]
|
| 90 |
+
return gr.update(choices=choices)
|
| 91 |
+
return gr.update(choices=[])
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"Error loading deployment list: {e}")
|
| 94 |
+
return gr.update(choices=[])
|
| 95 |
+
|
| 96 |
+
def load_code(deployment_id):
|
| 97 |
+
"""
|
| 98 |
+
Load code for selected deployment.
|
| 99 |
+
|
| 100 |
+
✅ FIXED: Now uses .get() with defaults for ALL fields
|
| 101 |
+
✅ FIXED: Wrapped in try/except for error handling
|
| 102 |
+
"""
|
| 103 |
+
# Handle empty selection
|
| 104 |
+
if not deployment_id:
|
| 105 |
+
return (
|
| 106 |
+
"# Select a deployment first",
|
| 107 |
+
"*No deployment selected*",
|
| 108 |
+
"",
|
| 109 |
+
{},
|
| 110 |
+
{"success": False, "error": "No deployment selected"}
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
result = get_deployment_code(deployment_id)
|
| 115 |
+
|
| 116 |
+
if result.get("success"):
|
| 117 |
+
# ✅ FIX: Use .get() with defaults for ALL fields to prevent KeyError
|
| 118 |
+
deployment_id_val = result.get('deployment_id', 'N/A')
|
| 119 |
+
server_name = result.get('server_name', 'Unknown')
|
| 120 |
+
description = result.get('description', 'No description')
|
| 121 |
+
url = result.get('url', '')
|
| 122 |
+
mcp_endpoint = result.get('mcp_endpoint', '')
|
| 123 |
+
status = result.get('status', 'unknown')
|
| 124 |
+
category = result.get('category', 'Uncategorized')
|
| 125 |
+
author = result.get('author', 'Anonymous')
|
| 126 |
+
version = result.get('version', '1.0.0')
|
| 127 |
+
tools = result.get('tools', [])
|
| 128 |
+
packages = result.get('packages', [])
|
| 129 |
+
code = result.get('code', '# No code available')
|
| 130 |
+
created_at = result.get('created_at', 'Unknown')
|
| 131 |
+
|
| 132 |
+
# Format deployment info markdown
|
| 133 |
+
# ✅ FIX: Handle empty/None URL gracefully
|
| 134 |
+
if url:
|
| 135 |
+
url_display = f"[{url}]({url})"
|
| 136 |
+
else:
|
| 137 |
+
url_display = "*Not available*"
|
| 138 |
+
|
| 139 |
+
if mcp_endpoint:
|
| 140 |
+
mcp_display = f"`{mcp_endpoint}`"
|
| 141 |
+
else:
|
| 142 |
+
mcp_display = "*Not available*"
|
| 143 |
+
|
| 144 |
+
info_md = f"""
|
| 145 |
+
### 📋 Deployment Details
|
| 146 |
+
|
| 147 |
+
| Field | Value |
|
| 148 |
+
|-------|-------|
|
| 149 |
+
| **Deployment ID** | `{deployment_id_val}` |
|
| 150 |
+
| **Server Name** | {server_name} |
|
| 151 |
+
| **Description** | {description or 'N/A'} |
|
| 152 |
+
| **Status** | {status} |
|
| 153 |
+
| **Category** | {category} |
|
| 154 |
+
| **Author** | {author} |
|
| 155 |
+
| **Version** | {version} |
|
| 156 |
+
| **Created** | {created_at} |
|
| 157 |
+
| **Tools Count** | {len(tools)} detected |
|
| 158 |
+
|
| 159 |
+
**🔗 URL:** {url_display}
|
| 160 |
+
|
| 161 |
+
**📡 MCP Endpoint:** {mcp_display}
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
# Format packages string
|
| 165 |
+
packages_str = ", ".join(packages) if packages else "No extra packages"
|
| 166 |
+
|
| 167 |
+
return (
|
| 168 |
+
code if code else "# No code available",
|
| 169 |
+
info_md,
|
| 170 |
+
packages_str,
|
| 171 |
+
tools if tools else [],
|
| 172 |
+
result
|
| 173 |
+
)
|
| 174 |
+
else:
|
| 175 |
+
# Handle API error response
|
| 176 |
+
error_msg = result.get('error', 'Unknown error occurred')
|
| 177 |
+
return (
|
| 178 |
+
f"# Error loading code\n# {error_msg}",
|
| 179 |
+
f"### ❌ Error\n\n{error_msg}",
|
| 180 |
+
"",
|
| 181 |
+
{},
|
| 182 |
+
result
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
# ✅ FIX: Catch any unexpected exceptions
|
| 187 |
+
error_msg = str(e)
|
| 188 |
+
return (
|
| 189 |
+
f"# Exception occurred while loading code\n# {error_msg}",
|
| 190 |
+
f"### ❌ Exception\n\n```\n{error_msg}\n```\n\nPlease try refreshing the deployment list.",
|
| 191 |
+
"",
|
| 192 |
+
{},
|
| 193 |
+
{"success": False, "error": error_msg, "exception": True}
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
def preview_code(code):
|
| 197 |
+
"""Preview code analysis"""
|
| 198 |
+
if not code or code.startswith("#"):
|
| 199 |
+
return {"info": "Enter code to preview"}
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
# Basic code analysis
|
| 203 |
+
lines = code.split('\n')
|
| 204 |
+
tool_count = sum(1 for line in lines if '@mcp.tool()' in line)
|
| 205 |
+
import_count = sum(1 for line in lines if line.strip().startswith(('import ', 'from ')))
|
| 206 |
+
|
| 207 |
+
# Check for required components
|
| 208 |
+
has_fastmcp_import = 'from fastmcp import FastMCP' in code or 'import FastMCP' in code
|
| 209 |
+
has_mcp_instance = 'mcp = FastMCP(' in code
|
| 210 |
+
has_tools = tool_count > 0
|
| 211 |
+
|
| 212 |
+
# Validation status
|
| 213 |
+
is_valid = has_fastmcp_import and has_mcp_instance and has_tools
|
| 214 |
+
|
| 215 |
+
validation_issues = []
|
| 216 |
+
if not has_fastmcp_import:
|
| 217 |
+
validation_issues.append("Missing: from fastmcp import FastMCP")
|
| 218 |
+
if not has_mcp_instance:
|
| 219 |
+
validation_issues.append("Missing: mcp = FastMCP('server-name')")
|
| 220 |
+
if not has_tools:
|
| 221 |
+
validation_issues.append("Missing: @mcp.tool() decorated functions")
|
| 222 |
+
|
| 223 |
+
return {
|
| 224 |
+
"total_lines": len(lines),
|
| 225 |
+
"detected_tools": tool_count,
|
| 226 |
+
"imports": import_count,
|
| 227 |
+
"is_valid": is_valid,
|
| 228 |
+
"validation_issues": validation_issues if validation_issues else ["✅ All checks passed"],
|
| 229 |
+
"preview": "Code analysis complete"
|
| 230 |
+
}
|
| 231 |
+
except Exception as e:
|
| 232 |
+
return {
|
| 233 |
+
"error": str(e),
|
| 234 |
+
"preview": "Code analysis failed"
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
def save_and_redeploy(deployment_id, edited_code, packages_str):
|
| 238 |
+
"""Save edited code and redeploy to Modal"""
|
| 239 |
+
# Validate deployment selection
|
| 240 |
+
if not deployment_id:
|
| 241 |
+
return {"success": False, "error": "No deployment selected"}
|
| 242 |
+
|
| 243 |
+
# Validate code
|
| 244 |
+
if not edited_code:
|
| 245 |
+
return {"success": False, "error": "No code provided"}
|
| 246 |
+
|
| 247 |
+
if edited_code.startswith("# Load") or edited_code.startswith("# Error") or edited_code.startswith("# Select"):
|
| 248 |
+
return {"success": False, "error": "No valid code to save. Please load a deployment first."}
|
| 249 |
+
|
| 250 |
+
if edited_code.startswith("# Exception"):
|
| 251 |
+
return {"success": False, "error": "Cannot save error placeholder. Please load a valid deployment."}
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
# Convert packages string to list
|
| 255 |
+
package_list = []
|
| 256 |
+
if packages_str and not packages_str.startswith("No"):
|
| 257 |
+
package_list = [p.strip() for p in packages_str.split(",") if p.strip()]
|
| 258 |
+
|
| 259 |
+
# Call update function
|
| 260 |
+
result = update_deployment_code(
|
| 261 |
+
deployment_id=deployment_id,
|
| 262 |
+
mcp_tools_code=edited_code,
|
| 263 |
+
extra_pip_packages=package_list
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
return result
|
| 267 |
+
|
| 268 |
+
except Exception as e:
|
| 269 |
+
return {
|
| 270 |
+
"success": False,
|
| 271 |
+
"error": f"Exception during save: {str(e)}",
|
| 272 |
+
"exception": True
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
# Wire up events
|
| 276 |
+
refresh_deployments_btn.click(
|
| 277 |
+
fn=load_deployment_list,
|
| 278 |
+
outputs=deployment_selector,
|
| 279 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
load_btn.click(
|
| 283 |
+
fn=load_code,
|
| 284 |
+
inputs=[deployment_selector],
|
| 285 |
+
outputs=[code_editor, deployment_info, packages_display, tools_preview, output],
|
| 286 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
save_btn.click(
|
| 290 |
+
fn=save_and_redeploy,
|
| 291 |
+
inputs=[deployment_selector, code_editor, packages_display],
|
| 292 |
+
outputs=output,
|
| 293 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
preview_btn.click(
|
| 297 |
+
fn=preview_code,
|
| 298 |
+
inputs=[code_editor],
|
| 299 |
+
outputs=output,
|
| 300 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
# Load deployments on editor load
|
| 304 |
+
editor.load(
|
| 305 |
+
fn=load_deployment_list,
|
| 306 |
+
outputs=deployment_selector,
|
| 307 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
return editor
|
ui_components/log_viewer.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Log Viewer UI Component
|
| 3 |
+
|
| 4 |
+
Real-time deployment log viewer with filtering.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from utils.database import get_db
|
| 9 |
+
from utils.models import Deployment, DeploymentHistory
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def create_log_viewer():
|
| 13 |
+
"""
|
| 14 |
+
Create the log viewer UI component.
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
gr.Blocks: Log viewer interface
|
| 18 |
+
"""
|
| 19 |
+
with gr.Blocks() as viewer:
|
| 20 |
+
gr.Markdown("## 📝 Deployment Logs")
|
| 21 |
+
gr.Markdown("View deployment history and events")
|
| 22 |
+
|
| 23 |
+
# Filters
|
| 24 |
+
with gr.Row():
|
| 25 |
+
deployment_filter = gr.Dropdown(
|
| 26 |
+
label="Filter by Deployment",
|
| 27 |
+
choices=["All Deployments"],
|
| 28 |
+
value="All Deployments",
|
| 29 |
+
interactive=True,
|
| 30 |
+
scale=2
|
| 31 |
+
)
|
| 32 |
+
action_filter = gr.Dropdown(
|
| 33 |
+
label="Filter by Action",
|
| 34 |
+
choices=["all", "created", "code_updated", "metadata_updated", "deleted", "security_scan_passed", "security_scan_warning"],
|
| 35 |
+
value="all",
|
| 36 |
+
interactive=True,
|
| 37 |
+
scale=1
|
| 38 |
+
)
|
| 39 |
+
auto_refresh = gr.Checkbox(label="Auto-refresh", value=False, scale=0)
|
| 40 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm", scale=0)
|
| 41 |
+
|
| 42 |
+
# Log Display
|
| 43 |
+
logs_display = gr.Code(
|
| 44 |
+
language="shell",
|
| 45 |
+
label="Event Logs",
|
| 46 |
+
lines=25,
|
| 47 |
+
interactive=False,
|
| 48 |
+
value="Loading logs..."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Log Stats
|
| 52 |
+
with gr.Row():
|
| 53 |
+
total_events = gr.Textbox(label="Total Events", interactive=False, scale=1)
|
| 54 |
+
date_range = gr.Textbox(label="Date Range", interactive=False, scale=1)
|
| 55 |
+
|
| 56 |
+
# Functions
|
| 57 |
+
def load_deployment_list():
|
| 58 |
+
"""Load deployments for filter dropdown"""
|
| 59 |
+
try:
|
| 60 |
+
with get_db() as db:
|
| 61 |
+
deployments = Deployment.get_active_deployments(db)
|
| 62 |
+
choices = ["All Deployments"] + [
|
| 63 |
+
f"{dep.server_name} ({dep.deployment_id[:16]}...)"
|
| 64 |
+
for dep in deployments
|
| 65 |
+
]
|
| 66 |
+
return gr.Dropdown(choices=choices)
|
| 67 |
+
except Exception:
|
| 68 |
+
return gr.Dropdown(choices=["All Deployments"])
|
| 69 |
+
|
| 70 |
+
def load_logs(deployment_filter_val="All Deployments", action="all"):
|
| 71 |
+
"""Load and format deployment logs"""
|
| 72 |
+
try:
|
| 73 |
+
with get_db() as db:
|
| 74 |
+
query = db.query(DeploymentHistory)
|
| 75 |
+
|
| 76 |
+
# Filter by deployment if not "All"
|
| 77 |
+
if deployment_filter_val != "All Deployments" and "(" in deployment_filter_val:
|
| 78 |
+
# Extract deployment_id from filter value
|
| 79 |
+
dep_id_part = deployment_filter_val.split("(")[1].split("...")[0]
|
| 80 |
+
query = query.filter(DeploymentHistory.deployment_id.like(f"%{dep_id_part}%"))
|
| 81 |
+
|
| 82 |
+
# Filter by action if not "all"
|
| 83 |
+
if action != "all":
|
| 84 |
+
query = query.filter(DeploymentHistory.action == action)
|
| 85 |
+
|
| 86 |
+
# Get logs ordered by newest first
|
| 87 |
+
logs = query.order_by(DeploymentHistory.created_at.desc()).limit(100).all()
|
| 88 |
+
|
| 89 |
+
if not logs:
|
| 90 |
+
return (
|
| 91 |
+
"No logs found matching the selected filters.",
|
| 92 |
+
"0",
|
| 93 |
+
"N/A"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Format logs
|
| 97 |
+
log_text = ""
|
| 98 |
+
for log in logs:
|
| 99 |
+
timestamp = log.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
| 100 |
+
dep_id_short = log.deployment_id[:20] + "..." if len(log.deployment_id) > 20 else log.deployment_id
|
| 101 |
+
|
| 102 |
+
# Format action with emoji
|
| 103 |
+
action_emoji = {
|
| 104 |
+
"created": "✨",
|
| 105 |
+
"deleted": "🗑️",
|
| 106 |
+
"code_updated": "📝",
|
| 107 |
+
"metadata_updated": "⚙️",
|
| 108 |
+
"security_scan_passed": "✅",
|
| 109 |
+
"security_scan_warning": "⚠️",
|
| 110 |
+
"pre_update_backup": "💾"
|
| 111 |
+
}.get(log.action, "📋")
|
| 112 |
+
|
| 113 |
+
log_text += f"[{timestamp}] {action_emoji} {log.action.upper()}\n"
|
| 114 |
+
log_text += f" Deployment: {dep_id_short}\n"
|
| 115 |
+
|
| 116 |
+
# Add details if available
|
| 117 |
+
if log.details:
|
| 118 |
+
details_str = str(log.details)
|
| 119 |
+
if len(details_str) > 200:
|
| 120 |
+
details_str = details_str[:200] + "..."
|
| 121 |
+
log_text += f" Details: {details_str}\n"
|
| 122 |
+
|
| 123 |
+
log_text += "\n"
|
| 124 |
+
|
| 125 |
+
# Calculate stats
|
| 126 |
+
total = len(logs)
|
| 127 |
+
oldest = logs[-1].created_at if logs else None
|
| 128 |
+
newest = logs[0].created_at if logs else None
|
| 129 |
+
|
| 130 |
+
if oldest and newest:
|
| 131 |
+
date_range_str = f"{oldest.strftime('%Y-%m-%d')} to {newest.strftime('%Y-%m-%d')}"
|
| 132 |
+
else:
|
| 133 |
+
date_range_str = "N/A"
|
| 134 |
+
|
| 135 |
+
return (
|
| 136 |
+
log_text or "No logs available",
|
| 137 |
+
str(total),
|
| 138 |
+
date_range_str
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
return (
|
| 143 |
+
f"Error loading logs: {str(e)}",
|
| 144 |
+
"0",
|
| 145 |
+
"N/A"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Wire up events
|
| 149 |
+
refresh_btn.click(
|
| 150 |
+
fn=load_logs,
|
| 151 |
+
inputs=[deployment_filter, action_filter],
|
| 152 |
+
outputs=[logs_display, total_events, date_range],
|
| 153 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
deployment_filter.change(
|
| 157 |
+
fn=load_logs,
|
| 158 |
+
inputs=[deployment_filter, action_filter],
|
| 159 |
+
outputs=[logs_display, total_events, date_range],
|
| 160 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
action_filter.change(
|
| 164 |
+
fn=load_logs,
|
| 165 |
+
inputs=[deployment_filter, action_filter],
|
| 166 |
+
outputs=[logs_display, total_events, date_range],
|
| 167 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Load deployment list and logs on viewer load
|
| 171 |
+
viewer.load(
|
| 172 |
+
fn=load_deployment_list,
|
| 173 |
+
outputs=deployment_filter,
|
| 174 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
viewer.load(
|
| 178 |
+
fn=load_logs,
|
| 179 |
+
outputs=[logs_display, total_events, date_range],
|
| 180 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
return viewer
|
ui_components/stats_dashboard.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Statistics Dashboard UI Component
|
| 3 |
+
|
| 4 |
+
Analytics and visualization dashboard for deployments.
|
| 5 |
+
Refactored from the standalone stats_dashboard.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import plotly.graph_objects as go
|
| 11 |
+
import plotly.express as px
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
from utils.database import get_db
|
| 15 |
+
from utils.models import Deployment
|
| 16 |
+
from utils.usage_tracker import (
|
| 17 |
+
get_deployment_statistics,
|
| 18 |
+
get_tool_usage_breakdown,
|
| 19 |
+
get_usage_timeline,
|
| 20 |
+
get_client_statistics,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Helper Functions (prefixed with _ to hide from MCP auto-discovery)
|
| 25 |
+
def _get_deployment_list():
|
| 26 |
+
"""Get list of all active deployments for dropdown."""
|
| 27 |
+
try:
|
| 28 |
+
with get_db() as db:
|
| 29 |
+
deployments = Deployment.get_active_deployments(db)
|
| 30 |
+
if not deployments:
|
| 31 |
+
return []
|
| 32 |
+
return [f"{dep.server_name} ({dep.deployment_id})" for dep in deployments]
|
| 33 |
+
except Exception:
|
| 34 |
+
return []
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _extract_deployment_id(selection: str) -> Optional[str]:
|
| 38 |
+
"""Extract deployment_id from dropdown selection."""
|
| 39 |
+
if not selection or "(" not in selection:
|
| 40 |
+
return None
|
| 41 |
+
return selection.split("(")[1].rstrip(")")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _format_number(num):
|
| 45 |
+
"""Format large numbers with K, M suffixes."""
|
| 46 |
+
if num is None:
|
| 47 |
+
return "N/A"
|
| 48 |
+
if num >= 1_000_000:
|
| 49 |
+
return f"{num/1_000_000:.1f}M"
|
| 50 |
+
if num >= 1_000:
|
| 51 |
+
return f"{num/1_000:.1f}K"
|
| 52 |
+
return str(int(num))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _format_duration(ms):
|
| 56 |
+
"""Format milliseconds into human-readable duration."""
|
| 57 |
+
if ms is None:
|
| 58 |
+
return "N/A"
|
| 59 |
+
if ms < 1000:
|
| 60 |
+
return f"{int(ms)}ms"
|
| 61 |
+
seconds = ms / 1000
|
| 62 |
+
if seconds < 60:
|
| 63 |
+
return f"{seconds:.1f}s"
|
| 64 |
+
minutes = seconds / 60
|
| 65 |
+
return f"{minutes:.1f}m"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _create_metric_card(title: str, value: str, subtitle: str = "") -> str:
|
| 69 |
+
"""Create an HTML metric card."""
|
| 70 |
+
return f"""
|
| 71 |
+
<div style="
|
| 72 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 73 |
+
border-radius: 12px;
|
| 74 |
+
padding: 24px;
|
| 75 |
+
color: white;
|
| 76 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 77 |
+
margin: 8px 0;
|
| 78 |
+
">
|
| 79 |
+
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">{title}</div>
|
| 80 |
+
<div style="font-size: 32px; font-weight: bold; margin-bottom: 4px;">{value}</div>
|
| 81 |
+
<div style="font-size: 12px; opacity: 0.8;">{subtitle}</div>
|
| 82 |
+
</div>
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _create_timeline_chart(deployment_id: str, days: int = 7):
|
| 87 |
+
"""Create timeline chart showing requests over time."""
|
| 88 |
+
timeline = get_usage_timeline(deployment_id, days=days, granularity="day")
|
| 89 |
+
|
| 90 |
+
if not timeline or len(timeline) == 0:
|
| 91 |
+
fig = go.Figure()
|
| 92 |
+
fig.add_annotation(text="No usage data available", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16, color="gray"))
|
| 93 |
+
fig.update_layout(title="Requests Over Time", height=300)
|
| 94 |
+
return fig
|
| 95 |
+
|
| 96 |
+
df = pd.DataFrame(timeline)
|
| 97 |
+
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
| 98 |
+
|
| 99 |
+
fig = go.Figure()
|
| 100 |
+
fig.add_trace(go.Scatter(
|
| 101 |
+
x=df['timestamp'], y=df['requests'], mode='lines+markers', name='Requests',
|
| 102 |
+
line=dict(color='#667eea', width=3), marker=dict(size=8, color='#764ba2'),
|
| 103 |
+
fill='tozeroy', fillcolor='rgba(102, 126, 234, 0.2)',
|
| 104 |
+
))
|
| 105 |
+
|
| 106 |
+
fig.update_layout(
|
| 107 |
+
title=f"Requests Over Time (Last {days} days)",
|
| 108 |
+
xaxis_title="Date", yaxis_title="Requests", height=350,
|
| 109 |
+
plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)',
|
| 110 |
+
)
|
| 111 |
+
return fig
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _create_tool_usage_chart(deployment_id: str, days: int = 30):
|
| 115 |
+
"""Create bar chart for tool usage."""
|
| 116 |
+
tools = get_tool_usage_breakdown(deployment_id, days=days, limit=10)
|
| 117 |
+
|
| 118 |
+
if not tools or len(tools) == 0:
|
| 119 |
+
fig = go.Figure()
|
| 120 |
+
fig.add_annotation(text="No tool usage data available", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16, color="gray"))
|
| 121 |
+
fig.update_layout(title="Top Tools Used", height=300)
|
| 122 |
+
return fig
|
| 123 |
+
|
| 124 |
+
df = pd.DataFrame(tools)
|
| 125 |
+
fig = go.Figure()
|
| 126 |
+
fig.add_trace(go.Bar(
|
| 127 |
+
x=df['count'], y=df['tool_name'], orientation='h',
|
| 128 |
+
marker=dict(color=df['count'], colorscale='Viridis', showscale=False),
|
| 129 |
+
text=df['count'], textposition='outside',
|
| 130 |
+
))
|
| 131 |
+
|
| 132 |
+
fig.update_layout(
|
| 133 |
+
title=f"Top Tools Used (Last {days} days)",
|
| 134 |
+
xaxis_title="Number of Calls", yaxis_title="Tool Name",
|
| 135 |
+
height=max(300, len(tools) * 40),
|
| 136 |
+
plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)',
|
| 137 |
+
)
|
| 138 |
+
return fig
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _load_deployment_stats(deployment_selection: str, days: int = 30):
|
| 142 |
+
"""Load and display statistics for selected deployment."""
|
| 143 |
+
if not deployment_selection:
|
| 144 |
+
return ("<div style='text-align: center; padding: 40px; color: gray;'>Please select a deployment</div>", None, None, "")
|
| 145 |
+
|
| 146 |
+
deployment_id = _extract_deployment_id(deployment_selection)
|
| 147 |
+
if not deployment_id:
|
| 148 |
+
return ("<div style='text-align: center; padding: 40px; color: red;'>Invalid deployment</div>", None, None, "")
|
| 149 |
+
|
| 150 |
+
stats = get_deployment_statistics(deployment_id, days=days)
|
| 151 |
+
if not stats:
|
| 152 |
+
return ("<div style='text-align: center; padding: 40px; color: red;'>Failed to load statistics</div>", None, None, "")
|
| 153 |
+
|
| 154 |
+
# Create metric cards
|
| 155 |
+
total_requests = _format_number(stats.get('total_requests', 0))
|
| 156 |
+
success_rate = stats.get('success_rate_percent', 0)
|
| 157 |
+
avg_time = _format_duration(stats.get('avg_response_time_ms'))
|
| 158 |
+
failed_requests = _format_number(stats.get('failed_requests', 0))
|
| 159 |
+
|
| 160 |
+
metrics_html = f"""
|
| 161 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 20px 0;">
|
| 162 |
+
{_create_metric_card("Total Requests", total_requests, f"Last {days} days")}
|
| 163 |
+
{_create_metric_card("Success Rate", f"{success_rate:.1f}%", f"{failed_requests} failures")}
|
| 164 |
+
{_create_metric_card("Avg Response Time", avg_time, "Per request")}
|
| 165 |
+
{_create_metric_card("Active Period", f"{days} days", "Data retention")}
|
| 166 |
+
</div>
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
# Get deployment info
|
| 170 |
+
with get_db() as db:
|
| 171 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 172 |
+
if deployment:
|
| 173 |
+
last_used = deployment.last_used_at.strftime("%Y-%m-%d %H:%M UTC") if deployment.last_used_at else "Never"
|
| 174 |
+
created = deployment.created_at.strftime("%Y-%m-%d %H:%M UTC") if deployment.created_at else "Unknown"
|
| 175 |
+
info_text = f"""
|
| 176 |
+
**Deployment Information**
|
| 177 |
+
- **Server Name:** {deployment.server_name}
|
| 178 |
+
- **Status:** {deployment.status}
|
| 179 |
+
- **Created:** {created}
|
| 180 |
+
- **Last Used:** {last_used}
|
| 181 |
+
- **URL:** {deployment.url}
|
| 182 |
+
"""
|
| 183 |
+
else:
|
| 184 |
+
info_text = "Deployment information not available"
|
| 185 |
+
|
| 186 |
+
# Create charts
|
| 187 |
+
timeline_chart = _create_timeline_chart(deployment_id, days)
|
| 188 |
+
tool_chart = _create_tool_usage_chart(deployment_id, days)
|
| 189 |
+
|
| 190 |
+
return (metrics_html, timeline_chart, tool_chart, info_text)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def create_stats_dashboard():
|
| 194 |
+
"""
|
| 195 |
+
Create the statistics dashboard UI component.
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
gr.Blocks: Stats dashboard interface
|
| 199 |
+
"""
|
| 200 |
+
with gr.Blocks() as dashboard:
|
| 201 |
+
gr.Markdown("## 📊 Statistics Dashboard")
|
| 202 |
+
gr.Markdown("Monitor and analyze your deployed MCP servers")
|
| 203 |
+
|
| 204 |
+
with gr.Row():
|
| 205 |
+
with gr.Column(scale=3):
|
| 206 |
+
deployment_dropdown = gr.Dropdown(
|
| 207 |
+
choices=_get_deployment_list(),
|
| 208 |
+
label="Select Deployment",
|
| 209 |
+
info="Choose a deployment to view its statistics",
|
| 210 |
+
interactive=True,
|
| 211 |
+
)
|
| 212 |
+
with gr.Column(scale=1):
|
| 213 |
+
days_slider = gr.Slider(
|
| 214 |
+
minimum=1, maximum=90, value=30, step=1,
|
| 215 |
+
label="Time Range (days)",
|
| 216 |
+
info="Number of days to analyze"
|
| 217 |
+
)
|
| 218 |
+
with gr.Column(scale=1):
|
| 219 |
+
refresh_btn = gr.Button("🔄 Refresh", variant="secondary", size="sm")
|
| 220 |
+
|
| 221 |
+
# Metrics Cards
|
| 222 |
+
metrics_html = gr.HTML(
|
| 223 |
+
"<div style='text-align: center; padding: 40px; color: gray;'>Select a deployment to view statistics</div>"
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Charts Row
|
| 227 |
+
with gr.Row():
|
| 228 |
+
with gr.Column():
|
| 229 |
+
timeline_plot = gr.Plot(label="Request Timeline")
|
| 230 |
+
with gr.Column():
|
| 231 |
+
tool_plot = gr.Plot(label="Tool Usage")
|
| 232 |
+
|
| 233 |
+
# Deployment Info
|
| 234 |
+
with gr.Accordion("📋 Deployment Details", open=False):
|
| 235 |
+
deployment_info = gr.Markdown("Select a deployment to view details")
|
| 236 |
+
|
| 237 |
+
# Event handlers
|
| 238 |
+
def _update_stats(deployment, days):
|
| 239 |
+
return _load_deployment_stats(deployment, int(days))
|
| 240 |
+
|
| 241 |
+
deployment_dropdown.change(
|
| 242 |
+
fn=_update_stats,
|
| 243 |
+
inputs=[deployment_dropdown, days_slider],
|
| 244 |
+
outputs=[metrics_html, timeline_plot, tool_plot, deployment_info],
|
| 245 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
days_slider.change(
|
| 249 |
+
fn=_update_stats,
|
| 250 |
+
inputs=[deployment_dropdown, days_slider],
|
| 251 |
+
outputs=[metrics_html, timeline_plot, tool_plot, deployment_info],
|
| 252 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
refresh_btn.click(
|
| 256 |
+
fn=lambda: gr.Dropdown(choices=_get_deployment_list()),
|
| 257 |
+
outputs=[deployment_dropdown],
|
| 258 |
+
api_visibility="private" # Don't expose UI handler as MCP tool
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
return dashboard
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility modules for MCP deployment platform
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .database import get_db, db_transaction, get_db_session, check_database_connection
|
| 6 |
+
from .models import Deployment, DeploymentPackage, DeploymentFile, DeploymentHistory, UsageEvent
|
| 7 |
+
from .security_scanner import scan_code_for_security
|
| 8 |
+
from .usage_tracker import (
|
| 9 |
+
track_usage,
|
| 10 |
+
get_deployment_statistics,
|
| 11 |
+
get_tool_usage_breakdown,
|
| 12 |
+
get_usage_timeline,
|
| 13 |
+
get_client_statistics,
|
| 14 |
+
get_all_deployments_stats,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
'get_db',
|
| 19 |
+
'db_transaction',
|
| 20 |
+
'get_db_session',
|
| 21 |
+
'check_database_connection',
|
| 22 |
+
'Deployment',
|
| 23 |
+
'DeploymentPackage',
|
| 24 |
+
'DeploymentFile',
|
| 25 |
+
'DeploymentHistory',
|
| 26 |
+
'UsageEvent',
|
| 27 |
+
'scan_code_for_security',
|
| 28 |
+
'track_usage',
|
| 29 |
+
'get_deployment_statistics',
|
| 30 |
+
'get_tool_usage_breakdown',
|
| 31 |
+
'get_usage_timeline',
|
| 32 |
+
'get_client_statistics',
|
| 33 |
+
'get_all_deployments_stats',
|
| 34 |
+
]
|
utils/database.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database Connection Management for MCP Server
|
| 3 |
+
|
| 4 |
+
This module handles PostgreSQL database connections using SQLAlchemy.
|
| 5 |
+
Provides session management, connection pooling, and transaction handling.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
from contextlib import contextmanager
|
| 11 |
+
from typing import Generator
|
| 12 |
+
|
| 13 |
+
from sqlalchemy import create_engine, event, text
|
| 14 |
+
from sqlalchemy.engine import Engine
|
| 15 |
+
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
| 16 |
+
from sqlalchemy.orm import sessionmaker, Session
|
| 17 |
+
from sqlalchemy.pool import QueuePool
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# Configuration
|
| 21 |
+
# ============================================================================
|
| 22 |
+
|
| 23 |
+
# Get database URL from environment variable
|
| 24 |
+
# IMPORTANT: DATABASE_URL must be set in environment - no default provided for security
|
| 25 |
+
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 26 |
+
|
| 27 |
+
# Connection pool configuration
|
| 28 |
+
POOL_SIZE = 5 # Number of connections to keep in the pool
|
| 29 |
+
MAX_OVERFLOW = 10 # Maximum number of connections that can be created beyond pool_size
|
| 30 |
+
POOL_TIMEOUT = 30 # Seconds to wait for connection from pool
|
| 31 |
+
POOL_RECYCLE = 3600 # Recycle connections after 1 hour
|
| 32 |
+
|
| 33 |
+
# Retry configuration
|
| 34 |
+
MAX_RETRIES = 3
|
| 35 |
+
RETRY_DELAY = 1 # seconds
|
| 36 |
+
|
| 37 |
+
# ============================================================================
|
| 38 |
+
# Engine Creation (Lazy Initialization)
|
| 39 |
+
# ============================================================================
|
| 40 |
+
|
| 41 |
+
# Global engine and session factory - initialized lazily
|
| 42 |
+
_engine: Engine = None
|
| 43 |
+
_SessionLocal = None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _get_engine() -> Engine:
|
| 47 |
+
"""
|
| 48 |
+
Get or create the database engine (lazy initialization).
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Engine: SQLAlchemy engine instance
|
| 52 |
+
|
| 53 |
+
Raises:
|
| 54 |
+
ValueError: If DATABASE_URL is not set
|
| 55 |
+
"""
|
| 56 |
+
global _engine
|
| 57 |
+
|
| 58 |
+
if _engine is not None:
|
| 59 |
+
return _engine
|
| 60 |
+
|
| 61 |
+
if not DATABASE_URL:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
"DATABASE_URL environment variable is not set. "
|
| 64 |
+
"Please set it to your PostgreSQL connection string."
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Create engine with connection pooling
|
| 68 |
+
_engine = create_engine(
|
| 69 |
+
DATABASE_URL,
|
| 70 |
+
poolclass=QueuePool,
|
| 71 |
+
pool_size=POOL_SIZE,
|
| 72 |
+
max_overflow=MAX_OVERFLOW,
|
| 73 |
+
pool_timeout=POOL_TIMEOUT,
|
| 74 |
+
pool_recycle=POOL_RECYCLE,
|
| 75 |
+
pool_pre_ping=True, # Test connections before using them
|
| 76 |
+
echo=False, # Set to True for SQL query logging (debugging)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Add connection event listeners
|
| 80 |
+
@event.listens_for(_engine, "connect")
|
| 81 |
+
def receive_connect(dbapi_conn, connection_record):
|
| 82 |
+
"""Event listener for new connections."""
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
@event.listens_for(_engine, "checkout")
|
| 86 |
+
def receive_checkout(dbapi_conn, connection_record, connection_proxy):
|
| 87 |
+
"""Event listener for connection checkout from pool."""
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
return _engine
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _get_session_factory():
|
| 94 |
+
"""Get or create the session factory (lazy initialization)."""
|
| 95 |
+
global _SessionLocal
|
| 96 |
+
|
| 97 |
+
if _SessionLocal is not None:
|
| 98 |
+
return _SessionLocal
|
| 99 |
+
|
| 100 |
+
_SessionLocal = sessionmaker(
|
| 101 |
+
autocommit=False,
|
| 102 |
+
autoflush=False,
|
| 103 |
+
bind=_get_engine(),
|
| 104 |
+
)
|
| 105 |
+
return _SessionLocal
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# Legacy compatibility - these now use lazy initialization
|
| 109 |
+
@property
|
| 110 |
+
def engine():
|
| 111 |
+
"""Lazy engine property for backward compatibility."""
|
| 112 |
+
return _get_engine()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def create_db_engine() -> Engine:
|
| 116 |
+
"""
|
| 117 |
+
Create or get SQLAlchemy engine with connection pooling.
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
Engine: SQLAlchemy engine instance
|
| 121 |
+
|
| 122 |
+
Raises:
|
| 123 |
+
ValueError: If DATABASE_URL is not set
|
| 124 |
+
"""
|
| 125 |
+
return _get_engine()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# Backward compatible SessionLocal - use get_session_factory() for new code
|
| 129 |
+
class SessionLocalProxy:
|
| 130 |
+
"""Proxy class for lazy SessionLocal initialization."""
|
| 131 |
+
|
| 132 |
+
def __call__(self):
|
| 133 |
+
return _get_session_factory()()
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
SessionLocal = SessionLocalProxy()
|
| 137 |
+
|
| 138 |
+
# ============================================================================
|
| 139 |
+
# Session Management
|
| 140 |
+
# ============================================================================
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def get_db_session() -> Session:
|
| 144 |
+
"""
|
| 145 |
+
Get a new database session.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Session: SQLAlchemy session instance
|
| 149 |
+
|
| 150 |
+
Example:
|
| 151 |
+
>>> session = get_db_session()
|
| 152 |
+
>>> try:
|
| 153 |
+
>>> # Use session
|
| 154 |
+
>>> session.commit()
|
| 155 |
+
>>> finally:
|
| 156 |
+
>>> session.close()
|
| 157 |
+
"""
|
| 158 |
+
return SessionLocal()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@contextmanager
|
| 162 |
+
def get_db() -> Generator[Session, None, None]:
|
| 163 |
+
"""
|
| 164 |
+
Context manager for database sessions.
|
| 165 |
+
|
| 166 |
+
Automatically handles session lifecycle and rollback on errors.
|
| 167 |
+
|
| 168 |
+
Yields:
|
| 169 |
+
Session: SQLAlchemy session instance
|
| 170 |
+
|
| 171 |
+
Example:
|
| 172 |
+
>>> with get_db() as db:
|
| 173 |
+
>>> deployment = db.query(Deployment).first()
|
| 174 |
+
>>> db.commit()
|
| 175 |
+
"""
|
| 176 |
+
session = SessionLocal()
|
| 177 |
+
try:
|
| 178 |
+
yield session
|
| 179 |
+
except Exception:
|
| 180 |
+
session.rollback()
|
| 181 |
+
raise
|
| 182 |
+
finally:
|
| 183 |
+
session.close()
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
@contextmanager
|
| 187 |
+
def db_transaction() -> Generator[Session, None, None]:
|
| 188 |
+
"""
|
| 189 |
+
Context manager for database transactions with automatic commit/rollback.
|
| 190 |
+
|
| 191 |
+
The transaction is automatically committed if no exception occurs,
|
| 192 |
+
and rolled back if an exception is raised.
|
| 193 |
+
|
| 194 |
+
Yields:
|
| 195 |
+
Session: SQLAlchemy session instance
|
| 196 |
+
|
| 197 |
+
Example:
|
| 198 |
+
>>> with db_transaction() as db:
|
| 199 |
+
>>> deployment = Deployment(...)
|
| 200 |
+
>>> db.add(deployment)
|
| 201 |
+
>>> # Automatically committed on successful exit
|
| 202 |
+
"""
|
| 203 |
+
session = SessionLocal()
|
| 204 |
+
try:
|
| 205 |
+
yield session
|
| 206 |
+
session.commit()
|
| 207 |
+
except Exception:
|
| 208 |
+
session.rollback()
|
| 209 |
+
raise
|
| 210 |
+
finally:
|
| 211 |
+
session.close()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# ============================================================================
|
| 215 |
+
# Retry Logic
|
| 216 |
+
# ============================================================================
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def execute_with_retry(func, *args, max_retries=MAX_RETRIES, **kwargs):
|
| 220 |
+
"""
|
| 221 |
+
Execute a database operation with retry logic.
|
| 222 |
+
|
| 223 |
+
Retries the operation if it fails due to connection issues.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
func: Function to execute
|
| 227 |
+
*args: Positional arguments for func
|
| 228 |
+
max_retries: Maximum number of retry attempts
|
| 229 |
+
**kwargs: Keyword arguments for func
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Result of func execution
|
| 233 |
+
|
| 234 |
+
Raises:
|
| 235 |
+
Exception: If all retry attempts fail
|
| 236 |
+
|
| 237 |
+
Example:
|
| 238 |
+
>>> result = execute_with_retry(
|
| 239 |
+
>>> lambda: db.query(Deployment).all()
|
| 240 |
+
>>> )
|
| 241 |
+
"""
|
| 242 |
+
last_exception = None
|
| 243 |
+
|
| 244 |
+
for attempt in range(max_retries):
|
| 245 |
+
try:
|
| 246 |
+
return func(*args, **kwargs)
|
| 247 |
+
except OperationalError as e:
|
| 248 |
+
last_exception = e
|
| 249 |
+
if attempt < max_retries - 1:
|
| 250 |
+
time.sleep(RETRY_DELAY * (attempt + 1)) # Exponential backoff
|
| 251 |
+
continue
|
| 252 |
+
raise
|
| 253 |
+
except SQLAlchemyError:
|
| 254 |
+
raise
|
| 255 |
+
|
| 256 |
+
# If we get here, all retries failed
|
| 257 |
+
if last_exception:
|
| 258 |
+
raise last_exception
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# ============================================================================
|
| 262 |
+
# Health Check
|
| 263 |
+
# ============================================================================
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def check_database_connection() -> bool:
|
| 267 |
+
"""
|
| 268 |
+
Check if database connection is healthy.
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
bool: True if connection is successful, False otherwise
|
| 272 |
+
|
| 273 |
+
Example:
|
| 274 |
+
>>> if check_database_connection():
|
| 275 |
+
>>> print("Database is connected")
|
| 276 |
+
>>> else:
|
| 277 |
+
>>> print("Database connection failed")
|
| 278 |
+
"""
|
| 279 |
+
try:
|
| 280 |
+
with get_db() as db:
|
| 281 |
+
# Execute a simple query to test connection
|
| 282 |
+
db.execute(text("SELECT 1"))
|
| 283 |
+
return True
|
| 284 |
+
except Exception as e:
|
| 285 |
+
print(f"Database connection check failed: {e}")
|
| 286 |
+
return False
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def get_database_info() -> dict:
|
| 290 |
+
"""
|
| 291 |
+
Get database connection information.
|
| 292 |
+
|
| 293 |
+
Returns:
|
| 294 |
+
dict: Database connection details
|
| 295 |
+
|
| 296 |
+
Example:
|
| 297 |
+
>>> info = get_database_info()
|
| 298 |
+
>>> print(f"Connected to: {info['database']}")
|
| 299 |
+
"""
|
| 300 |
+
try:
|
| 301 |
+
with get_db() as db:
|
| 302 |
+
result = db.execute(
|
| 303 |
+
text("""
|
| 304 |
+
SELECT
|
| 305 |
+
current_database() as database,
|
| 306 |
+
current_user as user,
|
| 307 |
+
version() as version,
|
| 308 |
+
inet_server_addr() as host,
|
| 309 |
+
inet_server_port() as port
|
| 310 |
+
""")
|
| 311 |
+
).first()
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
"database": result[0],
|
| 315 |
+
"user": result[1],
|
| 316 |
+
"version": result[2],
|
| 317 |
+
"host": result[3],
|
| 318 |
+
"port": result[4],
|
| 319 |
+
"connected": True,
|
| 320 |
+
}
|
| 321 |
+
except Exception as e:
|
| 322 |
+
return {
|
| 323 |
+
"connected": False,
|
| 324 |
+
"error": str(e),
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ============================================================================
|
| 329 |
+
# Cleanup
|
| 330 |
+
# ============================================================================
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def close_database_connections():
|
| 334 |
+
"""
|
| 335 |
+
Close all database connections and dispose of the engine.
|
| 336 |
+
|
| 337 |
+
Call this when shutting down the application.
|
| 338 |
+
|
| 339 |
+
Example:
|
| 340 |
+
>>> close_database_connections()
|
| 341 |
+
"""
|
| 342 |
+
global _engine
|
| 343 |
+
if _engine:
|
| 344 |
+
_engine.dispose()
|
| 345 |
+
_engine = None
|
| 346 |
+
print("Database connections closed")
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# ============================================================================
|
| 350 |
+
# Initialization
|
| 351 |
+
# ============================================================================
|
| 352 |
+
|
| 353 |
+
if __name__ == "__main__":
|
| 354 |
+
# Test database connection
|
| 355 |
+
print("Testing database connection...")
|
| 356 |
+
print("-" * 60)
|
| 357 |
+
|
| 358 |
+
if check_database_connection():
|
| 359 |
+
print("✓ Database connection successful!")
|
| 360 |
+
print()
|
| 361 |
+
info = get_database_info()
|
| 362 |
+
print("Database Information:")
|
| 363 |
+
print(f" Database: {info.get('database', 'N/A')}")
|
| 364 |
+
print(f" User: {info.get('user', 'N/A')}")
|
| 365 |
+
print(f" Host: {info.get('host', 'N/A')}")
|
| 366 |
+
print(f" Port: {info.get('port', 'N/A')}")
|
| 367 |
+
print()
|
| 368 |
+
print(f" PostgreSQL Version:")
|
| 369 |
+
version = info.get('version', 'N/A')
|
| 370 |
+
# Print first line of version (can be long)
|
| 371 |
+
print(f" {version.split(',')[0] if version else 'N/A'}")
|
| 372 |
+
else:
|
| 373 |
+
print("✗ Database connection failed!")
|
| 374 |
+
print()
|
| 375 |
+
print("Please check:")
|
| 376 |
+
print(" 1. DATABASE_URL environment variable is set correctly")
|
| 377 |
+
print(" 2. PostgreSQL server is running")
|
| 378 |
+
print(" 3. Network connectivity to database")
|
| 379 |
+
print(" 4. Database credentials are correct")
|
| 380 |
+
|
| 381 |
+
print("-" * 60)
|
utils/models.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SQLAlchemy ORM Models for MCP Server
|
| 3 |
+
|
| 4 |
+
Defines database models for deployment management and usage tracking.
|
| 5 |
+
|
| 6 |
+
FIXES APPLIED:
|
| 7 |
+
- DeploymentFile check constraint now includes 'tools_manifest'
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from typing import List, Optional, Dict, Any
|
| 12 |
+
|
| 13 |
+
from sqlalchemy import (
|
| 14 |
+
Column,
|
| 15 |
+
Integer,
|
| 16 |
+
String,
|
| 17 |
+
Text,
|
| 18 |
+
Float,
|
| 19 |
+
Boolean,
|
| 20 |
+
TIMESTAMP,
|
| 21 |
+
ForeignKey,
|
| 22 |
+
Index,
|
| 23 |
+
CheckConstraint,
|
| 24 |
+
)
|
| 25 |
+
from sqlalchemy.dialects.postgresql import JSONB
|
| 26 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 27 |
+
from sqlalchemy.orm import relationship, Session
|
| 28 |
+
from sqlalchemy.sql import func
|
| 29 |
+
|
| 30 |
+
# Base class for all models
|
| 31 |
+
Base = declarative_base()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ============================================================================
|
| 35 |
+
# DEPLOYMENT MODEL
|
| 36 |
+
# ============================================================================
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Deployment(Base):
|
| 40 |
+
"""
|
| 41 |
+
Main deployment model storing MCP server deployment information.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
__tablename__ = "deployments"
|
| 45 |
+
|
| 46 |
+
# Primary key
|
| 47 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 48 |
+
|
| 49 |
+
# Unique identifiers
|
| 50 |
+
deployment_id = Column(String(255), unique=True, nullable=False, index=True)
|
| 51 |
+
app_name = Column(String(255), unique=True, nullable=False, index=True)
|
| 52 |
+
server_name = Column(String(255), nullable=False)
|
| 53 |
+
|
| 54 |
+
# Deployment details
|
| 55 |
+
url = Column(Text, nullable=True)
|
| 56 |
+
mcp_endpoint = Column(Text, nullable=True)
|
| 57 |
+
description = Column(Text, nullable=True)
|
| 58 |
+
status = Column(String(50), default="deployed")
|
| 59 |
+
|
| 60 |
+
# Organization and metadata (new fields)
|
| 61 |
+
category = Column(String(100), nullable=True, default="Uncategorized", index=True)
|
| 62 |
+
tags = Column(JSONB, nullable=True, default=[]) # List of tags
|
| 63 |
+
author = Column(String(255), nullable=True, default="Anonymous")
|
| 64 |
+
version = Column(String(50), nullable=True, default="1.0.0")
|
| 65 |
+
documentation = Column(Text, nullable=True) # Markdown documentation
|
| 66 |
+
|
| 67 |
+
# Timestamps
|
| 68 |
+
created_at = Column(TIMESTAMP, nullable=False, default=datetime.utcnow, index=True)
|
| 69 |
+
updated_at = Column(TIMESTAMP, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 70 |
+
deleted_at = Column(TIMESTAMP, nullable=True, index=True) # Soft delete
|
| 71 |
+
|
| 72 |
+
# Usage statistics (cached for quick access)
|
| 73 |
+
total_requests = Column(Integer, default=0)
|
| 74 |
+
last_used_at = Column(TIMESTAMP, nullable=True, index=True)
|
| 75 |
+
avg_response_time_ms = Column(Float, nullable=True)
|
| 76 |
+
|
| 77 |
+
# Relationships
|
| 78 |
+
packages = relationship(
|
| 79 |
+
"DeploymentPackage",
|
| 80 |
+
back_populates="deployment",
|
| 81 |
+
cascade="all, delete-orphan",
|
| 82 |
+
)
|
| 83 |
+
files = relationship(
|
| 84 |
+
"DeploymentFile",
|
| 85 |
+
back_populates="deployment",
|
| 86 |
+
cascade="all, delete-orphan",
|
| 87 |
+
)
|
| 88 |
+
history = relationship(
|
| 89 |
+
"DeploymentHistory",
|
| 90 |
+
back_populates="deployment",
|
| 91 |
+
cascade="all, delete-orphan",
|
| 92 |
+
)
|
| 93 |
+
usage_events = relationship(
|
| 94 |
+
"UsageEvent",
|
| 95 |
+
back_populates="deployment",
|
| 96 |
+
cascade="all, delete-orphan",
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Constraints
|
| 100 |
+
__table_args__ = (
|
| 101 |
+
CheckConstraint("deployment_id != ''", name="deployments_deployment_id_check"),
|
| 102 |
+
CheckConstraint("app_name != ''", name="deployments_app_name_check"),
|
| 103 |
+
CheckConstraint("server_name != ''", name="deployments_server_name_check"),
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
def __repr__(self):
|
| 107 |
+
return f"<Deployment(id={self.id}, deployment_id='{self.deployment_id}', server_name='{self.server_name}')>"
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def is_deleted(self) -> bool:
|
| 111 |
+
"""Check if deployment is soft deleted."""
|
| 112 |
+
return self.deleted_at is not None
|
| 113 |
+
|
| 114 |
+
def soft_delete(self):
|
| 115 |
+
"""Soft delete this deployment."""
|
| 116 |
+
self.deleted_at = datetime.utcnow()
|
| 117 |
+
self.status = "deleted"
|
| 118 |
+
|
| 119 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 120 |
+
"""Convert deployment to dictionary."""
|
| 121 |
+
return {
|
| 122 |
+
"id": self.id,
|
| 123 |
+
"deployment_id": self.deployment_id,
|
| 124 |
+
"app_name": self.app_name,
|
| 125 |
+
"server_name": self.server_name,
|
| 126 |
+
"url": self.url,
|
| 127 |
+
"mcp_endpoint": self.mcp_endpoint,
|
| 128 |
+
"description": self.description,
|
| 129 |
+
"status": self.status,
|
| 130 |
+
"category": self.category,
|
| 131 |
+
"tags": self.tags or [],
|
| 132 |
+
"author": self.author,
|
| 133 |
+
"version": self.version,
|
| 134 |
+
"documentation": self.documentation,
|
| 135 |
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
| 136 |
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
| 137 |
+
"deleted_at": self.deleted_at.isoformat() if self.deleted_at else None,
|
| 138 |
+
"total_requests": self.total_requests,
|
| 139 |
+
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
| 140 |
+
"avg_response_time_ms": self.avg_response_time_ms,
|
| 141 |
+
"packages": [pkg.package_name for pkg in self.packages],
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
def update_usage_stats(self, duration_ms: float):
|
| 145 |
+
"""
|
| 146 |
+
Update usage statistics for this deployment.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
duration_ms: Response time in milliseconds
|
| 150 |
+
"""
|
| 151 |
+
self.total_requests += 1
|
| 152 |
+
self.last_used_at = datetime.utcnow()
|
| 153 |
+
|
| 154 |
+
# Update average response time (moving average)
|
| 155 |
+
if self.avg_response_time_ms is None:
|
| 156 |
+
self.avg_response_time_ms = duration_ms
|
| 157 |
+
else:
|
| 158 |
+
# Weighted average: 90% old average, 10% new value
|
| 159 |
+
self.avg_response_time_ms = (
|
| 160 |
+
0.9 * self.avg_response_time_ms + 0.1 * duration_ms
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
@staticmethod
|
| 164 |
+
def get_active_deployments(db: Session) -> List["Deployment"]:
|
| 165 |
+
"""Get all active (non-deleted) deployments."""
|
| 166 |
+
return (
|
| 167 |
+
db.query(Deployment)
|
| 168 |
+
.filter(Deployment.deleted_at.is_(None))
|
| 169 |
+
.order_by(Deployment.created_at.desc())
|
| 170 |
+
.all()
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
@staticmethod
|
| 174 |
+
def get_by_deployment_id(
|
| 175 |
+
db: Session, deployment_id: str, include_deleted: bool = False
|
| 176 |
+
) -> Optional["Deployment"]:
|
| 177 |
+
"""Get deployment by deployment_id."""
|
| 178 |
+
query = db.query(Deployment).filter(
|
| 179 |
+
Deployment.deployment_id == deployment_id
|
| 180 |
+
)
|
| 181 |
+
if not include_deleted:
|
| 182 |
+
query = query.filter(Deployment.deleted_at.is_(None))
|
| 183 |
+
return query.first()
|
| 184 |
+
|
| 185 |
+
@staticmethod
|
| 186 |
+
def get_by_app_name(
|
| 187 |
+
db: Session, app_name: str, include_deleted: bool = False
|
| 188 |
+
) -> Optional["Deployment"]:
|
| 189 |
+
"""Get deployment by app_name."""
|
| 190 |
+
query = db.query(Deployment).filter(Deployment.app_name == app_name)
|
| 191 |
+
if not include_deleted:
|
| 192 |
+
query = query.filter(Deployment.deleted_at.is_(None))
|
| 193 |
+
return query.first()
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ============================================================================
|
| 197 |
+
# DEPLOYMENT PACKAGE MODEL
|
| 198 |
+
# ============================================================================
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class DeploymentPackage(Base):
|
| 202 |
+
"""
|
| 203 |
+
Model for Python packages required by deployments.
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
__tablename__ = "deployment_packages"
|
| 207 |
+
|
| 208 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 209 |
+
deployment_id = Column(
|
| 210 |
+
String(255),
|
| 211 |
+
ForeignKey("deployments.deployment_id", ondelete="CASCADE"),
|
| 212 |
+
nullable=False,
|
| 213 |
+
index=True,
|
| 214 |
+
)
|
| 215 |
+
package_name = Column(String(255), nullable=False)
|
| 216 |
+
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
| 217 |
+
|
| 218 |
+
# Relationship
|
| 219 |
+
deployment = relationship("Deployment", back_populates="packages")
|
| 220 |
+
|
| 221 |
+
# Constraints
|
| 222 |
+
__table_args__ = (
|
| 223 |
+
Index("unique_deployment_package", "deployment_id", "package_name", unique=True),
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
def __repr__(self):
|
| 227 |
+
return f"<DeploymentPackage(deployment_id='{self.deployment_id}', package='{self.package_name}')>"
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
# ============================================================================
|
| 231 |
+
# DEPLOYMENT FILE MODEL
|
| 232 |
+
# ============================================================================
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class DeploymentFile(Base):
|
| 236 |
+
"""
|
| 237 |
+
Model for storing deployment code files.
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
__tablename__ = "deployment_files"
|
| 241 |
+
|
| 242 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 243 |
+
deployment_id = Column(
|
| 244 |
+
String(255),
|
| 245 |
+
ForeignKey("deployments.deployment_id", ondelete="CASCADE"),
|
| 246 |
+
nullable=False,
|
| 247 |
+
index=True,
|
| 248 |
+
)
|
| 249 |
+
file_type = Column(String(50), nullable=False, index=True) # 'app', 'original_tools', or 'tools_manifest'
|
| 250 |
+
file_path = Column(Text, nullable=True) # For backward compatibility
|
| 251 |
+
file_content = Column(Text, nullable=True) # Actual Python code
|
| 252 |
+
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
| 253 |
+
|
| 254 |
+
# Relationship
|
| 255 |
+
deployment = relationship("Deployment", back_populates="files")
|
| 256 |
+
|
| 257 |
+
# ✅ FIX: Updated constraint to include 'tools_manifest'
|
| 258 |
+
__table_args__ = (
|
| 259 |
+
CheckConstraint(
|
| 260 |
+
"file_type IN ('app', 'original_tools', 'tools_manifest')", # ✅ ADDED tools_manifest
|
| 261 |
+
name="deployment_files_type_check",
|
| 262 |
+
),
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
def __repr__(self):
|
| 266 |
+
return f"<DeploymentFile(deployment_id='{self.deployment_id}', type='{self.file_type}')>"
|
| 267 |
+
|
| 268 |
+
@staticmethod
|
| 269 |
+
def get_file(
|
| 270 |
+
db: Session, deployment_id: str, file_type: str
|
| 271 |
+
) -> Optional["DeploymentFile"]:
|
| 272 |
+
"""Get a specific file for a deployment."""
|
| 273 |
+
return (
|
| 274 |
+
db.query(DeploymentFile)
|
| 275 |
+
.filter(
|
| 276 |
+
DeploymentFile.deployment_id == deployment_id,
|
| 277 |
+
DeploymentFile.file_type == file_type,
|
| 278 |
+
)
|
| 279 |
+
.first()
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# ============================================================================
|
| 284 |
+
# DEPLOYMENT HISTORY MODEL
|
| 285 |
+
# ============================================================================
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
class DeploymentHistory(Base):
|
| 289 |
+
"""
|
| 290 |
+
Audit log for deployment lifecycle events.
|
| 291 |
+
"""
|
| 292 |
+
|
| 293 |
+
__tablename__ = "deployment_history"
|
| 294 |
+
|
| 295 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 296 |
+
deployment_id = Column(
|
| 297 |
+
String(255),
|
| 298 |
+
ForeignKey("deployments.deployment_id", ondelete="CASCADE"),
|
| 299 |
+
nullable=False,
|
| 300 |
+
index=True,
|
| 301 |
+
)
|
| 302 |
+
action = Column(String(50), nullable=False, index=True)
|
| 303 |
+
details = Column(JSONB, nullable=True)
|
| 304 |
+
created_at = Column(TIMESTAMP, default=datetime.utcnow, index=True)
|
| 305 |
+
|
| 306 |
+
# Relationship
|
| 307 |
+
deployment = relationship("Deployment", back_populates="history")
|
| 308 |
+
|
| 309 |
+
def __repr__(self):
|
| 310 |
+
return f"<DeploymentHistory(deployment_id='{self.deployment_id}', action='{self.action}')>"
|
| 311 |
+
|
| 312 |
+
@staticmethod
|
| 313 |
+
def log_event(
|
| 314 |
+
db: Session,
|
| 315 |
+
deployment_id: str,
|
| 316 |
+
action: str,
|
| 317 |
+
details: Optional[Dict[str, Any]] = None,
|
| 318 |
+
):
|
| 319 |
+
"""Log a deployment event."""
|
| 320 |
+
event = DeploymentHistory(
|
| 321 |
+
deployment_id=deployment_id,
|
| 322 |
+
action=action,
|
| 323 |
+
details=details or {},
|
| 324 |
+
)
|
| 325 |
+
db.add(event)
|
| 326 |
+
db.flush()
|
| 327 |
+
return event
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# ============================================================================
|
| 331 |
+
# USAGE EVENT MODEL
|
| 332 |
+
# ============================================================================
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class UsageEvent(Base):
|
| 336 |
+
"""
|
| 337 |
+
Model for tracking deployment usage events and statistics.
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
__tablename__ = "usage_events"
|
| 341 |
+
|
| 342 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 343 |
+
deployment_id = Column(
|
| 344 |
+
String(255),
|
| 345 |
+
ForeignKey("deployments.deployment_id", ondelete="CASCADE"),
|
| 346 |
+
nullable=False,
|
| 347 |
+
index=True,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
# Request details
|
| 351 |
+
tool_name = Column(String(255), nullable=True, index=True)
|
| 352 |
+
client_id = Column(String(255), nullable=True, index=True)
|
| 353 |
+
|
| 354 |
+
# Performance metrics
|
| 355 |
+
duration_ms = Column(Integer, nullable=True)
|
| 356 |
+
|
| 357 |
+
# Status
|
| 358 |
+
success = Column(Boolean, default=True, index=True)
|
| 359 |
+
error_message = Column(Text, nullable=True)
|
| 360 |
+
|
| 361 |
+
# Request metadata (renamed from 'metadata' to avoid SQLAlchemy reserved word)
|
| 362 |
+
request_metadata = Column("metadata", JSONB, nullable=True)
|
| 363 |
+
timestamp = Column(TIMESTAMP, default=datetime.utcnow, index=True)
|
| 364 |
+
|
| 365 |
+
# Relationship
|
| 366 |
+
deployment = relationship("Deployment", back_populates="usage_events")
|
| 367 |
+
|
| 368 |
+
# Composite index for common queries
|
| 369 |
+
__table_args__ = (
|
| 370 |
+
Index("idx_usage_events_deployment_timestamp", "deployment_id", "timestamp"),
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
def __repr__(self):
|
| 374 |
+
return f"<UsageEvent(deployment_id='{self.deployment_id}', tool='{self.tool_name}', timestamp='{self.timestamp}')>"
|
| 375 |
+
|
| 376 |
+
@staticmethod
|
| 377 |
+
def record_usage(
|
| 378 |
+
db: Session,
|
| 379 |
+
deployment_id: str,
|
| 380 |
+
tool_name: Optional[str] = None,
|
| 381 |
+
client_id: Optional[str] = None,
|
| 382 |
+
duration_ms: Optional[int] = None,
|
| 383 |
+
success: bool = True,
|
| 384 |
+
error_message: Optional[str] = None,
|
| 385 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 386 |
+
) -> "UsageEvent":
|
| 387 |
+
"""
|
| 388 |
+
Record a usage event.
|
| 389 |
+
|
| 390 |
+
Args:
|
| 391 |
+
db: Database session
|
| 392 |
+
deployment_id: Deployment identifier
|
| 393 |
+
tool_name: Name of tool/function called
|
| 394 |
+
client_id: Client identifier
|
| 395 |
+
duration_ms: Request duration in milliseconds
|
| 396 |
+
success: Whether request succeeded
|
| 397 |
+
error_message: Error message if failed
|
| 398 |
+
metadata: Additional metadata
|
| 399 |
+
|
| 400 |
+
Returns:
|
| 401 |
+
UsageEvent: Created usage event
|
| 402 |
+
"""
|
| 403 |
+
event = UsageEvent(
|
| 404 |
+
deployment_id=deployment_id,
|
| 405 |
+
tool_name=tool_name,
|
| 406 |
+
client_id=client_id,
|
| 407 |
+
duration_ms=duration_ms,
|
| 408 |
+
success=success,
|
| 409 |
+
error_message=error_message,
|
| 410 |
+
request_metadata=metadata or {},
|
| 411 |
+
)
|
| 412 |
+
db.add(event)
|
| 413 |
+
db.flush()
|
| 414 |
+
|
| 415 |
+
# Update deployment statistics
|
| 416 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 417 |
+
if deployment and duration_ms is not None:
|
| 418 |
+
deployment.update_usage_stats(duration_ms)
|
| 419 |
+
|
| 420 |
+
return event
|
| 421 |
+
|
| 422 |
+
@staticmethod
|
| 423 |
+
def get_stats(
|
| 424 |
+
db: Session,
|
| 425 |
+
deployment_id: str,
|
| 426 |
+
days: int = 30,
|
| 427 |
+
) -> Dict[str, Any]:
|
| 428 |
+
"""
|
| 429 |
+
Get usage statistics for a deployment.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
db: Database session
|
| 433 |
+
deployment_id: Deployment identifier
|
| 434 |
+
days: Number of days to look back
|
| 435 |
+
|
| 436 |
+
Returns:
|
| 437 |
+
dict: Usage statistics
|
| 438 |
+
"""
|
| 439 |
+
from sqlalchemy import and_
|
| 440 |
+
|
| 441 |
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
| 442 |
+
|
| 443 |
+
# Base query for time period
|
| 444 |
+
base_query = db.query(UsageEvent).filter(
|
| 445 |
+
and_(
|
| 446 |
+
UsageEvent.deployment_id == deployment_id,
|
| 447 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 448 |
+
)
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
# Total requests
|
| 452 |
+
total_requests = base_query.count()
|
| 453 |
+
|
| 454 |
+
# Success rate
|
| 455 |
+
successful_requests = base_query.filter(UsageEvent.success == True).count()
|
| 456 |
+
success_rate = (
|
| 457 |
+
(successful_requests / total_requests * 100) if total_requests > 0 else 0
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Average response time
|
| 461 |
+
avg_duration = (
|
| 462 |
+
db.query(func.avg(UsageEvent.duration_ms))
|
| 463 |
+
.filter(
|
| 464 |
+
and_(
|
| 465 |
+
UsageEvent.deployment_id == deployment_id,
|
| 466 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 467 |
+
UsageEvent.duration_ms.isnot(None),
|
| 468 |
+
)
|
| 469 |
+
)
|
| 470 |
+
.scalar()
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
# Most used tools
|
| 474 |
+
tool_stats = (
|
| 475 |
+
db.query(
|
| 476 |
+
UsageEvent.tool_name,
|
| 477 |
+
func.count(UsageEvent.id).label("count"),
|
| 478 |
+
)
|
| 479 |
+
.filter(
|
| 480 |
+
and_(
|
| 481 |
+
UsageEvent.deployment_id == deployment_id,
|
| 482 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 483 |
+
UsageEvent.tool_name.isnot(None),
|
| 484 |
+
)
|
| 485 |
+
)
|
| 486 |
+
.group_by(UsageEvent.tool_name)
|
| 487 |
+
.order_by(func.count(UsageEvent.id).desc())
|
| 488 |
+
.limit(10)
|
| 489 |
+
.all()
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# Client stats
|
| 493 |
+
client_stats = (
|
| 494 |
+
db.query(
|
| 495 |
+
UsageEvent.client_id,
|
| 496 |
+
func.count(UsageEvent.id).label("count"),
|
| 497 |
+
)
|
| 498 |
+
.filter(
|
| 499 |
+
and_(
|
| 500 |
+
UsageEvent.deployment_id == deployment_id,
|
| 501 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 502 |
+
UsageEvent.client_id.isnot(None),
|
| 503 |
+
)
|
| 504 |
+
)
|
| 505 |
+
.group_by(UsageEvent.client_id)
|
| 506 |
+
.order_by(func.count(UsageEvent.id).desc())
|
| 507 |
+
.limit(10)
|
| 508 |
+
.all()
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
return {
|
| 512 |
+
"period_days": days,
|
| 513 |
+
"total_requests": total_requests,
|
| 514 |
+
"successful_requests": successful_requests,
|
| 515 |
+
"failed_requests": total_requests - successful_requests,
|
| 516 |
+
"success_rate_percent": round(success_rate, 2),
|
| 517 |
+
"avg_response_time_ms": round(avg_duration, 2) if avg_duration else None,
|
| 518 |
+
"top_tools": [
|
| 519 |
+
{"tool_name": tool, "count": count} for tool, count in tool_stats
|
| 520 |
+
],
|
| 521 |
+
"top_clients": [
|
| 522 |
+
{"client_id": client, "count": count} for client, count in client_stats
|
| 523 |
+
],
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
# ============================================================================
|
| 528 |
+
# Helper Functions
|
| 529 |
+
# ============================================================================
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def create_all_tables(engine):
|
| 533 |
+
"""
|
| 534 |
+
Create all tables in the database.
|
| 535 |
+
|
| 536 |
+
Args:
|
| 537 |
+
engine: SQLAlchemy engine
|
| 538 |
+
"""
|
| 539 |
+
Base.metadata.create_all(engine)
|
| 540 |
+
print("All tables created successfully")
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
def drop_all_tables(engine):
|
| 544 |
+
"""
|
| 545 |
+
Drop all tables from the database.
|
| 546 |
+
|
| 547 |
+
Args:
|
| 548 |
+
engine: SQLAlchemy engine
|
| 549 |
+
"""
|
| 550 |
+
Base.metadata.drop_all(engine)
|
| 551 |
+
print("All tables dropped successfully")
|
utils/security_scanner.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Security Scanner Module - AI-powered vulnerability detection for MCP deployments
|
| 4 |
+
|
| 5 |
+
Uses Nebius AI to analyze Python code for security vulnerabilities before deployment.
|
| 6 |
+
Focuses on real threats: code injection, malicious behavior, resource abuse.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from typing import Optional
|
| 14 |
+
from openai import OpenAI
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Cache for security scan results (code_hash -> scan_result)
|
| 18 |
+
# Avoids re-scanning identical code
|
| 19 |
+
_scan_cache = {}
|
| 20 |
+
_cache_expiry = {}
|
| 21 |
+
CACHE_TTL_SECONDS = 3600 # 1 hour
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _get_code_hash(code: str) -> str:
|
| 25 |
+
"""Generate SHA256 hash of code for caching"""
|
| 26 |
+
return hashlib.sha256(code.encode('utf-8')).hexdigest()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _get_cached_scan(code_hash: str) -> Optional[dict]:
|
| 30 |
+
"""Retrieve cached scan result if still valid"""
|
| 31 |
+
if code_hash in _scan_cache:
|
| 32 |
+
expiry = _cache_expiry.get(code_hash)
|
| 33 |
+
if expiry and datetime.now() < expiry:
|
| 34 |
+
return _scan_cache[code_hash]
|
| 35 |
+
else:
|
| 36 |
+
# Expired, remove from cache
|
| 37 |
+
_scan_cache.pop(code_hash, None)
|
| 38 |
+
_cache_expiry.pop(code_hash, None)
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _cache_scan_result(code_hash: str, result: dict):
|
| 43 |
+
"""Cache scan result with TTL"""
|
| 44 |
+
_scan_cache[code_hash] = result
|
| 45 |
+
_cache_expiry[code_hash] = datetime.now() + timedelta(seconds=CACHE_TTL_SECONDS)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _map_severity(malicious_type: str) -> str:
|
| 49 |
+
"""
|
| 50 |
+
Map malicious type to severity level.
|
| 51 |
+
|
| 52 |
+
Critical: Immediate threat to system/data
|
| 53 |
+
High: Significant vulnerability
|
| 54 |
+
Medium: Potential issue
|
| 55 |
+
Low: Minor concern
|
| 56 |
+
Safe: No issues
|
| 57 |
+
"""
|
| 58 |
+
severity_map = {
|
| 59 |
+
# Critical threats
|
| 60 |
+
"ransomware": "critical",
|
| 61 |
+
"backdoor": "critical",
|
| 62 |
+
"remote_access_tool": "critical",
|
| 63 |
+
"credential_harvesting": "critical",
|
| 64 |
+
|
| 65 |
+
# High severity
|
| 66 |
+
"sql_injection": "high",
|
| 67 |
+
"command_injection": "high",
|
| 68 |
+
"ddos_script": "high",
|
| 69 |
+
|
| 70 |
+
# Medium severity
|
| 71 |
+
"obfuscated_suspicious": "medium",
|
| 72 |
+
"trojan": "medium",
|
| 73 |
+
"keylogger": "medium",
|
| 74 |
+
|
| 75 |
+
# Low severity
|
| 76 |
+
"other": "low",
|
| 77 |
+
"virus": "low",
|
| 78 |
+
"worm": "low",
|
| 79 |
+
|
| 80 |
+
# Safe
|
| 81 |
+
"none": "safe"
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
return severity_map.get(malicious_type.lower(), "medium")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _build_security_prompt(code: str, context: dict) -> str:
|
| 88 |
+
"""
|
| 89 |
+
Build comprehensive security analysis prompt.
|
| 90 |
+
|
| 91 |
+
Focuses on real threats while ignoring false positives like hardcoded keys
|
| 92 |
+
(since all deployed code is public on Modal.com).
|
| 93 |
+
"""
|
| 94 |
+
server_name = context.get("server_name", "Unknown")
|
| 95 |
+
packages = context.get("packages", [])
|
| 96 |
+
description = context.get("description", "")
|
| 97 |
+
|
| 98 |
+
prompt = f"""You are an expert security analyst reviewing Python code for MCP server deployments on Modal.com.
|
| 99 |
+
|
| 100 |
+
**IMPORTANT CONTEXT:**
|
| 101 |
+
- All deployed code is PUBLIC and visible to anyone
|
| 102 |
+
- Hardcoded API keys/credentials are NOT a security threat for this platform (though bad practice)
|
| 103 |
+
- Focus on vulnerabilities that could harm the platform or users
|
| 104 |
+
|
| 105 |
+
**Code to Analyze:**
|
| 106 |
+
```python
|
| 107 |
+
{code}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**Deployment Context:**
|
| 111 |
+
- Server Name: {server_name}
|
| 112 |
+
- Packages: {', '.join(packages) if packages else 'None'}
|
| 113 |
+
- Description: {description}
|
| 114 |
+
|
| 115 |
+
**Check for REAL THREATS (flag these):**
|
| 116 |
+
|
| 117 |
+
1. **Code Injection Vulnerabilities:**
|
| 118 |
+
- eval() or exec() with user input
|
| 119 |
+
- subprocess calls with unsanitized input (especially shell=True)
|
| 120 |
+
- SQL queries using string concatenation
|
| 121 |
+
- Dynamic imports from user input
|
| 122 |
+
|
| 123 |
+
2. **Malicious Network Behavior:**
|
| 124 |
+
- Data exfiltration to suspicious domains
|
| 125 |
+
- Command & Control (C2) communication patterns
|
| 126 |
+
- Cryptocurrency mining
|
| 127 |
+
- Unusual outbound connections to non-standard ports
|
| 128 |
+
|
| 129 |
+
3. **Resource Abuse:**
|
| 130 |
+
- Infinite loops or recursive calls
|
| 131 |
+
- Memory exhaustion attacks
|
| 132 |
+
- CPU intensive operations without limits
|
| 133 |
+
- Denial of Service patterns
|
| 134 |
+
|
| 135 |
+
4. **Destructive Operations:**
|
| 136 |
+
- Attempts to escape sandbox/container
|
| 137 |
+
- System file manipulation
|
| 138 |
+
- Process manipulation (killing other processes)
|
| 139 |
+
- Privilege escalation attempts
|
| 140 |
+
|
| 141 |
+
5. **Malicious Packages:**
|
| 142 |
+
- Known malicious PyPI packages
|
| 143 |
+
- Typosquatting package names
|
| 144 |
+
- Packages with known CVEs
|
| 145 |
+
|
| 146 |
+
**DO NOT FLAG (these are acceptable):**
|
| 147 |
+
- Hardcoded API keys, passwords, or tokens (code is public anyway)
|
| 148 |
+
- Legitimate external API calls (OpenAI, Anthropic, etc.)
|
| 149 |
+
- Normal file operations (reading/writing files in sandbox)
|
| 150 |
+
- Standard web requests to known services
|
| 151 |
+
- Environment variable usage
|
| 152 |
+
|
| 153 |
+
**Provide detailed analysis with specific line references if issues found.**
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
return prompt
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def scan_code_for_security(code: str, context: dict) -> dict:
|
| 160 |
+
"""
|
| 161 |
+
Scan Python code for security vulnerabilities using Nebius AI.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
code: The Python code to scan
|
| 165 |
+
context: Dictionary with deployment context:
|
| 166 |
+
- server_name: Name of the server
|
| 167 |
+
- packages: List of pip packages
|
| 168 |
+
- description: Server description
|
| 169 |
+
- deployment_id: Optional deployment ID
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
dict with:
|
| 173 |
+
- scan_completed: bool (whether scan finished)
|
| 174 |
+
- is_safe: bool (whether code is safe to deploy)
|
| 175 |
+
- severity: str ("safe", "low", "medium", "high", "critical")
|
| 176 |
+
- malicious_type: str (type of threat or "none")
|
| 177 |
+
- explanation: str (human-readable explanation)
|
| 178 |
+
- reasoning_steps: list[str] (AI's reasoning process)
|
| 179 |
+
- issues: list[dict] (specific issues found)
|
| 180 |
+
- recommendation: str (what to do)
|
| 181 |
+
- scanned_at: str (ISO timestamp)
|
| 182 |
+
- cached: bool (whether result came from cache)
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
# Check if scanning is enabled
|
| 186 |
+
if os.getenv("SECURITY_SCANNING_ENABLED", "true").lower() != "true":
|
| 187 |
+
return {
|
| 188 |
+
"scan_completed": False,
|
| 189 |
+
"is_safe": True,
|
| 190 |
+
"severity": "safe",
|
| 191 |
+
"malicious_type": "none",
|
| 192 |
+
"explanation": "Security scanning is disabled",
|
| 193 |
+
"reasoning_steps": ["Security scanning disabled via SECURITY_SCANNING_ENABLED=false"],
|
| 194 |
+
"issues": [],
|
| 195 |
+
"recommendation": "Allow (scanning disabled)",
|
| 196 |
+
"scanned_at": datetime.now().isoformat(),
|
| 197 |
+
"cached": False
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# Check cache first
|
| 201 |
+
code_hash = _get_code_hash(code)
|
| 202 |
+
cached_result = _get_cached_scan(code_hash)
|
| 203 |
+
if cached_result:
|
| 204 |
+
cached_result["cached"] = True
|
| 205 |
+
return cached_result
|
| 206 |
+
|
| 207 |
+
# Get API key
|
| 208 |
+
api_key = os.getenv("NEBIUS_API_KEY")
|
| 209 |
+
if not api_key:
|
| 210 |
+
# Fall back to warning mode if no API key
|
| 211 |
+
return {
|
| 212 |
+
"scan_completed": False,
|
| 213 |
+
"is_safe": True,
|
| 214 |
+
"severity": "safe",
|
| 215 |
+
"malicious_type": "none",
|
| 216 |
+
"explanation": "NEBIUS_API_KEY not configured - security scanning unavailable",
|
| 217 |
+
"reasoning_steps": ["No API key found in environment"],
|
| 218 |
+
"issues": [],
|
| 219 |
+
"recommendation": "Warn (no API key)",
|
| 220 |
+
"scanned_at": datetime.now().isoformat(),
|
| 221 |
+
"cached": False
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
# Initialize Nebius client (OpenAI-compatible)
|
| 226 |
+
client = OpenAI(
|
| 227 |
+
base_url="https://api.tokenfactory.nebius.com/v1/",
|
| 228 |
+
api_key=api_key
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# Build security analysis prompt
|
| 232 |
+
prompt = _build_security_prompt(code, context)
|
| 233 |
+
|
| 234 |
+
# Call Nebius API with structured JSON schema
|
| 235 |
+
response = client.chat.completions.create(
|
| 236 |
+
model="Qwen/Qwen3-32B-fast",
|
| 237 |
+
temperature=0.6,
|
| 238 |
+
top_p=0.95,
|
| 239 |
+
timeout=30.0, # 30 second timeout
|
| 240 |
+
response_format={
|
| 241 |
+
"type": "json_schema",
|
| 242 |
+
"json_schema": {
|
| 243 |
+
"name": "security_analysis_schema",
|
| 244 |
+
"strict": True,
|
| 245 |
+
"schema": {
|
| 246 |
+
"type": "object",
|
| 247 |
+
"properties": {
|
| 248 |
+
"reasoning_steps": {
|
| 249 |
+
"type": "array",
|
| 250 |
+
"items": {
|
| 251 |
+
"type": "string"
|
| 252 |
+
},
|
| 253 |
+
"description": "The reasoning steps leading to the final conclusion."
|
| 254 |
+
},
|
| 255 |
+
"is_malicious": {
|
| 256 |
+
"type": "boolean",
|
| 257 |
+
"description": "Indicates whether the provided code or content is malicious (true) or safe/non-malicious (false)."
|
| 258 |
+
},
|
| 259 |
+
"malicious_type": {
|
| 260 |
+
"type": "string",
|
| 261 |
+
"enum": [
|
| 262 |
+
"none",
|
| 263 |
+
"virus",
|
| 264 |
+
"worm",
|
| 265 |
+
"ransomware",
|
| 266 |
+
"trojan",
|
| 267 |
+
"keylogger",
|
| 268 |
+
"backdoor",
|
| 269 |
+
"remote_access_tool",
|
| 270 |
+
"sql_injection",
|
| 271 |
+
"command_injection",
|
| 272 |
+
"ddos_script",
|
| 273 |
+
"credential_harvesting",
|
| 274 |
+
"obfuscated_suspicious",
|
| 275 |
+
"other"
|
| 276 |
+
],
|
| 277 |
+
"description": "If malicious, classify the type. Use 'none' when code is safe."
|
| 278 |
+
},
|
| 279 |
+
"explanation": {
|
| 280 |
+
"type": "string",
|
| 281 |
+
"description": "A short, safe explanation of why the code is considered malicious or not, without including harmful details."
|
| 282 |
+
},
|
| 283 |
+
"answer": {
|
| 284 |
+
"type": "string",
|
| 285 |
+
"description": "The final answer, taking all reasoning steps into account."
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"required": [
|
| 289 |
+
"reasoning_steps",
|
| 290 |
+
"is_malicious",
|
| 291 |
+
"malicious_type",
|
| 292 |
+
"explanation",
|
| 293 |
+
"answer"
|
| 294 |
+
],
|
| 295 |
+
"additionalProperties": False
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
messages=[
|
| 300 |
+
{
|
| 301 |
+
"role": "user",
|
| 302 |
+
"content": prompt
|
| 303 |
+
}
|
| 304 |
+
]
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# Parse response
|
| 308 |
+
response_content = response.choices[0].message.content
|
| 309 |
+
scan_data = json.loads(response_content)
|
| 310 |
+
|
| 311 |
+
# Map to our format
|
| 312 |
+
severity = _map_severity(scan_data["malicious_type"])
|
| 313 |
+
is_safe = not scan_data["is_malicious"]
|
| 314 |
+
|
| 315 |
+
# Determine recommendation
|
| 316 |
+
if severity in ["critical", "high"]:
|
| 317 |
+
recommendation = "Block deployment"
|
| 318 |
+
elif severity in ["medium", "low"]:
|
| 319 |
+
recommendation = "Warn and allow"
|
| 320 |
+
else:
|
| 321 |
+
recommendation = "Allow"
|
| 322 |
+
|
| 323 |
+
# Build issues list
|
| 324 |
+
issues = []
|
| 325 |
+
if scan_data["is_malicious"]:
|
| 326 |
+
issues.append({
|
| 327 |
+
"type": scan_data["malicious_type"],
|
| 328 |
+
"severity": severity,
|
| 329 |
+
"description": scan_data["explanation"]
|
| 330 |
+
})
|
| 331 |
+
|
| 332 |
+
result = {
|
| 333 |
+
"scan_completed": True,
|
| 334 |
+
"is_safe": is_safe,
|
| 335 |
+
"severity": severity,
|
| 336 |
+
"malicious_type": scan_data["malicious_type"],
|
| 337 |
+
"explanation": scan_data["explanation"],
|
| 338 |
+
"reasoning_steps": scan_data["reasoning_steps"],
|
| 339 |
+
"issues": issues,
|
| 340 |
+
"recommendation": recommendation,
|
| 341 |
+
"scanned_at": datetime.now().isoformat(),
|
| 342 |
+
"cached": False,
|
| 343 |
+
"raw_answer": scan_data.get("answer", "")
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
# Cache the result
|
| 347 |
+
_cache_scan_result(code_hash, result)
|
| 348 |
+
|
| 349 |
+
return result
|
| 350 |
+
|
| 351 |
+
except Exception as e:
|
| 352 |
+
# On error, fall back to warning mode (allow deployment with warning)
|
| 353 |
+
error_msg = str(e)
|
| 354 |
+
|
| 355 |
+
return {
|
| 356 |
+
"scan_completed": False,
|
| 357 |
+
"is_safe": True, # Allow on error
|
| 358 |
+
"severity": "safe",
|
| 359 |
+
"malicious_type": "none",
|
| 360 |
+
"explanation": f"Security scan failed: {error_msg}",
|
| 361 |
+
"reasoning_steps": [f"Error during scan: {error_msg}"],
|
| 362 |
+
"issues": [],
|
| 363 |
+
"recommendation": "Warn (scan failed)",
|
| 364 |
+
"scanned_at": datetime.now().isoformat(),
|
| 365 |
+
"cached": False,
|
| 366 |
+
"error": error_msg
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def clear_scan_cache():
|
| 371 |
+
"""Clear the security scan cache (useful for testing)"""
|
| 372 |
+
_scan_cache.clear()
|
| 373 |
+
_cache_expiry.clear()
|
utils/simple_tracking.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Automatic Usage Tracking - Wraps @mcp.tool() decorator to track all tool calls
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
SIMPLE_TRACKING_CODE = '''
|
| 6 |
+
# ============================================================================
|
| 7 |
+
# AUTOMATIC USAGE TRACKING
|
| 8 |
+
# ============================================================================
|
| 9 |
+
import os
|
| 10 |
+
import requests
|
| 11 |
+
import time
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from functools import wraps
|
| 14 |
+
|
| 15 |
+
WEBHOOK_URL = os.getenv('MCP_WEBHOOK_URL', '')
|
| 16 |
+
DEPLOYMENT_ID = os.getenv('MCP_DEPLOYMENT_ID', 'unknown')
|
| 17 |
+
|
| 18 |
+
def _send_tracking(tool_name, duration_ms, success, error=None):
|
| 19 |
+
"""Send tracking data to webhook endpoint"""
|
| 20 |
+
if not WEBHOOK_URL:
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
requests.post(WEBHOOK_URL, json={
|
| 25 |
+
'deployment_id': DEPLOYMENT_ID,
|
| 26 |
+
'tool_name': tool_name,
|
| 27 |
+
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
| 28 |
+
'duration_ms': duration_ms,
|
| 29 |
+
'success': success,
|
| 30 |
+
'error': error
|
| 31 |
+
}, timeout=2)
|
| 32 |
+
except:
|
| 33 |
+
pass # Silent failure (fix Later)
|
| 34 |
+
|
| 35 |
+
# Save the original mcp.tool decorator
|
| 36 |
+
_original_tool_decorator = mcp.tool
|
| 37 |
+
|
| 38 |
+
def _tracking_tool_decorator(*dec_args, **dec_kwargs):
|
| 39 |
+
"""Wraps @mcp.tool() to automatically track all tool calls"""
|
| 40 |
+
|
| 41 |
+
def decorator(func):
|
| 42 |
+
@wraps(func)
|
| 43 |
+
def wrapper(*args, **kwargs):
|
| 44 |
+
start_time = time.time()
|
| 45 |
+
success = True
|
| 46 |
+
error_msg = None
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
result = func(*args, **kwargs)
|
| 50 |
+
return result
|
| 51 |
+
except Exception as e:
|
| 52 |
+
success = False
|
| 53 |
+
error_msg = str(e)
|
| 54 |
+
raise
|
| 55 |
+
finally:
|
| 56 |
+
duration_ms = int((time.time() - start_time) * 1000)
|
| 57 |
+
_send_tracking(func.__name__, duration_ms, success, error_msg)
|
| 58 |
+
|
| 59 |
+
# Apply the original FastMCP decorator to our tracking wrapper
|
| 60 |
+
return _original_tool_decorator(*dec_args, **dec_kwargs)(wrapper)
|
| 61 |
+
|
| 62 |
+
return decorator
|
| 63 |
+
|
| 64 |
+
# Replace mcp.tool with our tracking version
|
| 65 |
+
mcp.tool = _tracking_tool_decorator
|
| 66 |
+
|
| 67 |
+
if WEBHOOK_URL:
|
| 68 |
+
print(f"✅ Tracking enabled: {WEBHOOK_URL}")
|
| 69 |
+
print(f"📍 Deployment ID: {DEPLOYMENT_ID}")
|
| 70 |
+
else:
|
| 71 |
+
print("⚠️ No webhook URL - tracking disabled")
|
| 72 |
+
'''
|
| 73 |
+
|
| 74 |
+
def get_tracking_code():
|
| 75 |
+
return SIMPLE_TRACKING_CODE
|
utils/usage_tracker.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Usage Tracking for MCP Server
|
| 3 |
+
|
| 4 |
+
Provides decorators and utilities for tracking deployment usage statistics.
|
| 5 |
+
Tracks request counts, response times, tool usage, and client information.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import time
|
| 9 |
+
import functools
|
| 10 |
+
from typing import Optional, Callable, Any, Dict
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from sqlalchemy.orm import Session
|
| 14 |
+
|
| 15 |
+
from .database import get_db, db_transaction
|
| 16 |
+
from .models import UsageEvent, Deployment
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# Usage Tracking Decorator
|
| 21 |
+
# ============================================================================
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def track_usage(
|
| 25 |
+
deployment_id: Optional[str] = None,
|
| 26 |
+
tool_name: Optional[str] = None,
|
| 27 |
+
client_id_getter: Optional[Callable] = None,
|
| 28 |
+
):
|
| 29 |
+
"""
|
| 30 |
+
Decorator to track usage of MCP server functions.
|
| 31 |
+
|
| 32 |
+
Automatically records:
|
| 33 |
+
- Execution time
|
| 34 |
+
- Success/failure status
|
| 35 |
+
- Tool name
|
| 36 |
+
- Client identifier
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
deployment_id: Deployment ID (can be None if extracted from function args)
|
| 40 |
+
tool_name: Name of the tool/function being tracked
|
| 41 |
+
client_id_getter: Optional function to extract client ID from request
|
| 42 |
+
|
| 43 |
+
Example:
|
| 44 |
+
>>> @track_usage(tool_name="get_cat_facts")
|
| 45 |
+
>>> def get_cat_facts(deployment_id: str, count: int = 5):
|
| 46 |
+
>>> # Function implementation
|
| 47 |
+
>>> pass
|
| 48 |
+
|
| 49 |
+
>>> @track_usage(
|
| 50 |
+
>>> tool_name="custom_tool",
|
| 51 |
+
>>> client_id_getter=lambda req: req.headers.get("X-Client-ID")
|
| 52 |
+
>>> )
|
| 53 |
+
>>> def custom_tool(request, deployment_id: str):
|
| 54 |
+
>>> # Function implementation
|
| 55 |
+
>>> pass
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def decorator(func: Callable) -> Callable:
|
| 59 |
+
@functools.wraps(func)
|
| 60 |
+
def wrapper(*args, **kwargs):
|
| 61 |
+
# Extract deployment_id from arguments if not provided
|
| 62 |
+
dep_id = deployment_id
|
| 63 |
+
if dep_id is None:
|
| 64 |
+
# Try to get from kwargs
|
| 65 |
+
dep_id = kwargs.get("deployment_id")
|
| 66 |
+
# Try to get from first positional arg if it's a string
|
| 67 |
+
if dep_id is None and args and isinstance(args[0], str):
|
| 68 |
+
dep_id = args[0]
|
| 69 |
+
|
| 70 |
+
# Extract client_id if getter provided
|
| 71 |
+
client_id = None
|
| 72 |
+
if client_id_getter:
|
| 73 |
+
try:
|
| 74 |
+
# Try to get client_id from args/kwargs
|
| 75 |
+
if args:
|
| 76 |
+
client_id = client_id_getter(args[0])
|
| 77 |
+
elif kwargs:
|
| 78 |
+
client_id = client_id_getter(kwargs)
|
| 79 |
+
except Exception:
|
| 80 |
+
client_id = None
|
| 81 |
+
|
| 82 |
+
# Start timing
|
| 83 |
+
start_time = time.time()
|
| 84 |
+
success = True
|
| 85 |
+
error_msg = None
|
| 86 |
+
result = None
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
# Execute the function
|
| 90 |
+
result = func(*args, **kwargs)
|
| 91 |
+
return result
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
success = False
|
| 95 |
+
error_msg = str(e)
|
| 96 |
+
raise
|
| 97 |
+
|
| 98 |
+
finally:
|
| 99 |
+
# Calculate duration
|
| 100 |
+
duration_ms = int((time.time() - start_time) * 1000)
|
| 101 |
+
|
| 102 |
+
# Record usage asynchronously (non-blocking)
|
| 103 |
+
if dep_id:
|
| 104 |
+
try:
|
| 105 |
+
record_usage_event(
|
| 106 |
+
deployment_id=dep_id,
|
| 107 |
+
tool_name=tool_name or func.__name__,
|
| 108 |
+
client_id=client_id,
|
| 109 |
+
duration_ms=duration_ms,
|
| 110 |
+
success=success,
|
| 111 |
+
error_message=error_msg,
|
| 112 |
+
)
|
| 113 |
+
except Exception as tracking_error:
|
| 114 |
+
# Don't let tracking errors affect the main function
|
| 115 |
+
print(f"Warning: Failed to record usage: {tracking_error}")
|
| 116 |
+
|
| 117 |
+
return wrapper
|
| 118 |
+
|
| 119 |
+
return decorator
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ============================================================================
|
| 123 |
+
# Usage Recording Functions
|
| 124 |
+
# ============================================================================
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def record_usage_event(
|
| 128 |
+
deployment_id: str,
|
| 129 |
+
tool_name: Optional[str] = None,
|
| 130 |
+
client_id: Optional[str] = None,
|
| 131 |
+
duration_ms: Optional[int] = None,
|
| 132 |
+
success: bool = True,
|
| 133 |
+
error_message: Optional[str] = None,
|
| 134 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 135 |
+
) -> bool:
|
| 136 |
+
"""
|
| 137 |
+
Record a usage event in the database.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
deployment_id: Deployment identifier
|
| 141 |
+
tool_name: Name of tool/function called
|
| 142 |
+
client_id: Client identifier
|
| 143 |
+
duration_ms: Request duration in milliseconds
|
| 144 |
+
success: Whether request succeeded
|
| 145 |
+
error_message: Error message if failed
|
| 146 |
+
metadata: Additional metadata
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
bool: True if recorded successfully, False otherwise
|
| 150 |
+
|
| 151 |
+
Example:
|
| 152 |
+
>>> record_usage_event(
|
| 153 |
+
>>> deployment_id="deploy-mcp-example-123456",
|
| 154 |
+
>>> tool_name="get_cat_facts",
|
| 155 |
+
>>> duration_ms=150,
|
| 156 |
+
>>> success=True
|
| 157 |
+
>>> )
|
| 158 |
+
"""
|
| 159 |
+
try:
|
| 160 |
+
with db_transaction() as db:
|
| 161 |
+
UsageEvent.record_usage(
|
| 162 |
+
db=db,
|
| 163 |
+
deployment_id=deployment_id,
|
| 164 |
+
tool_name=tool_name,
|
| 165 |
+
client_id=client_id,
|
| 166 |
+
duration_ms=duration_ms,
|
| 167 |
+
success=success,
|
| 168 |
+
error_message=error_message,
|
| 169 |
+
metadata=metadata,
|
| 170 |
+
)
|
| 171 |
+
return True
|
| 172 |
+
except Exception as e:
|
| 173 |
+
print(f"Error recording usage event: {e}")
|
| 174 |
+
return False
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def increment_deployment_counter(deployment_id: str, duration_ms: Optional[int] = None):
|
| 178 |
+
"""
|
| 179 |
+
Increment deployment usage counter and update statistics.
|
| 180 |
+
|
| 181 |
+
This is a lightweight alternative to recording full events.
|
| 182 |
+
Updates total_requests, last_used_at, and avg_response_time_ms.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
deployment_id: Deployment identifier
|
| 186 |
+
duration_ms: Optional response time to update average
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
bool: True if updated successfully, False otherwise
|
| 190 |
+
|
| 191 |
+
Example:
|
| 192 |
+
>>> increment_deployment_counter("deploy-mcp-example-123456", 150)
|
| 193 |
+
"""
|
| 194 |
+
try:
|
| 195 |
+
with db_transaction() as db:
|
| 196 |
+
deployment = Deployment.get_by_deployment_id(db, deployment_id)
|
| 197 |
+
if deployment:
|
| 198 |
+
if duration_ms is not None:
|
| 199 |
+
deployment.update_usage_stats(duration_ms)
|
| 200 |
+
else:
|
| 201 |
+
deployment.total_requests += 1
|
| 202 |
+
deployment.last_used_at = datetime.utcnow()
|
| 203 |
+
return True
|
| 204 |
+
except Exception as e:
|
| 205 |
+
print(f"Error incrementing deployment counter: {e}")
|
| 206 |
+
return False
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# ============================================================================
|
| 210 |
+
# Statistics Retrieval
|
| 211 |
+
# ============================================================================
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def get_deployment_statistics(
|
| 215 |
+
deployment_id: str,
|
| 216 |
+
days: int = 30,
|
| 217 |
+
) -> Optional[Dict[str, Any]]:
|
| 218 |
+
"""
|
| 219 |
+
Get usage statistics for a deployment.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
deployment_id: Deployment identifier
|
| 223 |
+
days: Number of days to look back
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
dict: Usage statistics or None if error
|
| 227 |
+
|
| 228 |
+
Example:
|
| 229 |
+
>>> stats = get_deployment_statistics("deploy-mcp-example-123456", days=7)
|
| 230 |
+
>>> print(f"Total requests: {stats['total_requests']}")
|
| 231 |
+
>>> print(f"Success rate: {stats['success_rate_percent']}%")
|
| 232 |
+
"""
|
| 233 |
+
try:
|
| 234 |
+
with get_db() as db:
|
| 235 |
+
stats = UsageEvent.get_stats(db, deployment_id, days)
|
| 236 |
+
return stats
|
| 237 |
+
except Exception as e:
|
| 238 |
+
print(f"Error getting deployment statistics: {e}")
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def get_tool_usage_breakdown(
|
| 243 |
+
deployment_id: str,
|
| 244 |
+
days: int = 30,
|
| 245 |
+
limit: int = 10,
|
| 246 |
+
) -> Optional[list]:
|
| 247 |
+
"""
|
| 248 |
+
Get breakdown of tool usage for a deployment.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
deployment_id: Deployment identifier
|
| 252 |
+
days: Number of days to look back
|
| 253 |
+
limit: Maximum number of tools to return
|
| 254 |
+
|
| 255 |
+
Returns:
|
| 256 |
+
list: List of dicts with tool_name and count
|
| 257 |
+
|
| 258 |
+
Example:
|
| 259 |
+
>>> tools = get_tool_usage_breakdown("deploy-mcp-example-123456")
|
| 260 |
+
>>> for tool in tools:
|
| 261 |
+
>>> print(f"{tool['tool_name']}: {tool['count']} requests")
|
| 262 |
+
"""
|
| 263 |
+
try:
|
| 264 |
+
from sqlalchemy import and_, func
|
| 265 |
+
from datetime import datetime, timedelta
|
| 266 |
+
|
| 267 |
+
with get_db() as db:
|
| 268 |
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
| 269 |
+
|
| 270 |
+
tool_stats = (
|
| 271 |
+
db.query(
|
| 272 |
+
UsageEvent.tool_name,
|
| 273 |
+
func.count(UsageEvent.id).label("count"),
|
| 274 |
+
)
|
| 275 |
+
.filter(
|
| 276 |
+
and_(
|
| 277 |
+
UsageEvent.deployment_id == deployment_id,
|
| 278 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 279 |
+
UsageEvent.tool_name.isnot(None),
|
| 280 |
+
)
|
| 281 |
+
)
|
| 282 |
+
.group_by(UsageEvent.tool_name)
|
| 283 |
+
.order_by(func.count(UsageEvent.id).desc())
|
| 284 |
+
.limit(limit)
|
| 285 |
+
.all()
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
return [
|
| 289 |
+
{"tool_name": tool, "count": count}
|
| 290 |
+
for tool, count in tool_stats
|
| 291 |
+
]
|
| 292 |
+
except Exception as e:
|
| 293 |
+
print(f"Error getting tool usage breakdown: {e}")
|
| 294 |
+
return None
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def get_usage_timeline(
|
| 298 |
+
deployment_id: str,
|
| 299 |
+
days: int = 7,
|
| 300 |
+
granularity: str = "day",
|
| 301 |
+
) -> Optional[list]:
|
| 302 |
+
"""
|
| 303 |
+
Get usage timeline for a deployment.
|
| 304 |
+
|
| 305 |
+
Args:
|
| 306 |
+
deployment_id: Deployment identifier
|
| 307 |
+
days: Number of days to look back
|
| 308 |
+
granularity: 'hour' or 'day'
|
| 309 |
+
|
| 310 |
+
Returns:
|
| 311 |
+
list: List of dicts with timestamp and count
|
| 312 |
+
|
| 313 |
+
Example:
|
| 314 |
+
>>> timeline = get_usage_timeline("deploy-mcp-example-123456", days=7)
|
| 315 |
+
>>> for entry in timeline:
|
| 316 |
+
>>> print(f"{entry['date']}: {entry['requests']} requests")
|
| 317 |
+
"""
|
| 318 |
+
try:
|
| 319 |
+
from sqlalchemy import and_, func
|
| 320 |
+
from datetime import datetime, timedelta
|
| 321 |
+
|
| 322 |
+
with get_db() as db:
|
| 323 |
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
| 324 |
+
|
| 325 |
+
# Choose date truncation based on granularity
|
| 326 |
+
if granularity == "hour":
|
| 327 |
+
time_bucket = func.date_trunc("hour", UsageEvent.timestamp)
|
| 328 |
+
else:
|
| 329 |
+
time_bucket = func.date_trunc("day", UsageEvent.timestamp)
|
| 330 |
+
|
| 331 |
+
timeline_data = (
|
| 332 |
+
db.query(
|
| 333 |
+
time_bucket.label("time_bucket"),
|
| 334 |
+
func.count(UsageEvent.id).label("count"),
|
| 335 |
+
)
|
| 336 |
+
.filter(
|
| 337 |
+
and_(
|
| 338 |
+
UsageEvent.deployment_id == deployment_id,
|
| 339 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 340 |
+
)
|
| 341 |
+
)
|
| 342 |
+
.group_by(time_bucket)
|
| 343 |
+
.order_by(time_bucket)
|
| 344 |
+
.all()
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
return [
|
| 348 |
+
{
|
| 349 |
+
"timestamp": bucket.isoformat() if bucket else None,
|
| 350 |
+
"requests": count,
|
| 351 |
+
}
|
| 352 |
+
for bucket, count in timeline_data
|
| 353 |
+
]
|
| 354 |
+
except Exception as e:
|
| 355 |
+
print(f"Error getting usage timeline: {e}")
|
| 356 |
+
return None
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def get_client_statistics(
|
| 360 |
+
deployment_id: str,
|
| 361 |
+
days: int = 30,
|
| 362 |
+
limit: int = 10,
|
| 363 |
+
) -> Optional[list]:
|
| 364 |
+
"""
|
| 365 |
+
Get client usage statistics for a deployment.
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
deployment_id: Deployment identifier
|
| 369 |
+
days: Number of days to look back
|
| 370 |
+
limit: Maximum number of clients to return
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
list: List of dicts with client_id and count
|
| 374 |
+
|
| 375 |
+
Example:
|
| 376 |
+
>>> clients = get_client_statistics("deploy-mcp-example-123456")
|
| 377 |
+
>>> for client in clients:
|
| 378 |
+
>>> print(f"Client {client['client_id']}: {client['count']} requests")
|
| 379 |
+
"""
|
| 380 |
+
try:
|
| 381 |
+
from sqlalchemy import and_, func
|
| 382 |
+
from datetime import datetime, timedelta
|
| 383 |
+
|
| 384 |
+
with get_db() as db:
|
| 385 |
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
| 386 |
+
|
| 387 |
+
client_stats = (
|
| 388 |
+
db.query(
|
| 389 |
+
UsageEvent.client_id,
|
| 390 |
+
func.count(UsageEvent.id).label("count"),
|
| 391 |
+
)
|
| 392 |
+
.filter(
|
| 393 |
+
and_(
|
| 394 |
+
UsageEvent.deployment_id == deployment_id,
|
| 395 |
+
UsageEvent.timestamp >= cutoff_date,
|
| 396 |
+
UsageEvent.client_id.isnot(None),
|
| 397 |
+
)
|
| 398 |
+
)
|
| 399 |
+
.group_by(UsageEvent.client_id)
|
| 400 |
+
.order_by(func.count(UsageEvent.id).desc())
|
| 401 |
+
.limit(limit)
|
| 402 |
+
.all()
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
return [
|
| 406 |
+
{"client_id": client, "count": count}
|
| 407 |
+
for client, count in client_stats
|
| 408 |
+
]
|
| 409 |
+
except Exception as e:
|
| 410 |
+
print(f"Error getting client statistics: {e}")
|
| 411 |
+
return None
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
# ============================================================================
|
| 415 |
+
# Utility Functions
|
| 416 |
+
# ============================================================================
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def get_all_deployments_stats() -> Optional[list]:
|
| 420 |
+
"""
|
| 421 |
+
Get quick statistics for all active deployments.
|
| 422 |
+
|
| 423 |
+
Returns:
|
| 424 |
+
list: List of dicts with deployment info and stats
|
| 425 |
+
|
| 426 |
+
Example:
|
| 427 |
+
>>> all_stats = get_all_deployments_stats()
|
| 428 |
+
>>> for deployment in all_stats:
|
| 429 |
+
>>> print(f"{deployment['server_name']}: {deployment['total_requests']} requests")
|
| 430 |
+
"""
|
| 431 |
+
try:
|
| 432 |
+
with get_db() as db:
|
| 433 |
+
deployments = Deployment.get_active_deployments(db)
|
| 434 |
+
return [
|
| 435 |
+
{
|
| 436 |
+
"deployment_id": dep.deployment_id,
|
| 437 |
+
"server_name": dep.server_name,
|
| 438 |
+
"total_requests": dep.total_requests or 0,
|
| 439 |
+
"last_used_at": dep.last_used_at.isoformat() if dep.last_used_at else None,
|
| 440 |
+
"avg_response_time_ms": dep.avg_response_time_ms,
|
| 441 |
+
"status": dep.status,
|
| 442 |
+
}
|
| 443 |
+
for dep in deployments
|
| 444 |
+
]
|
| 445 |
+
except Exception as e:
|
| 446 |
+
print(f"Error getting all deployments stats: {e}")
|
| 447 |
+
return None
|
utils/webhook_receiver.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple Webhook Configuration for MCP Usage Tracking
|
| 3 |
+
|
| 4 |
+
Provides basic configuration functions for the webhook endpoint.
|
| 5 |
+
The actual webhook endpoint is defined in app.py.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# Load environment variables
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_webhook_url() -> str:
|
| 16 |
+
"""Get the configured webhook URL"""
|
| 17 |
+
base_url = os.getenv('MCP_BASE_URL', 'http://localhost:7860')
|
| 18 |
+
return f"{base_url}/api/webhook/usage"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def is_webhook_enabled() -> bool:
|
| 22 |
+
"""Check if webhook endpoint is enabled"""
|
| 23 |
+
return os.getenv('MCP_WEBHOOK_ENABLED', 'true').lower() == 'true'
|