Introduction

MCP servers can run anywhere from a local desktop process to a globally distributed cloud service. The deployment model depends on the transport (stdio vs Streamable HTTP), the audience (single user vs multi-tenant), and the operational requirements (availability, latency, compliance). This lesson covers the major deployment patterns and their trade-offs.

Key Concepts

  • stdio Transport: The server runs as a local child process, communicating via stdin/stdout. Ideal for desktop applications like Claude Desktop and Cursor. No network exposure.
  • Streamable HTTP Transport: The server runs as an HTTP service, accepting JSON-RPC messages over HTTP POST. Required for remote, multi-user, and cloud deployments.
  • Serverless Deployment: Running MCP servers as functions on AWS Lambda or Cloudflare Workers. Cost-effective for low-traffic tools with the trade-off of cold start latency.
  • Process Manager: Tools like PM2 or systemd that manage server lifecycle, automatic restarts, log rotation, and graceful shutdown for stdio-based servers.

Real World Context

A developer tools company offers three MCP servers: a code search tool (used by individual developers in their IDEs), a CI/CD trigger (used by team leads via a shared web interface), and a metrics dashboard (used company-wide). The code search tool deploys as a stdio server bundled with their VS Code extension. The CI/CD trigger deploys as a Streamable HTTP server on Kubernetes with OAuth. The metrics dashboard runs as a serverless function on Cloudflare Workers for cost efficiency.

Deep Dive

Local stdio Deployment

For desktop applications, deploy MCP servers as stdio processes managed by the host application:

json
{
  "mcpServers": {
    "code-search": {
      "command": "node",
      "args": ["./dist/code-search-server.js"],
      "env": {
        "WORKSPACE_ROOT": "/Users/dev/projects",
        "MAX_RESULTS": "50"
      }
    }
  }
}

This configuration tells the Host application (Claude Desktop, Cursor) to spawn the server as a child process. The server reads JSON-RPC messages from stdin and writes responses to stdout.

Streamable HTTP Server with Express

For cloud deployment, wrap the MCP server in an HTTP endpoint:

typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "crypto";

const app = express();

// Store transports by session ID for stateful connections
const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;

  let transport: StreamableHTTPServerTransport;

  if (sessionId && sessions.has(sessionId)) {
    transport = sessions.get(sessionId)!;
  } else {
    const newSessionId = randomUUID();
    transport = new StreamableHTTPServerTransport("/mcp", res);

    const server = new McpServer({
      name: "production-server",
      version: "1.0.0"
    });

    // Register tools
    server.tool("health", "Check service health", {},
      async () => ({ content: [{ type: "text", text: "OK" }] })
    );

    await server.connect(transport);
    sessions.set(newSessionId, transport);
    res.setHeader("mcp-session-id", newSessionId);
  }

  await transport.handleRequest(req, res);
});

// Health check for load balancers
app.get("/health", (req, res) => {
  res.json({ status: "healthy", sessions: sessions.size });
});

app.listen(3100, () => {
  console.log("MCP server listening on port 3100");
});

The session map maintains state across multiple requests from the same client, which is necessary for tools that have multi-step workflows.

Serverless Deployment on Cloudflare Workers

For cost-effective, globally distributed MCP servers:

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "GET" && new URL(request.url).pathname === "/health") {
      return new Response(JSON.stringify({ status: "healthy" }), {
        headers: { "Content-Type": "application/json" }
      });
    }

    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    const server = new McpServer({
      name: "serverless-tools",
      version: "1.0.0"
    });

    server.tool("lookup", "Look up information",
      { query: z.string().max(500) },
      async ({ query }) => {
        const result = await env.KV.get(query);
        return {
          content: [{ type: "text", text: result ?? "Not found" }]
        };
      }
    );

    const transport = new StreamableHTTPServerTransport("/mcp");
    await server.connect(transport);
    return transport.handleRequest(request);
  }
};

Serverless deployments are stateless by nature. Any session state must be stored externally (KV store, database).

Process Management with PM2

For long-running stdio servers that need reliability:

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "mcp-file-server",
      script: "./dist/file-server.js",
      instances: 1,
      autorestart: true,
      max_restarts: 10,
      restart_delay: 5000,
      max_memory_restart: "256M",
      env: {
        NODE_ENV: "production",
        ALLOWED_ROOT: "/data/files"
      },
      error_file: "./logs/file-server-error.log",
      out_file: "./logs/file-server-out.log",
      log_date_format: "YYYY-MM-DD HH:mm:ss Z"
    }
  ]
};

PM2 provides automatic restarts on crashes, memory limit enforcement, and structured logging.

Common Pitfalls

  1. Using stdio for multi-user servers: stdio transport is 1:1 (one client, one server process). For multi-user scenarios, use Streamable HTTP transport.
  2. No health checks: Cloud orchestrators (Kubernetes, ECS) need health check endpoints to detect and replace unhealthy instances.
  3. Storing session state in memory for serverless: Serverless functions may be terminated between requests. Store session state in a persistent store like Redis or DynamoDB.

Best Practices

  1. Match transport to deployment model: stdio for local/desktop, Streamable HTTP for cloud/multi-user, serverless for low-traffic global tools.
  2. Always implement health checks: Expose a /health endpoint that orchestrators can probe. Include basic metrics like active session count.
  3. Use graceful shutdown: Handle SIGTERM to close active connections cleanly before the process exits.

Summary

  • stdio transport suits local desktop deployments with 1:1 client-server relationships
  • Streamable HTTP transport enables cloud deployment with multi-user support and OAuth
  • Serverless platforms offer cost-effective global distribution for stateless tools
  • Process managers (PM2, systemd) ensure reliability for long-running stdio servers
  • Health check endpoints are essential for all cloud and container deployments

Code Examples

typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport("/mcp");
  const server = new McpServer({ name: "prod", version: "1.0.0" });
  // Register tools...
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.get("/health", (req, res) => res.json({ status: "healthy" }));
app.listen(3100);
✓ Completed