Add production deployment for N8N with SSE transport
Production-ready MCP IMAP server for hosted N8N workflows. Changes: - SSE transport using create_sse_app() for N8N compatibility - API key authentication middleware for multi-tenant access - Systemd service file for auto-restart on Hetzner - Nginx reverse proxy config with TLS and rate limiting - Complete deployment guide (DEPLOY.md) - Removed REST API bridge (api_server.py) - N8N uses MCP protocol Deployment: - SSE endpoint: https://imap.maxtheweb.ai/mcp/sse - Messages endpoint: https://imap.maxtheweb.ai/mcp/messages - Authentication: Bearer token in Authorization header Stack: FastMCP 2.13 + uvicorn + nginx + systemd
This commit is contained in:
parent
8232a58600
commit
88fbde4542
3
.gitignore
vendored
3
.gitignore
vendored
@ -47,5 +47,8 @@ build/
|
|||||||
.backend.pid
|
.backend.pid
|
||||||
.frontend.pid
|
.frontend.pid
|
||||||
|
|
||||||
|
# Archived files
|
||||||
|
backend/archived/
|
||||||
|
|
||||||
# Next.js
|
# Next.js
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
@ -3,6 +3,7 @@ MCP_PORT=8000
|
|||||||
MCP_HOST=0.0.0.0
|
MCP_HOST=0.0.0.0
|
||||||
|
|
||||||
# Security Settings
|
# Security Settings
|
||||||
|
MCP_API_KEYS=your_api_key_1,your_api_key_2,your_api_key_3
|
||||||
RATE_LIMIT_PER_MINUTE=60
|
RATE_LIMIT_PER_MINUTE=60
|
||||||
MAX_EMAIL_FETCH_SIZE=100
|
MAX_EMAIL_FETCH_SIZE=100
|
||||||
SESSION_TIMEOUT_MINUTES=30
|
SESSION_TIMEOUT_MINUTES=30
|
||||||
|
|||||||
253
backend/deployment/DEPLOY.md
Normal file
253
backend/deployment/DEPLOY.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# MCP IMAP Agent - Hetzner Deployment Guide
|
||||||
|
|
||||||
|
Deploy production-ready MCP IMAP server for N8N automation.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Hetzner VPS (Ubuntu/Arch Linux)
|
||||||
|
- Domain: `imap.maxtheweb.ai` pointing to server IP
|
||||||
|
- Root or sudo access
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. System Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update system (Arch Linux)
|
||||||
|
sudo pacman -Syu
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
sudo pacman -S python python-pip nginx certbot certbot-nginx git
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
sudo mkdir -p /opt/mcp-imap-agent
|
||||||
|
sudo chown $USER:$USER /opt/mcp-imap-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
cd /opt/mcp-imap-agent
|
||||||
|
git clone <your-repo> .
|
||||||
|
|
||||||
|
# Create virtual environment
|
||||||
|
cd backend
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create production .env
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required .env variables:**
|
||||||
|
```bash
|
||||||
|
MCP_PORT=8000
|
||||||
|
MCP_API_KEYS=generate_random_key_1,generate_random_key_2
|
||||||
|
RATE_LIMIT_PER_MINUTE=60
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate secure API keys:**
|
||||||
|
```bash
|
||||||
|
# Generate 3 random API keys for clients
|
||||||
|
openssl rand -hex 32
|
||||||
|
openssl rand -hex 32
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Install Systemd Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy service file
|
||||||
|
sudo cp deployment/mcp-imap.service /etc/systemd/system/
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable auto-start
|
||||||
|
sudo systemctl enable mcp-imap
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start mcp-imap
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status mcp-imap
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configure Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy nginx config
|
||||||
|
sudo cp deployment/nginx-mcp-imap.conf /etc/nginx/sites-available/mcp-imap
|
||||||
|
|
||||||
|
# Enable site
|
||||||
|
sudo ln -s /etc/nginx/sites-available/mcp-imap /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# Test config
|
||||||
|
sudo nginx -t
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Setup SSL with Certbot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get SSL certificate
|
||||||
|
sudo certbot --nginx -d imap.maxtheweb.ai
|
||||||
|
|
||||||
|
# Certbot auto-configures nginx and auto-renews
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Test Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check (no auth)
|
||||||
|
curl https://imap.maxtheweb.ai/health
|
||||||
|
|
||||||
|
# MCP endpoint (needs API key)
|
||||||
|
curl -H "Authorization: Bearer YOUR_API_KEY" https://imap.maxtheweb.ai/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## N8N Configuration
|
||||||
|
|
||||||
|
### Add MCP Server to N8N
|
||||||
|
|
||||||
|
N8N uses **MCP over SSE** (Server-Sent Events) protocol.
|
||||||
|
|
||||||
|
**Connection Settings:**
|
||||||
|
- **SSE URL**: `https://imap.maxtheweb.ai/mcp/sse`
|
||||||
|
- **Messages URL**: `https://imap.maxtheweb.ai/mcp/messages`
|
||||||
|
- **Authentication**: `Authorization: Bearer YOUR_API_KEY`
|
||||||
|
|
||||||
|
**In N8N:**
|
||||||
|
1. Add "HTTP Request" node or MCP-specific node
|
||||||
|
2. Configure SSE connection with API key in headers
|
||||||
|
3. MCP tools are called via JSON-RPC 2.0 protocol
|
||||||
|
|
||||||
|
### Example N8N MCP Tool Call
|
||||||
|
|
||||||
|
**Search emails:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "search_emails",
|
||||||
|
"arguments": {
|
||||||
|
"username": "user@gmail.com",
|
||||||
|
"password": "app_password",
|
||||||
|
"host": "imap.gmail.com",
|
||||||
|
"folder": "INBOX",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Send email:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "send_email",
|
||||||
|
"arguments": {
|
||||||
|
"username": "user@gmail.com",
|
||||||
|
"password": "app_password",
|
||||||
|
"smtp_host": "smtp.gmail.com",
|
||||||
|
"to": "recipient@example.com",
|
||||||
|
"subject": "Automated email",
|
||||||
|
"body": "This is sent via N8N + MCP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available MCP Tools
|
||||||
|
1. **list_folders** - List IMAP folders
|
||||||
|
2. **search_emails** - Search with filters (sender, subject, date)
|
||||||
|
3. **get_email** - Fetch full email content
|
||||||
|
4. **send_email** - Send email via SMTP
|
||||||
|
5. **health_check** - Server health status
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u mcp-imap -f
|
||||||
|
|
||||||
|
# Check nginx logs
|
||||||
|
sudo tail -f /var/log/nginx/mcp-imap-access.log
|
||||||
|
sudo tail -f /var/log/nginx/mcp-imap-error.log
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
htop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] Firewall configured (only 80, 443, 22 open)
|
||||||
|
- [ ] SSL certificate installed and auto-renewing
|
||||||
|
- [ ] Strong API keys generated (32+ chars)
|
||||||
|
- [ ] Rate limiting enabled (10 req/s)
|
||||||
|
- [ ] Logs monitored for unauthorized access
|
||||||
|
- [ ] Regular updates: `sudo pacman -Syu`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Service won't start
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u mcp-imap -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL errors
|
||||||
|
```bash
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection refused
|
||||||
|
```bash
|
||||||
|
# Check if service is running
|
||||||
|
sudo systemctl status mcp-imap
|
||||||
|
|
||||||
|
# Check port binding
|
||||||
|
sudo netstat -tlnp | grep 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Update code
|
||||||
|
```bash
|
||||||
|
cd /opt/mcp-imap-agent/backend
|
||||||
|
git pull
|
||||||
|
sudo systemctl restart mcp-imap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotate API keys
|
||||||
|
```bash
|
||||||
|
nano .env # Update MCP_API_KEYS
|
||||||
|
sudo systemctl restart mcp-imap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
```bash
|
||||||
|
# Backup .env (contains API keys)
|
||||||
|
sudo cp /opt/mcp-imap-agent/backend/.env ~/mcp-backup.env.$(date +%Y%m%d)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Issues: https://github.com/yourusername/mcp-imap-agent/issues
|
||||||
|
Docs: https://modelcontextprotocol.io
|
||||||
25
backend/deployment/mcp-imap.service
Normal file
25
backend/deployment/mcp-imap.service
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MCP IMAP Agent - Email automation server for N8N
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
WorkingDirectory=/opt/mcp-imap-agent/backend
|
||||||
|
Environment="PATH=/opt/mcp-imap-agent/backend/venv/bin"
|
||||||
|
EnvironmentFile=/opt/mcp-imap-agent/backend/.env
|
||||||
|
ExecStart=/opt/mcp-imap-agent/backend/venv/bin/python src/mcp_server.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/opt/mcp-imap-agent/backend
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
94
backend/deployment/nginx-mcp-imap.conf
Normal file
94
backend/deployment/nginx-mcp-imap.conf
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Nginx configuration for MCP IMAP Agent
|
||||||
|
# Place in: /etc/nginx/sites-available/mcp-imap
|
||||||
|
# Enable with: ln -s /etc/nginx/sites-available/mcp-imap /etc/nginx/sites-enabled/
|
||||||
|
# Get SSL cert: certbot --nginx -d imap.maxtheweb.ai
|
||||||
|
|
||||||
|
upstream mcp_backend {
|
||||||
|
server 127.0.0.1:8000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name imap.maxtheweb.ai;
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name imap.maxtheweb.ai;
|
||||||
|
|
||||||
|
# SSL certificates (certbot will populate these)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/imap.maxtheweb.ai/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/imap.maxtheweb.ai/privkey.pem;
|
||||||
|
|
||||||
|
# SSL configuration (Mozilla Intermediate)
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/mcp-imap-access.log;
|
||||||
|
error_log /var/log/nginx/mcp-imap-error.log;
|
||||||
|
|
||||||
|
# MCP SSE endpoint for N8N (streaming)
|
||||||
|
location /mcp/sse {
|
||||||
|
proxy_pass http://mcp_backend;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# SSE support (critical for MCP protocol)
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
|
||||||
|
# Chunked transfer encoding for streaming
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limit_req zone=api_limit burst=10 nodelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
# MCP messages endpoint for N8N (posting)
|
||||||
|
location /mcp/messages {
|
||||||
|
proxy_pass http://mcp_backend;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# Standard HTTP
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limit_req zone=api_limit burst=10 nodelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint (no auth required)
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://mcp_backend;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rate limiting zone (10 req/sec per IP)
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||||
@ -1,401 +0,0 @@
|
|||||||
"""
|
|
||||||
REST API server for MCP IMAP Agent
|
|
||||||
Provides HTTP endpoints for the Next.js frontend to communicate with MCP tools
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request, BackgroundTasks
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from pydantic import BaseModel, EmailStr
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
||||||
from slowapi.util import get_remote_address
|
|
||||||
from slowapi.errors import RateLimitExceeded
|
|
||||||
|
|
||||||
# Import MCP server components
|
|
||||||
from mcp_server import (
|
|
||||||
get_session,
|
|
||||||
IMAPConfig,
|
|
||||||
list_folders as mcp_list_folders,
|
|
||||||
search_emails as mcp_search_emails,
|
|
||||||
get_email as mcp_get_email,
|
|
||||||
send_email as mcp_send_email,
|
|
||||||
health_check as mcp_health_check,
|
|
||||||
Context
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Create rate limiter
|
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
|
||||||
|
|
||||||
# Lifespan manager for FastAPI
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting API server...")
|
|
||||||
yield
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down API server...")
|
|
||||||
|
|
||||||
# Create FastAPI app
|
|
||||||
app = FastAPI(
|
|
||||||
title="MCP IMAP Agent API",
|
|
||||||
version="1.0.0",
|
|
||||||
description="REST API for MCP IMAP Agent",
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add rate limiter error handler
|
|
||||||
app.state.limiter = limiter
|
|
||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
||||||
|
|
||||||
# Configure CORS
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000").split(","),
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Request models
|
|
||||||
class ConnectionConfig(BaseModel):
|
|
||||||
"""Email connection configuration"""
|
|
||||||
host: str
|
|
||||||
port: int = 993
|
|
||||||
username: EmailStr
|
|
||||||
password: str
|
|
||||||
smtpHost: Optional[str] = None
|
|
||||||
smtpPort: Optional[int] = 587
|
|
||||||
|
|
||||||
class ValidateConnectionRequest(BaseModel):
|
|
||||||
"""Connection validation request"""
|
|
||||||
host: str
|
|
||||||
port: int = 993
|
|
||||||
username: EmailStr
|
|
||||||
password: str
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
|
||||||
"""Chat request with LLM"""
|
|
||||||
message: str
|
|
||||||
connection: ConnectionConfig
|
|
||||||
history: Optional[list] = []
|
|
||||||
|
|
||||||
class SearchRequest(BaseModel):
|
|
||||||
"""Email search request"""
|
|
||||||
connection: ConnectionConfig
|
|
||||||
folder: str = "INBOX"
|
|
||||||
sender: Optional[str] = None
|
|
||||||
subject: Optional[str] = None
|
|
||||||
body: Optional[str] = None
|
|
||||||
since: Optional[str] = None
|
|
||||||
limit: int = 20
|
|
||||||
|
|
||||||
class GetEmailRequest(BaseModel):
|
|
||||||
"""Get single email request"""
|
|
||||||
connection: ConnectionConfig
|
|
||||||
folder: str = "INBOX"
|
|
||||||
email_id: str
|
|
||||||
|
|
||||||
class SendEmailRequest(BaseModel):
|
|
||||||
"""Send email request"""
|
|
||||||
connection: ConnectionConfig
|
|
||||||
to: EmailStr
|
|
||||||
subject: str
|
|
||||||
body: str
|
|
||||||
cc: Optional[str] = None
|
|
||||||
is_html: bool = False
|
|
||||||
|
|
||||||
# Endpoints
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
|
||||||
return {"message": "MCP IMAP Agent API", "version": "1.0.0"}
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
@limiter.limit("10/minute")
|
|
||||||
async def health(request: Request):
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/api/validate-connection")
|
|
||||||
@limiter.limit("5/minute")
|
|
||||||
async def validate_connection(request: Request, conn: ValidateConnectionRequest):
|
|
||||||
"""Validate IMAP connection"""
|
|
||||||
try:
|
|
||||||
# Test connection by listing folders
|
|
||||||
result = await mcp_list_folders(
|
|
||||||
None, # Context not needed for direct calls
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.get("success"):
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Connection successful",
|
|
||||||
"folders": result.get("folders", [])
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=400, detail=result.get("error", "Connection failed"))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Connection validation error: {e}")
|
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/chat")
|
|
||||||
@limiter.limit("30/minute")
|
|
||||||
async def chat(request: Request, chat_req: ChatRequest):
|
|
||||||
"""
|
|
||||||
Handle chat messages and execute MCP tools
|
|
||||||
Returns streaming response with results
|
|
||||||
"""
|
|
||||||
async def generate():
|
|
||||||
try:
|
|
||||||
# Parse the message to determine action
|
|
||||||
message = chat_req.message.lower()
|
|
||||||
conn = chat_req.connection
|
|
||||||
# Context not needed for REST API calls
|
|
||||||
|
|
||||||
# Simple intent detection (in production, use LLM)
|
|
||||||
if "list folder" in message or "show folder" in message:
|
|
||||||
result = await mcp_list_folders(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host
|
|
||||||
)
|
|
||||||
yield json.dumps({"type": "folders", "data": result}) + "\n"
|
|
||||||
|
|
||||||
elif "search" in message or "find" in message or "show" in message:
|
|
||||||
# Extract search parameters from message
|
|
||||||
search_params = {
|
|
||||||
"folder": "INBOX",
|
|
||||||
"limit": 20
|
|
||||||
}
|
|
||||||
|
|
||||||
if "from" in message:
|
|
||||||
# Simple extraction (in production, use NLP)
|
|
||||||
parts = message.split("from")
|
|
||||||
if len(parts) > 1:
|
|
||||||
sender = parts[1].strip().split()[0]
|
|
||||||
search_params["sender"] = sender
|
|
||||||
|
|
||||||
if "subject" in message:
|
|
||||||
parts = message.split("subject")
|
|
||||||
if len(parts) > 1:
|
|
||||||
subject = parts[1].strip().split()[0]
|
|
||||||
search_params["subject"] = subject
|
|
||||||
|
|
||||||
if "last week" in message:
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
week_ago = datetime.now() - timedelta(days=7)
|
|
||||||
search_params["since"] = week_ago.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
result = await mcp_search_emails(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host,
|
|
||||||
**search_params
|
|
||||||
)
|
|
||||||
|
|
||||||
yield json.dumps({"type": "search", "data": result}) + "\n"
|
|
||||||
|
|
||||||
elif "send" in message or "draft" in message or "reply" in message:
|
|
||||||
# For demo, just return a template
|
|
||||||
yield json.dumps({
|
|
||||||
"type": "draft",
|
|
||||||
"data": {
|
|
||||||
"success": True,
|
|
||||||
"message": "Ready to compose email. Please provide recipient, subject, and body.",
|
|
||||||
"template": {
|
|
||||||
"to": "",
|
|
||||||
"subject": "",
|
|
||||||
"body": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) + "\n"
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Default: search recent emails
|
|
||||||
result = await mcp_search_emails(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host,
|
|
||||||
folder="INBOX",
|
|
||||||
limit=10
|
|
||||||
)
|
|
||||||
|
|
||||||
yield json.dumps({
|
|
||||||
"type": "info",
|
|
||||||
"data": {
|
|
||||||
"message": f"Here are your recent emails. You can ask me to search, read, draft, or send emails.",
|
|
||||||
"emails": result.get("emails", [])
|
|
||||||
}
|
|
||||||
}) + "\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Chat error: {e}")
|
|
||||||
yield json.dumps({"type": "error", "data": {"error": str(e)}}) + "\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="application/x-ndjson")
|
|
||||||
|
|
||||||
@app.post("/api/search")
|
|
||||||
@limiter.limit("20/minute")
|
|
||||||
async def search_emails(request: Request, search_req: SearchRequest):
|
|
||||||
"""Search emails with filters"""
|
|
||||||
try:
|
|
||||||
# Context not needed for REST API calls
|
|
||||||
conn = search_req.connection
|
|
||||||
|
|
||||||
result = await mcp_search_emails(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host,
|
|
||||||
folder=search_req.folder,
|
|
||||||
sender=search_req.sender,
|
|
||||||
subject=search_req.subject,
|
|
||||||
body=search_req.body,
|
|
||||||
since=search_req.since,
|
|
||||||
limit=search_req.limit
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Search error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/get-email")
|
|
||||||
@limiter.limit("30/minute")
|
|
||||||
async def get_email(request: Request, email_req: GetEmailRequest):
|
|
||||||
"""Get full email content"""
|
|
||||||
try:
|
|
||||||
# Context not needed for REST API calls
|
|
||||||
conn = email_req.connection
|
|
||||||
|
|
||||||
result = await mcp_get_email(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host,
|
|
||||||
folder=email_req.folder,
|
|
||||||
email_id=email_req.email_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Get email error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/send")
|
|
||||||
@limiter.limit("10/minute")
|
|
||||||
async def send_email_endpoint(request: Request, send_req: SendEmailRequest):
|
|
||||||
"""Send an email"""
|
|
||||||
try:
|
|
||||||
# Context not needed for REST API calls
|
|
||||||
conn = send_req.connection
|
|
||||||
|
|
||||||
# Use SMTP host if provided, otherwise derive from IMAP host
|
|
||||||
smtp_host = conn.smtpHost
|
|
||||||
if not smtp_host:
|
|
||||||
if "gmail" in conn.host:
|
|
||||||
smtp_host = "smtp.gmail.com"
|
|
||||||
elif "outlook" in conn.host or "office365" in conn.host:
|
|
||||||
smtp_host = "smtp.office365.com"
|
|
||||||
else:
|
|
||||||
# Try replacing imap with smtp
|
|
||||||
smtp_host = conn.host.replace("imap.", "smtp.")
|
|
||||||
|
|
||||||
result = await mcp_send_email(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
smtp_host=smtp_host,
|
|
||||||
to=send_req.to,
|
|
||||||
subject=send_req.subject,
|
|
||||||
body=send_req.body,
|
|
||||||
cc=send_req.cc,
|
|
||||||
is_html=send_req.is_html
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Send email error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/folders")
|
|
||||||
@limiter.limit("10/minute")
|
|
||||||
async def list_folders(request: Request, conn: ConnectionConfig):
|
|
||||||
"""List email folders"""
|
|
||||||
try:
|
|
||||||
# Context not needed for REST API calls
|
|
||||||
result = await mcp_list_folders(
|
|
||||||
ctx,
|
|
||||||
username=conn.username,
|
|
||||||
password=conn.password,
|
|
||||||
host=conn.host
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"List folders error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/demo-request")
|
|
||||||
@limiter.limit("5/minute")
|
|
||||||
async def demo_request(request: Request, data: dict):
|
|
||||||
"""
|
|
||||||
Handle demo requests - sends user inquiry to max@maxtheweb.ai
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
user_email = data.get("email")
|
|
||||||
user_message = data.get("message", "Demo request")
|
|
||||||
|
|
||||||
# TODO: Send email to max@maxtheweb.ai with user's demo request
|
|
||||||
# For now, just log it
|
|
||||||
logger.info(f"Demo request from {user_email}: {user_message}")
|
|
||||||
|
|
||||||
# In production, you would:
|
|
||||||
# 1. Send email to max@maxtheweb.ai with user details
|
|
||||||
# 2. Queue AI processing job
|
|
||||||
# 3. Send results back to user_email
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Demo request received"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Demo request error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
port = int(os.getenv("API_PORT", 8001))
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
||||||
@ -34,6 +34,37 @@ logger = logging.getLogger(__name__)
|
|||||||
# Initialize FastMCP server
|
# Initialize FastMCP server
|
||||||
mcp = FastMCP("mcp-imap-agent")
|
mcp = FastMCP("mcp-imap-agent")
|
||||||
|
|
||||||
|
# Authentication middleware for N8N multi-tenant access
|
||||||
|
@mcp.add_middleware
|
||||||
|
async def auth_middleware(request, call_next):
|
||||||
|
"""Validate API key from N8N clients"""
|
||||||
|
# Get allowed API keys from environment
|
||||||
|
allowed_keys = os.getenv("MCP_API_KEYS", "").split(",")
|
||||||
|
|
||||||
|
# Extract API key from Authorization header
|
||||||
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
|
||||||
|
if not auth_header.startswith("Bearer "):
|
||||||
|
logger.warning(f"Missing or invalid Authorization header from {request.client.host}")
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=401,
|
||||||
|
content={"error": "Missing or invalid Authorization header"}
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key = auth_header.replace("Bearer ", "")
|
||||||
|
|
||||||
|
if api_key not in allowed_keys or not api_key:
|
||||||
|
logger.warning(f"Invalid API key attempt from {request.client.host}")
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=403,
|
||||||
|
content={"error": "Invalid API key"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Valid API key - proceed with request
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
# Rate limiter for security
|
# Rate limiter for security
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
@ -446,10 +477,18 @@ async def health_check(ctx: Context) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Run the MCP server
|
# Run the MCP server with SSE transport (N8N compatibility)
|
||||||
|
from fastmcp.server.http import create_sse_app
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
app = create_sse_app(
|
||||||
|
mcp,
|
||||||
|
message_path="/mcp/messages",
|
||||||
|
sse_path="/mcp/sse"
|
||||||
|
)
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
mcp.get_asgi_app(),
|
app,
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=int(os.getenv("MCP_PORT", 8000)),
|
port=int(os.getenv("MCP_PORT", 8000)),
|
||||||
log_level="info"
|
log_level="info"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user