π Complete Guide: Deploying MCP Server on AWS EC2 with SSE Transport and HTTPS
Model Context Protocol (MCP) is revolutionizing how AI applications interact with external data sources and tools. In this comprehensive guide, we'll walk through deploying a production-ready MCP server on AWS EC2 with Server-Sent Events (SSE) transport, complete with HTTPS SSL certificates and proper Nginx configuration.
π What You'll Learn
- Understanding MCP and SSE transport protocol
- Building and pushing Docker images to AWS ECR
- Deploying MCP server on EC2 with proper security
- Configuring Nginx for SSE streaming with SSL
- Troubleshooting common MCP deployment issues
- Testing with n8n and other MCP clients
π Understanding MCP and SSE Transport
What is Model Context Protocol (MCP)?
Model Context Protocol is an open standard that enables AI applications to securely connect to external data sources, databases, and tools. It provides a standardized way for Large Language Models (LLMs) to access real-time information and execute actions.
Key Benefits:
- Standardized Interface: Consistent API across different tools and services
- Security: Controlled access to external resources
- Real-time Data: Live connections to databases, APIs, and file systems
- Extensibility: Easy to add new tools and capabilities
Why SSE (Server-Sent Events) Transport?
Server-Sent Events provide unidirectional, real-time communication from server to client over HTTP. Unlike WebSockets, SSE is simpler and perfect for MCP's use case:
SSE Advantages for MCP:
- HTTP-based: Works with existing infrastructure (load balancers, proxies)
- Auto-reconnection: Built-in connection recovery
- Simpler than WebSockets: Less overhead for one-way communication
- Firewall-friendly: Uses standard HTTP/HTTPS ports
MCP Protocol Flow:
1. Client β GET /sse β Server (Establish SSE stream for responses)
2. Client β POST /messages β Server (Send MCP commands)
3. Server β SSE Stream β Client (Send responses back)
ποΈ Architecture Overview
βββββββββββββββ HTTPS βββββββββββββββ HTTP βββββββββββββββ
β MCP β βββββββββββΊ β Nginx β ββββββββββΊ β Docker β
β Client β β Proxy β β Container β
β (n8n/VSC) β β + SSL β β (MCP Server)β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β
βββββββββββββββ
β Let's β
β Encrypt β
β SSL Cert β
βββββββββββββββ
π οΈ Prerequisites
- AWS Account with CLI configured
- Docker installed locally
- Domain name with DNS management access
- Basic knowledge of Linux/Ubuntu commands
π¦ Step 1: Build and Push Docker Image to ECR
1.1 Create ECR Repository
# Create ECR repository
aws ecr create-repository --repository-name mcp-server --region us-east-1
# Get login token
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com
1.2 Build and Tag Docker Image
# Build your MCP server image
docker build -t mcp-server:latest .
# Tag for ECR
docker tag mcp-server:latest <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mcp-server:latest
# Push to ECR
docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mcp-server:latest
π₯οΈ Step 2: Launch and Configure EC2 Instance
2.1 Launch EC2 Instance
Via AWS Console:
- AMI: Ubuntu Server 22.04 LTS
- Instance Type: t3.small (or t2.micro for testing)
- Key Pair: Create or select existing
-
Security Group: Create with these ports:
- SSH (22): Your IP
- HTTP (80): 0.0.0.0/0
- HTTPS (443): 0.0.0.0/0
- Custom (8000): 0.0.0.0/0 (for testing)
2.2 Create IAM Role for ECR Access
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Attach Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
}
]
}
π³ Step 3: Setup Docker and Deploy Container
3.1 SSH into EC2 and Install Dependencies
# SSH into your instance
ssh -i your-key.pem ubuntu@<EC2_PUBLIC_IP>
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker ubuntu
# Apply docker group (avoid logout)
newgrp docker
# Install AWS CLI
sudo apt install -y awscli
3.2 Pull and Run MCP Server Container
# Login to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com
# Pull image
docker pull <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mcp-server:latest
# Create logs directory
mkdir -p ~/mcp-logs
# Run container
docker run -d \
--name mcp-server \
--restart unless-stopped \
-p 8000:8000 \
-v ~/mcp-logs:/app/logs \
<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mcp-server:latest
# Verify container is running
docker ps
docker logs mcp-server
# Test endpoint
curl http://localhost:8000/sse
π Step 4: Configure DNS
Add an A record in your DNS provider:
Type: A
Name: mcp-server
Value: <EC2_PUBLIC_IP>
TTL: 300
Verify DNS propagation:
nslookup mcp-server.yourdomain.com
dig mcp-server.yourdomain.com
π§ Step 5: Install and Configure Nginx
5.1 Install Nginx
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
5.2 Configure Nginx for MCP SSE Transport
β οΈ Critical Configuration for MCP Protocol:
sudo tee /etc/nginx/sites-available/mcp-server.conf > /dev/null << 'EOF'
server {
listen 80;
server_name mcp-server.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name mcp-server.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mcp-server.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp-server.yourdomain.com/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# CRITICAL: SSE endpoint - ONLY GET requests
location = /sse {
if ($request_method != GET) {
return 405;
}
proxy_pass http://127.0.0.1:8000/sse;
# Essential for SSE streaming
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 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;
# SSE specific - NO BUFFERING
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# CORS headers
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'Content-Type, Authorization';
}
# CRITICAL: Messages endpoint for POST requests
location /messages {
proxy_pass http://127.0.0.1:8000;
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;
proxy_set_header Content-Type $content_type;
# CORS headers
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Content-Type, Authorization';
}
# Handle CORS preflight requests
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Content-Type, Authorization';
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
return 301 /sse;
}
}
EOF
# Enable the configuration
sudo ln -s /etc/nginx/sites-available/mcp-server.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
π Step 6: Setup SSL Certificate with Let's Encrypt
6.1 Install Certbot
sudo apt install -y certbot python3-certbot-nginx
6.2 Obtain SSL Certificate
sudo certbot --nginx -d mcp-server.yourdomain.com --email your@email.com --agree-tos --non-interactive
6.3 Setup Auto-renewal
echo "0 12 * * * /usr/bin/certbot renew --quiet" | sudo tee -a /etc/crontab > /dev/null
6.4 Verify SSL
sudo certbot certificates
curl -v https://mcp-server.yourdomain.com/sse
π¨ Critical: Understanding the Nginx Configuration
Why This Configuration is Essential for MCP
β Common Mistake - Single Endpoint:
# This WILL NOT WORK for MCP
location /sse {
proxy_pass http://localhost:8000/sse;
# Tries to handle both GET and POST on same endpoint
}
β
Correct Configuration - Separate Endpoints:
# GET /sse - For SSE streaming (responses)
location = /sse {
if ($request_method != GET) { return 405; }
proxy_pass http://127.0.0.1:8000/sse;
}
# POST /messages - For MCP commands
location /messages {
proxy_pass http://127.0.0.1:8000;
}
Why MCP Needs Two Endpoints
MCP Protocol Requirements:
- SSE Stream (GET /sse): Persistent connection for receiving responses
- Command Channel (POST /messages): Send MCP commands and requests
What Happens:
Client establishes: GET /sse β Long-lived SSE connection
Client sends command: POST /messages/?session_id=xxx β JSON-RPC request
Server responds via: SSE stream β Real-time response
Common Errors and Solutions
Error: RuntimeError: Expected ASGI message 'http.response.body'
- Cause: POST requests sent to SSE endpoint
- Solution: Separate GET and POST endpoints as shown above
Error: 405 Method Not Allowed
- Cause: Wrong HTTP method for endpoint
- Solution: Ensure GET for /sse, POST for /messages
β Step 7: Testing Your MCP Server
7.1 Basic Connectivity Tests
# Test SSE endpoint
curl -v -H "Accept: text/event-stream" https://mcp-server.yourdomain.com/sse
# Expected output:
# event: endpoint
# data: /messages/?session_id=xxx
# : ping - 2024-01-01T12:00:00+00:00
# Test HTTPS redirect
curl -v http://mcp-server.yourdomain.com/sse
# Should redirect to HTTPS
7.2 Monitor Server Logs
# Watch container logs
docker logs mcp-server -f
# Expected successful logs:
# INFO: 172.17.0.1:40138 - "GET /sse HTTP/1.1" 200 OK
# INFO: 172.17.0.1:40152 - "POST /messages/?session_id=xxx HTTP/1.1" 202 Accepted
7.3 Test with VSCode Copilot, n8n
Add MCP Tool in n8n:
{
"url": "https://mcp-server.yourdomain.com/sse",
"transport": "SSE"
}
Successful Connection Indicators:
- n8n shows "Connected" status
- Server logs show both GET and POST requests
- Tools are listed and callable
π§ Troubleshooting Common Issues
Issue 1: DNS Not Resolving
Symptoms:
nslookup mcp-server.yourdomain.com
# Returns NXDOMAIN
Solutions:
- Verify A record is created correctly
- Wait for DNS propagation (up to 48 hours)
- Use online DNS checkers to verify propagation
Issue 2: SSL Certificate Fails
Symptoms:
Certbot failed to authenticate some domains
DNS problem: NXDOMAIN looking up A for mcp-server.yourdomain.com
Solutions:
- Ensure DNS is fully propagated before running certbot
- Verify domain points to correct IP
- Check firewall allows HTTP (port 80) for validation
Issue 3: MCP Client Can't Connect
Symptoms:
- Client shows "Connection failed"
- 405 Method Not Allowed errors
- ASGI protocol errors
Solutions:
- Verify Nginx configuration separates GET and POST endpoints
- Check CORS headers are present
- Ensure container is running and accessible on port 8000
Issue 4: SSE Stream Disconnects
Symptoms:
- Connection drops after few seconds
- Client keeps reconnecting
Solutions:
- Verify
proxy_buffering off
in Nginx - Check
proxy_read_timeout
is set high (3600s) - Ensure container handles SSE properly
π Production Considerations
Security Hardening
# Restrict SSH access
sudo ufw enable
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
# Update security group to restrict SSH to your IP only
Monitoring and Logging
# Setup log rotation
sudo tee /etc/logrotate.d/mcp-server << EOF
~/mcp-logs/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
}
EOF
# Monitor with CloudWatch (optional)
# Install CloudWatch agent for detailed monitoring
Backup Strategy
# Backup container logs
tar -czf mcp-logs-backup-$(date +%Y%m%d).tar.gz ~/mcp-logs/
# Backup Nginx configuration
sudo cp /etc/nginx/sites-available/mcp-server.conf ~/nginx-backup.conf
π― Final Verification Checklist
- [ ] Container running and accessible on port 8000
- [ ] DNS resolves to correct IP address
- [ ] HTTPS certificate valid and auto-renewing
- [ ] GET /sse returns SSE stream
- [ ] POST /messages accepts JSON requests
- [ ] MCP client (n8n/VS Code) connects successfully
- [ ] Server logs show both GET and POST requests
- [ ] Tools are discoverable and executable
π Conclusion
You now have a production-ready MCP server running on AWS EC2 with:
- Secure HTTPS with automatic SSL certificate renewal
- Proper SSE transport configuration for real-time communication
- Scalable architecture ready for production workloads
- Comprehensive monitoring and troubleshooting capabilities
The key insight from this deployment is understanding that MCP requires two separate endpoints: one for SSE streaming (GET /sse) and another for command messages (POST /messages). This separation is crucial for the protocol to work correctly with clients like n8n, VS Code, and other MCP-compatible tools.
Next Steps
- Scale horizontally: Use Application Load Balancer for multiple instances
- Add monitoring: Implement CloudWatch dashboards and alerts
- Enhance security: Add API authentication and rate limiting
- Optimize performance: Implement caching and connection pooling
π Additional Resources
- Model Context Protocol Specification
- Server-Sent Events MDN Documentation
- n8n MCP Integration Guide
- AWS EC2 Best Practices
Happy deploying! π If you found this guide helpful, please share it with others building MCP servers.
Top comments (0)