Deploying & Hosting Remote MCP Servers
TL;DR — Production Deployment Quickstart
Running a local stdio MCP server is ideal for personal dev tools. To share tools across your team or AI agent fleet, wrap your MCP logic in an HTTP/SSE transport, package it as a Docker image or Cloudflare Worker, enforce TLS & Bearer token auth, and route requests to dedicated /sse and /message endpoints.
Stdio vs Remote HTTP/SSE: When to Deploy
The Model Context Protocol (MCP) supports two primary transport mechanisms for communicating between host AI applications (Claude Desktop, Claude Code, Cursor, Windsurf) and MCP servers:
- Standard Input/Output (stdio): The client launches the server as a local child process. Messages stream over OS IPC pipes (
stdinandstdout). This requires zero network setup and is perfect for desktop tools touching local files. - Server-Sent Events (SSE) & Streamable HTTP: The server operates as an independent web service listening on an HTTP port. The client connects over network endpoints (
/ssefor streaming server-to-client events and/messagefor client-to-server POST requests).
Deploying a remote MCP server is necessary when:
- Multiple team members or autonomous AI agents need to query a shared centralized database or private microservice without replicating database credentials locally.
- Your MCP server requires high-throughput compute, GPU acceleration, or persistent background tasks that cannot run on end-user laptops.
- You are building a SaaS product or commercial tool that exposes MCP capabilities to subscribers over API authentication. Once that server is monetized, tools like AgentMRR let you publish provider-verified recurring revenue for the agent product it powers.
Remote MCP Architecture & Transport Flow
Understanding the lifecycle of a remote SSE MCP session helps avoid common network disconnects and connection leaks:
- Session Initialization (HTTP GET /sse): The AI client opens an HTTP GET request to the server’s
/sseendpoint. The server responds withContent-Type: text/event-streamand sends an initial event payload containing anendpointURL with a unique session ID:event: endpoint data: /message?sessionId=sess_987654321_abc - Client Request Dispatch (HTTP POST /message): Whenever the host model calls an MCP tool or requests a resource, the client sends an HTTP POST to
/message?sessionId=sess_987654321_abccontaining standard JSON-RPC 2.0 requests:{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "query_database", "arguments": { "query": "SELECT count(*) FROM users;" } } } - Server Execution & SSE Stream Response: The server receives the POST request, processes the handler asynchronously, and pushes the JSON-RPC response back down the persistent SSE connection.
Deploying on Cloudflare Workers (Edge Serverless)
Cloudflare Workers provide an ultra-low latency, globally distributed edge environment for hosting stateless or durable MCP tools. Using Cloudflare’s official agents framework, you can deploy a remote MCP server in minutes:
// wrangler.json
{
"name": "production-mcp-agent",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"observability": { "enabled": true }
}
Write your server logic inside src/index.ts:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class CloudflareMCPServer extends McpAgent {
server = new McpServer({
name: "cloud-mcp-service",
version: "1.0.0",
});
async init() {
// Register custom production tool
this.server.tool(
"fetch_weather_forecast",
{
city: z.string().min(2),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
return {
content: [
{
type: "text",
text: JSON.stringify({
city,
temp: units === "celsius" ? 22 : 72,
condition: "Sunny",
}),
},
],
};
}
);
}
}
export default {
async fetch(request: Request, env: Record<string, unknown>, ctx: ExecutionContext) {
const url = new URL(request.url);
// Secure endpoint with Bearer Token validation
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Unauthorized: Missing Bearer Token" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname.startsWith("/mcp")) {
return CloudflareMCPServer.serve("/mcp").fetch(request, env, ctx);
}
return new Response("MCP Server operational.", { status: 200 });
},
};
Deploy to Cloudflare Workers with a single command:
npx wrangler deploy
Containerizing MCP Servers with Docker
For microservices, enterprise Linux servers, or Kubernetes deployments, containerizing your MCP server guarantees consistent runtimes and isolates host system dependencies.
Below is an optimized, multi-stage Dockerfile for a TypeScript MCP server:
# Stage 1: Build TypeScript binaries
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
# Stage 2: Production runtime image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Security: Run as non-root user
USER node
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3001
CMD ["node", "dist/server.js"]
Combine your server container with Docker Compose for local testing or production deployment:
# docker-compose.yml
version: '3.8'
services:
mcp-server:
build: .
ports:
- "3001:3001"
environment:
- PORT=3001
- DATABASE_URL=postgresql://user:secret@db:5432/app_db
- MCP_API_KEY=prod_mcp_sec_8f92a1b3c4
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
interval: 30s
timeout: 5s
retries: 3
Deploying to Fly.io & Cloud Platforms
Platforms like Fly.io, Railway, and Render excel at hosting containerized SSE services because they support persistent, long-lived TCP/HTTP connections without strict gateway timeouts.
To deploy to Fly.io using their CLI:
# Generate fly.toml configuration
fly launch --name my-remote-mcp-server --no-deploy
# Deploy container image to Fly.io global region
fly deploy
Configure secrets securely using Fly CLI instead of committing credentials to source code:
fly secrets set DATABASE_URL="postgresql://user:pass@host:5432/db" MCP_AUTH_TOKEN="sec_key_12345"
Express SSE Remote Server Code (TypeScript)
Below is a complete, production-ready Express.js server written in TypeScript that configures the official SSEServerTransport from @modelcontextprotocol/sdk with session tracking:
import express from 'express';
import cors from 'cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { z } from 'zod';
const app = express();
app.use(cors({ origin: '*' }));
app.use(express.json());
// Active sessions map
const transports = new Map<string, SSEServerTransport>();
// Initialize MCP Server capabilities
function createMcpServer() {
const server = new McpServer({
name: "production-express-mcp",
version: "1.0.0",
});
server.tool(
"calculate_tax",
{
amount: z.number().positive(),
state: z.string().length(2),
},
async ({ amount, state }) => {
const taxRate = state.toUpperCase() === 'CA' ? 0.0725 : 0.05;
return {
content: [
{
type: "text",
text: JSON.stringify({
subtotal: amount,
tax: Number((amount * taxRate).toFixed(2)),
total: Number((amount * (1 + taxRate)).toFixed(2)),
}),
},
],
};
}
);
return server;
}
// 1. SSE Connection Endpoint
app.get('/sse', async (req, res) => {
console.log('Client connected to /sse');
const transport = new SSEServerTransport('/message', res);
const server = createMcpServer();
transports.set(transport.sessionId, transport);
req.on('close', () => {
console.log(`Session ${transport.sessionId} closed`);
transports.delete(transport.sessionId);
});
await server.connect(transport);
});
// 2. HTTP POST Message Endpoint
app.post('/message', async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(404).send('Session not found or expired');
return;
}
await transport.handlePostMessage(req, res);
});
// Health check endpoint for load balancers
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', activeSessions: transports.size });
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Remote MCP Express server listening on http://localhost:${PORT}`);
});
Reverse Proxies (Nginx & Caddy) & SSL Setup
Never expose Node.js or Python application processes directly to the public internet. Always place a reverse proxy like Nginx or Caddy in front to handle HTTPS TLS termination, HTTP/1.1 response streaming, and client request buffering.
Caddyfile Configuration (Automatic Let’s Encrypt SSL)
mcp.yourdomain.com {
# Automatic TLS certificate provisioned by Caddy
reverse_proxy localhost:3001 {
# Flush SSE data immediately to prevent response buffering delay
flush_interval -1
}
}
Nginx Configuration (For Long-Lived SSE Connections)
server {
listen 443 ssl http2;
server_name mcp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Mandatory Nginx directives for Server-Sent Events (SSE)
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# Increase timeout for persistent client connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
Secrets Management & CORS Hardening
When hosting an MCP server in the cloud, security is a paramount concern. Review our comprehensive MCP Security Guide and Remote Authentication Guide for enterprise threat models. Always follow these infrastructure rules:
- Never hardcode API Keys: Inject credentials using environment variables (
process.env.API_KEY) or platform secret vaults (AWS Secrets Manager, Cloudflare Environment Secrets, Vault). - Restrict CORS Origins: If your remote server will be accessed from browser-based web clients or extensions, restrict
Access-Control-Allow-Originto known explicit domain origins rather than wildcard (*). - Rate Limiting: Use Nginx
limit_req_zoneor Redis rate-limiting middleware to cap incoming tool calls per token, protecting downstream APIs from runaway agent loops.
Health Monitoring & Log Hygiene
Maintaining operational observability for remote MCP servers requires separating stdout, stderr, and HTTP response channels cleanly:
⚠️ Critical Logging Rule for Stdio vs SSE Transports
In stdio mode, writing raw console.log() text to standard output corrupts the JSON-RPC transport stream and crashes the client. In remote HTTP/SSE mode, standard output is safe for application logs, but server metrics should still route to structured log aggregators (Datadog, CloudWatch, Axiom).
Expose a lightweight /health endpoint for load balancer health probes:
app.get('/health', async (req, res) => {
try {
// Optionally ping database or internal cache
await db.raw('SELECT 1');
res.status(200).json({ status: 'healthy', uptime: process.uptime() });
} catch (err) {
res.status(503).json({ status: 'unhealthy', error: err instanceof Error ? err.message : 'DB error' });
}
});
Connecting Clients to Remote Servers
Once your remote MCP server is deployed over HTTPS, users and developers can connect their AI clients by updating their JSON configuration snippets.
Example claude_desktop_config.json configuration for a remote SSE server:
{
"mcpServers": {
"remote-analytics-mcp": {
"url": "https://mcp.yourdomain.com/sse",
"headers": {
"Authorization": "Bearer sec_prod_token_998877"
}
}
}
}
For step-by-step instructions across Claude Code, Cursor, Windsurf, and VS Code, consult our detailed LLM Agents Integration Guide.
Frequently Asked Questions
Next Steps & Ecosystem Resources
Now that your remote MCP server is live in production, explore the rest of the AllMCPs documentation hub:
- Need to build a custom server first? Follow our step-by-step How to Build an MCP Server Guide.
- Deep dive into OAuth 2.0 PKCE and JWT auth with our guide to Securing Remote MCP Servers.
- Review complete threat models and prompt injection safety in our MCP Security Best Practices.
- Ready to put your server where developers and AI agents look for MCP tools? Submit your MCP server to AllMCPs or check out our Free Developer Tools.
- If your deployed server is powering a paid AI agent product, AgentMRR tracks provider-verified MRR for AI-native products, a way to show real revenue instead of just traffic numbers.