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:
typescriptimport 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:
typescriptimport { 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
- Using stdio for multi-user servers: stdio transport is 1:1 (one client, one server process). For multi-user scenarios, use Streamable HTTP transport.
- No health checks: Cloud orchestrators (Kubernetes, ECS) need health check endpoints to detect and replace unhealthy instances.
- 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
- Match transport to deployment model: stdio for local/desktop, Streamable HTTP for cloud/multi-user, serverless for low-traffic global tools.
- Always implement health checks: Expose a
/healthendpoint that orchestrators can probe. Include basic metrics like active session count. - 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
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);