From f653bd3079f316a370371e5ec78955dc7412afee Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 19:14:42 +0000 Subject: [PATCH] Add production deployment guide and automation for MCP servers This commit adds comprehensive documentation and tooling for deploying MCP servers to production environments using systemd, nginx, and Let's Encrypt SSL. ## Overview Production deployment runs MCP servers directly on VMs with systemd service management, nginx reverse proxy, and automated deployment workflows. ## Changes **Documentation:** - `docs/DEPLOYMENT.md` - Generic production deployment guide - Covers systemd setup, nginx configuration, SSL certificates - Includes concrete pizzaz example as implementation reference **Deployment Automation:** - `docs/scripts/deploy-example.sh` - Automated deployment script template - Handles building, syncing, dependency installation, service restart - Health checks and verification **Setup Files:** - `docs/setup/example-mcp.service` - Systemd service template for MCP server - `docs/setup/example-assets.service` - Systemd service for assets (optional) - `docs/setup/example-nginx-production.conf` - Production nginx with SSE ## Key Features - **Generic approach**: Works with any MCP server (Node.js, Python, etc.) - **Systemd integration**: Auto-restart on failure, logging to journalctl - **nginx optimization**: SSE-specific settings for MCP protocol - **Automated deployment**: One-command deployment with verification - **Security**: HTTPS with Let's Encrypt, firewall guidelines - **Monitoring**: Service status checks, log viewing commands - **Pizzaz example**: Concrete two-server architecture implementation ## Production Features - Service management with systemd - Automatic restarts on failure - Centralized logging with journalctl - SSL termination with nginx - Health verification after deployment - Rollback instructions - Multi-app support on single VM This enables developers to quickly deploy any MCP server from this repository to production with proper service management, monitoring, and security. --- docs/DEPLOYMENT.md | 388 +++++++++++++++++++++++ docs/scripts/deploy-example.sh | 130 ++++++++ docs/setup/example-assets.service | 48 +++ docs/setup/example-mcp.service | 51 +++ docs/setup/example-nginx-production.conf | 64 ++++ 5 files changed, 681 insertions(+) create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/scripts/deploy-example.sh create mode 100644 docs/setup/example-assets.service create mode 100644 docs/setup/example-mcp.service create mode 100644 docs/setup/example-nginx-production.conf diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..47b520d --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,388 @@ +# MCP Server - Production Deployment Guide + +This guide covers deploying MCP servers from this repository to production. This assumes you're deploying to a VM with systemd services, nginx reverse proxy, and Let's Encrypt SSL. + +> **Note:** This guide is for **production deployment**. For development/testing with local servers, see [`DEVELOPMENT.md`](DEVELOPMENT.md). + +## Overview + +Production deployment differs from development setup: +- **Development**: SSH tunnels from local machine → VM +- **Production**: Code runs directly on VM, managed by systemd + +## Prerequisites + +- VM with Ubuntu (tested on Ubuntu 20.04+) +- Domain name with DNS configured +- SSH access to the VM +- Node.js 18+ and pnpm installed on the VM (or Python 3.10+ for Python servers) +- SSL certificate (Let's Encrypt recommended) + +## Architecture + +``` +Internet → HTTPS:443 → nginx → localhost:MCP_PORT (MCP Server) + → localhost:ASSETS_PORT (Assets, optional) +``` + +**Key Components:** +- **MCP Server**: Implements MCP protocol over SSE +- **Assets Server** (optional): Static file server for widget bundles +- **nginx**: Reverse proxy with SSL termination and SSE optimization +- **systemd**: Service management with auto-restart on failure + +## Initial Setup (One-time) + +### 1. DNS Configuration + +Point your domain to your VM: +``` +Type: A +Name: (e.g., mcp or your-app-name) +Value: +TTL: 3600 (or default) +``` + +### 2. Firewall Configuration + +Ensure ports are open: +- **22** (SSH) - For deployment access +- **80** (HTTP) - For Let's Encrypt validation +- **443** (HTTPS) - For ChatGPT/user access + +Check both VM firewall (`ufw`, `iptables`) and cloud provider firewalls. + +### 3. SSL Certificate + +Obtain Let's Encrypt certificate: +```bash +ssh @ +sudo apt update && sudo apt install -y certbot python3-certbot-nginx +sudo certbot --nginx -d +``` + +### 4. nginx Configuration + +See [`docs/setup/example-nginx-production.conf`](setup/example-nginx-production.conf) for a template. + +Key requirements: +- Proxy `/mcp` → your MCP server port +- Disable buffering for SSE support +- Optional: proxy `/` → assets server port +- SSL configuration with Let's Encrypt certificates + +Copy config to nginx: +```bash +sudo cp your-config.conf /etc/nginx/sites-available/ +sudo ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +### 5. Application Directory + +Create deployment directory on VM: +```bash +ssh @ +sudo mkdir -p /opt/ +sudo chown : /opt/ +``` + +## Deployment Process + +### Option 1: Using Deployment Script (Recommended) + +See [`docs/scripts/deploy-example.sh`](scripts/deploy-example.sh) for a template deployment script. + +The script should: +1. Build assets locally (if applicable) +2. Sync code to VM (rsync) +3. Install dependencies on VM +4. Update systemd service files +5. Restart services +6. Verify health + +Customize and run: +```bash +./scripts/deploy-your-app.sh +``` + +### Option 2: Manual Deployment + +```bash +# 1. Build assets locally (if applicable) +BASE_URL=https:// pnpm run build + +# 2. Sync code to VM +rsync -avz --delete \ + --exclude 'node_modules' \ + --exclude '.git' \ + --exclude '.env' \ + ./ @:/opt// + +# 3. Install dependencies on VM +ssh @ "cd /opt/ && pnpm install --prod" + +# 4. Copy systemd service files +scp setup/-mcp.service @:/tmp/ +ssh @ " + sudo cp /tmp/-mcp.service /etc/systemd/system/ && + sudo systemctl daemon-reload && + sudo systemctl enable -mcp.service && + sudo systemctl restart -mcp.service +" + +# 5. Verify +curl https:///mcp +``` + +## Service Management + +### Check Service Status + +```bash +ssh @ 'sudo systemctl status -mcp.service' +``` + +### View Logs + +```bash +# Follow logs in real-time +ssh @ 'sudo journalctl -u -mcp.service -f' + +# View recent logs +ssh @ 'sudo journalctl -u -mcp.service -n 50 --no-pager' +``` + +### Restart Services + +```bash +ssh @ 'sudo systemctl restart -mcp.service' +``` + +### Stop Services + +```bash +ssh @ 'sudo systemctl stop -mcp.service' +``` + +## systemd Service Files + +See [`docs/setup/example-mcp.service`](setup/example-mcp.service) and [`docs/setup/example-assets.service`](setup/example-assets.service) for templates. + +Key features: +- `Restart=always` - Auto-restart on failure +- `RestartSec=10` - Wait 10s before restart +- `WorkingDirectory` - Set to your app directory +- `Environment` - Set environment variables (BASE_URL, PORT, etc.) +- `StandardOutput=journal` - Logs to journalctl + +## nginx Production Configuration + +Critical SSE settings: +```nginx +location /mcp { + proxy_pass http://127.0.0.1:; + proxy_buffering off; # Critical for SSE + proxy_cache off; + proxy_set_header Connection ''; + chunked_transfer_encoding off; + proxy_http_version 1.1; + proxy_read_timeout 3600s; # Long timeout for SSE +} +``` + +## Testing + +### Run Protocol Tests + +If your MCP server has tests: +```bash +MCP_URL=https:///mcp pnpm exec tsx tests/test-mcp.ts +``` + +### Manual Testing + +```bash +# Test MCP endpoint (should return SSE stream) +curl https:///mcp + +# Test in ChatGPT +# Go to ChatGPT → Settings → Add MCP Server → https:///mcp +``` + +## Troubleshooting + +### Service Won't Start + +Check logs for errors: +```bash +ssh @ 'sudo journalctl -u -mcp.service -n 50' +``` + +Common issues: +- **Port already in use**: Check `sudo lsof -i:` +- **Missing dependencies**: Run `pnpm install` in app directory +- **Missing environment variables**: Check `Environment=` in service file +- **Wrong working directory**: Verify `WorkingDirectory=` in service file + +### nginx 502 Bad Gateway + +- Service not running: Check `sudo systemctl status -mcp.service` +- Wrong port in config: Verify nginx proxy_pass port matches service port +- Firewall blocking: Check VM and cloud provider firewalls + +### SSE Connection Issues + +- nginx buffering: Ensure `proxy_buffering off` in nginx config +- Timeout too short: Increase `proxy_read_timeout` in nginx config +- Check server logs for connection errors + +### Assets Don't Load (Two-Server Architecture) + +- Verify BASE_URL was set during build +- Check assets service is running +- Test asset URL directly: `curl https:///asset.html` + +## Rollback + +If deployment fails: + +```bash +# 1. Check git history on your machine +git log --oneline + +# 2. Checkout previous commit +git checkout + +# 3. Redeploy +./scripts/deploy-your-app.sh +``` + +Or on VM, restart previous version if code still exists: +```bash +ssh @ 'sudo systemctl restart -mcp.service' +``` + +## Port Configuration + +**Choosing ports:** +- Use high ports (8000+) to avoid conflicts +- Check if port is available: `ssh @ 'sudo lsof -i:'` +- Document your port choices in your systemd service files + +## Security Considerations + +**Current setup:** +- ✅ HTTPS with Let's Encrypt SSL +- ✅ Auto-restart on crash (systemd) +- ✅ Logging to journalctl +- ⚠️ No authentication (MCP server is public by default) +- ⚠️ No rate limiting +- ⚠️ No monitoring/alerts + +**Production hardening recommendations:** +- Add rate limiting in nginx +- Implement authentication if handling sensitive data +- Set up monitoring (UptimeRobot, Prometheus, etc.) +- Configure log rotation +- Regular security updates (`apt update && apt upgrade`) +- Backup strategy for application data +- Set up alerts for service failures + +## Monitoring + +### Basic Health Checks + +```bash +# Check if MCP endpoint responds +curl -f https:///mcp || echo "MCP endpoint down" + +# Check service status +ssh @ 'systemctl is-active -mcp.service' +``` + +### Log Monitoring + +Set up log alerts: +```bash +# Watch for errors +ssh @ 'sudo journalctl -u -mcp.service -f | grep -i error' +``` + +## Multiple Apps on Same VM + +To run multiple MCP servers on the same VM: + +1. **Use different ports** for each app +2. **Create separate systemd services** with unique names +3. **Add nginx location blocks** for each app: + ```nginx + server { + server_name app1.example.com; + location /mcp { proxy_pass http://127.0.0.1:8001; } + } + + server { + server_name app2.example.com; + location /mcp { proxy_pass http://127.0.0.1:8002; } + } + ``` +4. **Obtain SSL certificates** for each domain +5. **Deploy to separate directories**: `/opt/app1/`, `/opt/app2/` + +## Files Reference + +- [`docs/setup/example-nginx-production.conf`](setup/example-nginx-production.conf) - Example nginx config +- [`docs/setup/example-mcp.service`](setup/example-mcp.service) - Example systemd service for MCP server +- [`docs/setup/example-assets.service`](setup/example-assets.service) - Example systemd service for assets +- [`docs/scripts/deploy-example.sh`](scripts/deploy-example.sh) - Example deployment script + +--- + +## Appendix: Pizzaz Production Deployment Example + +The **pizzaz** MCP server is deployed at `https://pizzaz.lazzloe.com/mcp` using this architecture. + +### Configuration + +**Domain**: pizzaz.lazzloe.com +**VM**: Ubuntu 20.04 at 46.224.27.7 +**Deploy Directory**: `/opt/pizzaz-mcp/` + +**Ports:** +- MCP Server: 8001 (port 8000 was occupied by another service) +- Assets Server: 4444 + +### Services + +- `pizzaz-mcp.service` - MCP server +- `pizzaz-assets.service` - Static assets server + +### Deployment + +```bash +# Automated deployment +./scripts/deploy-pizzaz.sh + +# Manual check +ssh ubuntu@46.224.27.7 'sudo systemctl status pizzaz-mcp.service' +``` + +### Build Command + +```bash +BASE_URL=https://pizzaz.lazzloe.com pnpm run build +``` + +### Testing + +```bash +MCP_URL=https://pizzaz.lazzloe.com/mcp pnpm exec tsx tests/test-mcp.ts +``` + +### Files + +- `scripts/deploy-pizzaz.sh` - Deployment automation +- `setup/pizzaz-mcp.service` - MCP service config +- `setup/pizzaz-assets.service` - Assets service config +- `setup/nginx-pizzaz-production.conf` - nginx configuration diff --git a/docs/scripts/deploy-example.sh b/docs/scripts/deploy-example.sh new file mode 100644 index 0000000..b43822a --- /dev/null +++ b/docs/scripts/deploy-example.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Example deployment script for MCP server to production +# +# Customize the following variables for your app: +# - VM_HOST: SSH connection string (user@ip or user@hostname) +# - DEPLOY_DIR: Directory on VM where app will be deployed +# - BASE_URL: Your production domain URL (if building assets) +# - PNPM or NODE: Path to pnpm/node on VM (if using Node.js) +# - SERVICE_NAMES: Your systemd service names +# - MCP_PORT: Port your MCP server runs on +# - NGINX_CONF: Your nginx config filename +# +# Usage: ./deploy-example.sh + +set -e + +# ============ CONFIGURATION - CUSTOMIZE THESE ============ +VM_HOST="@" # e.g., ubuntu@192.168.1.100 +DEPLOY_DIR="/opt/" # e.g., /opt/my-mcp-server +BASE_URL="https://" # e.g., https://mcp.example.com +PNPM="/path/to/pnpm" # e.g., /home/ubuntu/.local/share/pnpm/pnpm +MCP_SERVICE="-mcp.service" # e.g., my-app-mcp.service +ASSETS_SERVICE="-assets.service" # Optional, remove if single-server +MCP_PORT="" # e.g., 8001 +NGINX_CONF="" # e.g., example-nginx-production.conf +NGINX_SITE="" # e.g., mcp.example.com +# ========================================================= + +echo "🚀 Starting deployment to production..." + +# Step 1: Build assets locally (if applicable) +echo "" +echo "📦 Building assets with BASE_URL=$BASE_URL..." +# Customize this for your build process: +# - Node.js with widgets: BASE_URL=$BASE_URL pnpm run build +# - Python: might not need this step +# - Static only: skip this step +BASE_URL=$BASE_URL pnpm run build + +# Step 2: Create deployment directory on VM +echo "" +echo "📁 Creating deployment directory on VM..." +ssh $VM_HOST "sudo mkdir -p $DEPLOY_DIR && sudo chown $(whoami):$(whoami) $DEPLOY_DIR" + +# Step 3: Copy files to VM +echo "" +echo "📤 Copying files to VM..." +rsync -avz --delete \ + --exclude 'node_modules' \ + --exclude '.git' \ + --exclude 'dist' \ + --exclude '.next' \ + --exclude '__pycache__' \ + --exclude '.env' \ + --exclude '.venv' \ + ./ $VM_HOST:$DEPLOY_DIR/ + +# Step 4: Install dependencies on VM +echo "" +echo "📥 Installing dependencies on VM..." +# Customize for your stack: +# - Node.js with pnpm: ssh $VM_HOST "cd $DEPLOY_DIR && $PNPM install --prod" +# - Node.js with npm: ssh $VM_HOST "cd $DEPLOY_DIR && npm install --production" +# - Python: ssh $VM_HOST "cd $DEPLOY_DIR && python3 -m pip install -r requirements.txt" +ssh $VM_HOST "cd $DEPLOY_DIR && $PNPM install --prod" + +# If your MCP server is in a subdirectory: +# ssh $VM_HOST "cd $DEPLOY_DIR/ && $PNPM install --prod" + +# Step 5: Copy and enable systemd services +echo "" +echo "⚙️ Setting up systemd services..." +ssh $VM_HOST "sudo cp $DEPLOY_DIR/setup/$MCP_SERVICE /etc/systemd/system/" +# If using assets service (two-server architecture): +ssh $VM_HOST "sudo cp $DEPLOY_DIR/setup/$ASSETS_SERVICE /etc/systemd/system/" + +ssh $VM_HOST "sudo systemctl daemon-reload" +ssh $VM_HOST "sudo systemctl enable $MCP_SERVICE" +# If using assets service: +ssh $VM_HOST "sudo systemctl enable $ASSETS_SERVICE" + +# Step 6: Update nginx configuration +echo "" +echo "🌐 Updating nginx configuration..." +ssh $VM_HOST "sudo cp $DEPLOY_DIR/setup/$NGINX_CONF /etc/nginx/sites-available/$NGINX_SITE" +ssh $VM_HOST "sudo nginx -t" + +# Step 7: Stop dev tunnel service if switching from dev to prod +echo "" +echo "🛑 Stopping dev tunnel service (if exists)..." +ssh $VM_HOST "sudo systemctl stop -tunnel.service 2>/dev/null || true" +ssh $VM_HOST "sudo systemctl disable -tunnel.service 2>/dev/null || true" + +# Step 8: Restart services +echo "" +echo "🔄 Restarting services..." +ssh $VM_HOST "sudo systemctl restart $MCP_SERVICE" +# If using assets service: +ssh $VM_HOST "sudo systemctl restart $ASSETS_SERVICE" +ssh $VM_HOST "sudo systemctl reload nginx" + +# Step 9: Check service status +echo "" +echo "✅ Checking service status..." +ssh $VM_HOST "sudo systemctl status $MCP_SERVICE --no-pager | head -10" +# If using assets service: +ssh $VM_HOST "sudo systemctl status $ASSETS_SERVICE --no-pager | head -10" + +# Step 10: Wait and verify health +echo "" +echo "🔍 Verifying deployment..." +sleep 5 +HTTP_CODE=$(ssh $VM_HOST "curl -s -o /dev/null -w '%{http_code}' http://localhost:$MCP_PORT/mcp") + +if [ "$HTTP_CODE" = "200" ]; then + echo "✅ MCP server responding correctly (HTTP $HTTP_CODE)" +else + echo "❌ MCP server not responding correctly (HTTP $HTTP_CODE)" + echo "Check logs with: ssh $VM_HOST sudo journalctl -u $MCP_SERVICE -n 50" + exit 1 +fi + +echo "" +echo "🎉 Deployment complete!" +echo "🌐 MCP endpoint: $BASE_URL/mcp" +echo "" +echo "Useful commands:" +echo " View MCP logs: ssh $VM_HOST sudo journalctl -u $MCP_SERVICE -f" +echo " Restart service: ssh $VM_HOST sudo systemctl restart $MCP_SERVICE" +echo " Check status: ssh $VM_HOST sudo systemctl status $MCP_SERVICE" diff --git a/docs/setup/example-assets.service b/docs/setup/example-assets.service new file mode 100644 index 0000000..e31a00b --- /dev/null +++ b/docs/setup/example-assets.service @@ -0,0 +1,48 @@ +# Example systemd service for assets server (production) +# This runs your static assets server as a systemd service with auto-restart +# Only needed if using two-server architecture +# +# Customize the following: +# - - Your app name (used in Description and SyslogIdentifier) +# - - User to run the service as (e.g., ubuntu, youruser) +# - - Path to your assets directory +# - - Path to pnpm executable (if using pnpm) +# - - Command to start your assets server +# +# Installation: +# 1. Customize this file +# 2. Copy to: /etc/systemd/system/-assets.service +# 3. Run: sudo systemctl daemon-reload +# 4. Run: sudo systemctl enable -assets.service +# 5. Run: sudo systemctl start -assets.service + +[Unit] +Description= Assets Server +After=network.target + +[Service] +Type=simple +User= +WorkingDirectory= + +# Adjust PATH to include your runtime +Environment="PATH=:/usr/local/bin:/usr/bin:/bin" + +# Start command for serving static assets +# Examples: +# - pnpm: /path/to/pnpm -w run serve +# - serve: npx serve -l 4444 assets/ +# - Python: python3 -m http.server 4444 +ExecStart= + +# Auto-restart on failure +Restart=always +RestartSec=10 + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=-assets + +[Install] +WantedBy=multi-user.target diff --git a/docs/setup/example-mcp.service b/docs/setup/example-mcp.service new file mode 100644 index 0000000..8bb618d --- /dev/null +++ b/docs/setup/example-mcp.service @@ -0,0 +1,51 @@ +# Example systemd service for MCP server (production) +# This runs your MCP server as a systemd service with auto-restart +# +# Customize the following: +# - - Your app name (used in Description and SyslogIdentifier) +# - - User to run the service as (e.g., ubuntu, youruser) +# - - Path to your MCP server directory +# - or - Path to pnpm/node executable +# - - Command to start your server (e.g., pnpm start, node server.js, python main.py) +# - - Port for your MCP server (optional, if needed) +# - Additional Environment variables as needed +# +# Installation: +# 1. Customize this file +# 2. Copy to: /etc/systemd/system/-mcp.service +# 3. Run: sudo systemctl daemon-reload +# 4. Run: sudo systemctl enable -mcp.service +# 5. Run: sudo systemctl start -mcp.service + +[Unit] +Description= MCP Server +After=network.target + +[Service] +Type=simple +User= +WorkingDirectory= + +# Adjust PATH to include your runtime (Node.js, Python, etc.) +Environment="PATH=:/usr/local/bin:/usr/bin:/bin" + +# Optional: Set port or other environment variables +Environment="PORT=" + +# Start command - customize for your stack +# Node.js with pnpm: /path/to/pnpm start +# Node.js direct: /usr/bin/node server.js +# Python: /usr/bin/python3 main.py +ExecStart= + +# Auto-restart on failure +Restart=always +RestartSec=10 + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=-mcp + +[Install] +WantedBy=multi-user.target diff --git a/docs/setup/example-nginx-production.conf b/docs/setup/example-nginx-production.conf new file mode 100644 index 0000000..48e49bb --- /dev/null +++ b/docs/setup/example-nginx-production.conf @@ -0,0 +1,64 @@ +# Example nginx configuration for MCP server production deployment +# This is a generic template - customize with your values: +# - - Your full domain (e.g., mcp.example.com) +# - - Port where your MCP server runs (e.g., 8001) +# - - Port where your assets server runs (optional, e.g., 4444) + +server { + listen 80; + server_name ; + + # Redirect HTTP to HTTPS + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name ; + + # SSL certificates (Let's Encrypt will populate these) + ssl_certificate /etc/letsencrypt/live//fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live//privkey.pem; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + + # MCP endpoints - proxy to your MCP server + location /mcp { + proxy_pass http://127.0.0.1:; + proxy_http_version 1.1; + + # SSE-specific settings - CRITICAL for Server-Sent Events + proxy_buffering off; + proxy_cache off; + proxy_set_header Connection ''; + chunked_transfer_encoding off; + + # Standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Increase timeouts for long-lived SSE connections + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Static assets - proxy to your assets server (optional, for two-server architecture) + # Comment out this block if using single-server architecture + location / { + proxy_pass http://127.0.0.1:; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Cache static assets + proxy_cache_valid 200 1h; + } +}