Skip to main content
Prepare your FastMCP server for production with this comprehensive checklist covering security, performance, monitoring, and reliability.

Security

HTTPS Configuration

Always use HTTPS in production to encrypt traffic:
server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8443,
    sslCert: "/path/to/cert.pem",
    sslKey: "/path/to/key.pem",
    sslCa: "/path/to/ca.pem", // Optional: for client cert auth
  },
});
1

Obtain SSL Certificates

Use a trusted Certificate Authority:
# Install certbot
sudo apt-get install certbot

# Obtain certificate
sudo certbot certonly --standalone -d yourdomain.com

# Certificates will be in:
# /etc/letsencrypt/live/yourdomain.com/fullchain.pem
# /etc/letsencrypt/live/yourdomain.com/privkey.pem
2

Configure Auto-Renewal

Set up automatic certificate renewal:
# Test renewal
sudo certbot renew --dry-run

# Add to crontab for auto-renewal
0 0 * * * certbot renew --quiet --post-hook "systemctl restart fastmcp"
3

Use Strong TLS Settings

Ensure your SSL configuration uses modern TLS:
import { readFileSync } from "fs";

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8443,
    sslCert: readFileSync("/etc/letsencrypt/live/yourdomain.com/fullchain.pem", "utf8"),
    sslKey: readFileSync("/etc/letsencrypt/live/yourdomain.com/privkey.pem", "utf8"),
  },
});
Never use self-signed certificates in production. They provide encryption but no identity verification.

Authentication

Implement authentication to protect your MCP server:
import { FastMCP, GoogleProvider, requireAuth } from "fastmcp";

const server = new FastMCP({
  name: "Production Server",
  version: "1.0.0",
  auth: new GoogleProvider({
    baseUrl: "https://mcp.yourdomain.com",
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  }),
});

server.addTool({
  name: "protected_tool",
  description: "Requires authentication",
  canAccess: requireAuth,
  execute: async () => "Authenticated access",
});

Environment Variables

Never hardcode secrets:
// ❌ Bad - hardcoded secrets
const server = new FastMCP({
  auth: new GoogleProvider({
    clientId: "123456.apps.googleusercontent.com",
    clientSecret: "GOCSPX-abc123def456",
    baseUrl: "https://example.com",
  }),
});

// ✅ Good - use environment variables
const server = new FastMCP({
  auth: new GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    baseUrl: process.env.BASE_URL!,
  }),
});
Use a .env file (never commit to git):
.env
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
BASE_URL=https://mcp.yourdomain.com
API_KEY=your-secure-api-key
DATABASE_URL=postgresql://user:pass@host:5432/db
Add to .gitignore:
.gitignore
.env
.env.local
.env.*.local
*.pem
*.key

Rate Limiting

Protect against abuse:
import { ratelimit } from "@hono/rate-limit";

const server = new FastMCP({
  name: "Production Server",
  version: "1.0.0",
});

const app = server.getApp();

// Apply rate limiting
app.use(
  "*",
  ratelimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    message: "Too many requests, please try again later",
  })
);

Health Checks and Monitoring

Health Check Endpoints

Configure health and readiness checks:
const server = new FastMCP({
  name: "Production Server",
  version: "1.0.0",
  health: {
    enabled: true,
    path: "/health",
    message: "healthy",
    status: 200,
  },
});
curl https://mcp.yourdomain.com/health
# Response: healthy

Structured Logging

Implement comprehensive logging:
import { FastMCP, Logger } from "fastmcp";
import winston from "winston";

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "combined.log" }),
  ],
});

// Add console in development
if (process.env.NODE_ENV !== "production") {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

class WinstonLogger implements Logger {
  debug(...args: unknown[]): void {
    logger.debug(args.join(" "));
  }
  error(...args: unknown[]): void {
    logger.error(args.join(" "));
  }
  info(...args: unknown[]): void {
    logger.info(args.join(" "));
  }
  log(...args: unknown[]): void {
    logger.info(args.join(" "));
  }
  warn(...args: unknown[]): void {
    logger.warn(args.join(" "));
  }
}

const server = new FastMCP({
  name: "Production Server",
  version: "1.0.0",
  logger: new WinstonLogger(),
});

Error Tracking

Integrate with error tracking services:
import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
});

server.addTool({
  name: "monitored_tool",
  description: "Tool with error tracking",
  execute: async ({ input }) => {
    const transaction = Sentry.startTransaction({
      op: "tool.execute",
      name: "monitored_tool",
    });

    try {
      // Tool logic
      const result = await processInput(input);
      transaction.setStatus("ok");
      return result;
    } catch (error) {
      transaction.setStatus("internal_error");
      Sentry.captureException(error);
      throw error;
    } finally {
      transaction.finish();
    }
  },
});

Performance Optimization

Connection Pooling

Reuse database and HTTP connections:
import { Pool } from "pg";

// Create connection pool outside request handlers
const dbPool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

server.addTool({
  name: "query_database",
  description: "Execute database query",
  execute: async ({ query }) => {
    const client = await dbPool.connect();
    try {
      const result = await client.query(query);
      return JSON.stringify(result.rows);
    } finally {
      client.release();
    }
  },
});

Caching

Implement caching for expensive operations:
import { LRUCache } from "lru-cache";

const cache = new LRUCache<string, string>({
  max: 500,
  ttl: 1000 * 60 * 5, // 5 minutes
});

server.addTool({
  name: "cached_operation",
  description: "Expensive operation with caching",
  execute: async ({ key }) => {
    // Check cache first
    const cached = cache.get(key);
    if (cached) {
      return cached;
    }

    // Perform expensive operation
    const result = await expensiveOperation(key);

    // Store in cache
    cache.set(key, result);

    return result;
  },
});

Timeout Configuration

Set appropriate timeouts:
server.addTool({
  name: "api_call",
  description: "Call external API with timeout",
  execute: async ({ url }) => {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000); // 5s timeout

    try {
      const response = await fetch(url, {
        signal: controller.signal,
      });
      return await response.text();
    } catch (error) {
      if (error.name === "AbortError") {
        throw new UserError("Request timeout after 5 seconds");
      }
      throw error;
    } finally {
      clearTimeout(timeout);
    }
  },
});

Ping Configuration

Optimize ping behavior for your transport:
const server = new FastMCP({
  name: "Production Server",
  version: "1.0.0",
  ping: {
    enabled: true,
    intervalMs: 10000, // 10 seconds
    logLevel: "debug", // Don't spam logs
  },
});

Process Management

Using PM2

Keep your server running with PM2:
# Install PM2
npm install -g pm2

# Start server
pm2 start dist/server.js --name fastmcp-server

# Configure auto-restart
pm2 startup
pm2 save

# Monitor
pm2 monit

# View logs
pm2 logs fastmcp-server
PM2 ecosystem file:
ecosystem.config.js
module.exports = {
  apps: [{
    name: "fastmcp-server",
    script: "./dist/server.js",
    instances: "max",
    exec_mode: "cluster",
    env: {
      NODE_ENV: "production",
      PORT: 8080,
    },
    error_file: "./logs/err.log",
    out_file: "./logs/out.log",
    log_date_format: "YYYY-MM-DD HH:mm:ss Z",
    max_memory_restart: "1G",
  }],
};

Using systemd

Create a systemd service:
/etc/systemd/system/fastmcp.service
[Unit]
Description=FastMCP Server
After=network.target

[Service]
Type=simple
User=fastmcp
WorkingDirectory=/opt/fastmcp
Environment=NODE_ENV=production
ExecStart=/usr/bin/node dist/server.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
# Enable and start service
sudo systemctl enable fastmcp
sudo systemctl start fastmcp

# Check status
sudo systemctl status fastmcp

# View logs
sudo journalctl -u fastmcp -f

Production Checklist

1

Security

  • HTTPS enabled with valid SSL certificates
  • Authentication configured (OAuth or API keys)
  • Secrets stored in environment variables (not in code)
  • Rate limiting implemented
  • Input validation on all tools
  • CORS configured appropriately
2

Monitoring

  • Health check endpoint configured
  • Structured logging implemented
  • Error tracking service integrated
  • Performance metrics collected
  • Alerts configured for critical errors
3

Performance

  • Connection pooling for databases
  • Caching for expensive operations
  • Appropriate timeout values set
  • Stateless mode for serverless deployments
  • Ping behavior optimized
4

Reliability

  • Process manager configured (PM2 or systemd)
  • Auto-restart on failure
  • Graceful shutdown handling
  • Load balancing if needed
  • Backup and disaster recovery plan
5

Documentation

  • API documentation updated
  • Deployment runbook created
  • Incident response plan documented
  • Team trained on operations

Next Steps

Build docs developers (and LLMs) love