Skip to content

Deploy ​

Step-by-step guide to deploying air MCP servers.

Build ​

bash
# Compile TypeScript
npx tsc

# Check output
ls dist/
# index.js  index.d.ts  ...

Local execution ​

stdio (Claude Desktop direct connection) ​

No process management needed. Claude Desktop starts and stops the server as a child process.

bash
# Register with Claude Desktop
npx @airmcp-dev/cli connect claude-desktop

# Done. Restart Claude Desktop and the server runs automatically.

Generated config:

json
{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["tsx", "/path/to/my-server/src/index.ts"]
    }
  }
}

Development mode ​

bash
# Default (stdio)
npx @airmcp-dev/cli dev

# SSE + test console
npx @airmcp-dev/cli dev --console -p 3510

# HTTP
npx @airmcp-dev/cli dev --transport http -p 3510

Includes hot reload — auto-restarts on src/ file changes.

Direct execution ​

Run with node after build:

bash
# Default (auto detect)
node dist/index.js

# Specify transport
MCP_TRANSPORT=sse PORT=3510 node dist/index.js
MCP_TRANSPORT=http PORT=3510 node dist/index.js

Remote deployment (SSE / HTTP) ​

Remote servers need a process manager and (optionally) a reverse proxy.

PM2 ​

bash
npm install -g pm2

# Start
pm2 start dist/index.js --name my-server \
  --env NODE_ENV=production \
  --env MCP_TRANSPORT=http \
  --env PORT=3510

# Auto-start on system boot
pm2 startup
pm2 save

# Status / logs / restart / stop
pm2 status
pm2 logs my-server
pm2 restart my-server
pm2 stop my-server

Docker ​

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ dist/

ENV NODE_ENV=production
ENV MCP_TRANSPORT=http
ENV PORT=3510

EXPOSE 3510
CMD ["node", "dist/index.js"]
bash
docker build -t my-mcp-server .
docker run -d -p 3510:3510 --name mcp my-mcp-server

# Pass environment variables
docker run -d -p 3510:3510 \
  -e MCP_API_KEY=your-key \
  --name mcp my-mcp-server

systemd ​

ini
[Unit]
Description=My MCP Server
After=network.target

[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/my-server
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=MCP_TRANSPORT=http
Environment=PORT=3510

[Install]
WantedBy=multi-user.target
bash
sudo cp my-mcp-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable my-mcp-server
sudo systemctl start my-mcp-server

# Status / logs
sudo systemctl status my-mcp-server
sudo journalctl -u my-mcp-server -f

Cloud deployment ​

AWS (EC2 + Docker) ​

bash
# SSH into EC2 instance
sudo yum install -y docker   # Amazon Linux
sudo systemctl start docker

# Build & run
docker build -t my-mcp-server .
docker run -d -p 3510:3510 \
  -e NODE_ENV=production \
  -e MCP_TRANSPORT=http \
  -e MCP_API_KEY=$MCP_API_KEY \
  --restart unless-stopped \
  --name mcp my-mcp-server

AWS (ECS Fargate) ​

task-definition.json:

json
{
  "family": "mcp-server",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [{
    "name": "mcp",
    "image": "YOUR_ECR_URI/my-mcp-server:latest",
    "portMappings": [{ "containerPort": 3510 }],
    "environment": [
      { "name": "NODE_ENV", "value": "production" },
      { "name": "MCP_TRANSPORT", "value": "http" },
      { "name": "PORT", "value": "3510" }
    ],
    "secrets": [
      { "name": "MCP_API_KEY", "valueFrom": "arn:aws:ssm:REGION:ACCOUNT:parameter/mcp-api-key" }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/mcp-server",
        "awslogs-region": "ap-northeast-2",
        "awslogs-stream-prefix": "mcp"
      }
    }
  }]
}
bash
# Push image to ECR
aws ecr get-login-password | docker login --username AWS --password-stdin YOUR_ECR_URI
docker tag my-mcp-server:latest YOUR_ECR_URI/my-mcp-server:latest
docker push YOUR_ECR_URI/my-mcp-server:latest

# Create service
aws ecs create-service \
  --cluster my-cluster \
  --service-name mcp-server \
  --task-definition mcp-server \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx],assignPublicIp=ENABLED}"

AWS (Lambda + API Gateway) ​

Run on Lambda via HTTP transport. Pay-per-request pricing makes this ideal for low-traffic servers.

typescript
// lambda.ts
import { defineServer, defineTool } from '@airmcp-dev/core';

const server = defineServer({
  name: 'lambda-server',
  transport: { type: 'http' },
  storage: { type: 'memory' },  // Lambda's filesystem is ephemeral — use memory
  tools: [ /* ... */ ],
});

export const handler = async (event: any) => {
  // API Gateway → Lambda proxy integration
  return server.handleLambdaEvent(event);
};

WARNING

FileStore on Lambda writes to /tmp, but data is lost when the instance shuts down. For persistent storage, use DynamoDB or S3 directly.

Cloudflare Workers ​

See the Cloudflare Workers deployment guide.

Nginx reverse proxy ​

HTTP transport ​

nginx
server {
    listen 443 ssl;
    server_name mcp.example.com;

    ssl_certificate /etc/ssl/certs/mcp.crt;
    ssl_certificate_key /etc/ssl/private/mcp.key;

    location / {
        proxy_pass http://127.0.0.1:3510;
        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;
    }
}

SSE transport ​

SSE requires long-lived connection settings:

nginx
server {
    listen 443 ssl;
    server_name mcp.example.com;

    location / {
        proxy_pass http://127.0.0.1:3510;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400;    # 24 hours
        proxy_send_timeout 86400;
    }
}

TIP

Use http transport behind reverse proxies. SSE needs extra config (proxy_read_timeout 86400) and some CDNs/proxies may drop SSE connections unexpectedly.

Environment variables ​

VariableDefaultDescription
NODE_ENVdevelopmentproduction switches logging to JSON
PORT3510HTTP/SSE port (overrides transport.port)
MCP_TRANSPORT(auto)Force transport: stdio, sse, http
AIR_LOG_LEVELinfoLog level override

Health check ​

HTTP/SSE transports expose server status via GET /:

bash
curl http://localhost:3510/

Response:

json
{
  "name": "my-server",
  "version": "1.0.0",
  "state": "running",
  "uptime": 86400000,
  "toolCount": 5,
  "resourceCount": 0,
  "transport": "http"
}

Production checklist ​

  • Build with npx tsc (no type errors)
  • Set NODE_ENV=production (enables JSON logging)
  • Manage API keys via environment variables (never hardcode)
  • Use a process manager (PM2, systemd, Docker)
  • Set up HTTPS reverse proxy (for remote deployments)
  • Register cleanup logic with onShutdown (DB connections, etc.)

Released under the Apache-2.0 License.