Back to Blog

MCP Server Hosting and Deployment: Complete Infrastructure Guide

Ultrion TeamJuly 22, 202614 min read

title: "MCP Server Hosting and Deployment: Complete Infrastructure Guide" slug: "mcp-server-hosting-and-deployment" description: "Everything you need to host, deploy, and scale MCP servers in production β€” from single-server setups to multi-region fleets with auto-scaling, monitoring, and cost optimization." category: "Technical" publishedAt: "2026-07-22"

MCP Server Hosting and Deployment: Complete Infrastructure Guide

Building an MCP server is one thing. Hosting it reliably in production β€” with auto-scaling, monitoring, security, and cost optimization β€” is an entirely different challenge. This guide covers every deployment option, from simple single-server setups to enterprise-grade multi-region fleets.

Understanding MCP Server Deployment Requirements

An MCP server is a process that exposes tools, resources, and prompts to AI clients via the Model Context Protocol. Unlike traditional web servers, MCP servers have unique characteristics:

  • Stateful connections β€” Many MCP transports use stdio or SSE, requiring persistent connections
  • Variable latency β€” Tool execution can take 100ms (simple lookups) to 30s (complex AI operations)
  • Unpredictable load β€” Agent usage patterns create bursty traffic
  • Tool-specific dependencies β€” Each tool may need different runtimes, databases, or APIs
  • Cost attribution β€” You need to track per-invocation costs for billing and optimization

These requirements influence every infrastructure decision.

Transport Layer Options

stdio Transport (Local)

The simplest transport β€” the MCP server communicates with the client via stdin/stdout. Used for local development and agents running on the same machine.

Use case: Development, local tools, single-user agents Pros: Zero network overhead, simplest setup Cons: Cannot serve remote clients, no horizontal scaling

SSE (Server-Sent Events) Transport

HTTP-based transport using Server-Sent Events for server→client and POST for client→server. Enables remote connections.

Use case: Production servers serving remote agents Pros: Works over HTTP, supports authentication, scales horizontally Cons: Requires web server infrastructure

Streamable HTTP Transport (2026 Standard)

The newest MCP transport, replacing SSE with a more efficient bidirectional HTTP protocol:

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

const app = express();

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

// Register tools
server.tool("get_weather", { city: z.string() }, async ({ city }) => {
  const weather = await fetchWeather(city);
  return { content: [{ type: "text", text: JSON.stringify(weather) }] };
});

const transport = new StreamableHTTPServerTransport({ sessionIdMethod: "header" });
await server.connect(transport);

app.post("/mcp", express.json(), (req, res) => {
  transport.handleRequest(req, res);
});

app.listen(3000);

Use case: Production deployments in 2026 Pros: Best performance, connection reuse, works with load balancers Cons: Requires MCP SDK 2026.1+

Deployment Option 1: Single VPS (€5-€30/month)

For early-stage MCP servers handling <1,000 requests/day:

Setup

# Dockerfile
FROM node:22-slim

WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .

# Install MCP server dependencies
RUN npm install @modelcontextprotocol/sdk

EXPOSE 3000
CMD ["node", "server.js"]

Docker Compose

version: "3.9"
services:
  mcp-server:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
      - RATE_LIMIT_RPM=100
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
  
  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certs:/etc/nginx/certs
    depends_on:
      - mcp-server

Nginx Reverse Proxy

upstream mcp_backend {
    server mcp-server:3000;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name mcp.yourdomain.com;
    
    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    
    location / {
        proxy_pass http://mcp_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 60s;
    }
    
    # Rate limiting
    limit_req zone=mcp_zone burst=20 nodelay;
}

Providers: Hetzner (€5-€15), DigitalOcean (€12-€24), Contabo (€6-€15) Recommendation: Hetzner CX22 (2 vCPU, 4GB RAM) at €4.5/month β€” best value in 2026

Deployment Option 2: Container Orchestration (€50-€200/month)

For MCP servers handling 1,000-50,000 requests/day:

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mcp-server
  template:
    metadata:
      labels:
        app: mcp-server
    spec:
      containers:
        - name: mcp-server
          image: registry.gitlab.com/myorg/mcp-server:v1.4.2
          ports:
            - containerPort: 3000
          env:
            - name: MCP_TRANSPORT
              value: "streamable-http"
            - name: REDIS_URL
              valueFrom:
                secretKeyRef:
                  name: mcp-secrets
                  key: redis-url
            - name: MODEL_API_KEY
              valueFrom:
                secretKeyRef:
                  name: mcp-secrets
                  key: model-api-key
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1000m"
              memory: "512Mi"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-server
spec:
  selector:
    app: mcp-server
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mcp-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mcp-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Managed Kubernetes Options

Provider Entry Price Best For
DigitalOcean DOKS €12/month + nodes Small teams
Google GKE Autopilot €70/month Auto-scaling focus
AWS EKS €80/month + nodes AWS-native shops
Azure AKS Free + nodes Enterprise Microsoft
Hetzner + k3s €10/node Budget-conscious

Recommendation: k3s on Hetzner VPS for cost efficiency. 3 nodes (€15/node) + k3s = €45/month for a production-grade cluster.

Deployment Option 3: Serverless (Pay-Per-Use)

For MCP servers with variable or low traffic:

Vercel Functions

// api/mcp/[...path].ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

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

server.tool("search", { query: z.string() }, async ({ query }) => {
  const results = await searchIndex(query);
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
});

export default async function handler(req, res) {
  const transport = new StreamableHTTPServerTransport({});
  await server.connect(transport);
  await transport.handleRequest(req, res);
}

Cloudflare Workers

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

export default {
  async fetch(request, env) {
    const server = new McpServer({ name: "cf-mcp", version: "1.0" });
    
    server.tool("kv_get", { key: z.string() }, async ({ key }) => {
      const value = await env.MCP_KV.get(key);
      return { content: [{ type: "text", text: value || "not found" }] };
    });
    
    // Handle request
    return server.handleRequest(request);
  }
};

Pros: Zero idle cost, auto-scaling, global edge Cons: Cold starts (200-800ms), execution time limits (30s), statelessness

Best for: MCP servers that are mostly stateless and handle <10,000 requests/day

Production Architecture

A production MCP server deployment includes several components beyond the server itself:

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Load Balancer      β”‚
                    β”‚   (nginx / traefik)  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                β”‚                β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”
     β”‚ MCP Server β”‚  β”‚ MCP Server β”‚  β”‚ MCP Server β”‚
     β”‚   #1       β”‚  β”‚   #2       β”‚  β”‚   #3       β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
              β”‚                β”‚                β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                β”‚                β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”
     β”‚  Redis     β”‚  β”‚ Postgres   β”‚  β”‚ Vector DB  β”‚
     β”‚  (cache)   β”‚  β”‚ (state)    β”‚  β”‚ (embeddingsβ”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Redis (Caching + Rate Limiting)

import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

// Rate limiting
async function checkRateLimit(clientId: string, limit: number = 100) {
  const key = `ratelimit:${clientId}:${Math.floor(Date.now() / 60000)}`;
  const current = await redis.incr(key);
  if (current === 1) await redis.expire(key, 60);
  return current <= limit;
}

// Response caching
async function cacheResponse(toolName: string, params: any, response: any, ttl: number = 300) {
  const key = `cache:${toolName}:${JSON.stringify(params)}`;
  await redis.setex(key, ttl, JSON.stringify(response));
}

Monitoring and Observability

Health Check Endpoint

app.get("/health", async (req, res) => {
  const health = {
    status: "healthy",
    uptime: process.uptime(),
    version: process.env.npm_package_version,
    checks: {
      database: await checkDB(),
      redis: await checkRedis(),
      llm_api: await checkLLMAPI()
    }
  };
  
  const allHealthy = Object.values(health.checks).every(c => c === "ok");
  res.status(allHealthy ? 200 : 503).json(health);
});

Prometheus Metrics

import { Counter, Histogram, register } from "prom-client";

const toolCalls = new Counter({
  name: "mcp_tool_calls_total",
  help: "Total MCP tool calls",
  labelNames: ["tool_name", "status"]
});

const toolLatency = new Histogram({
  name: "mcp_tool_latency_seconds",
  help: "Tool execution latency",
  labelNames: ["tool_name"],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 5, 10, 30]
});

const tokenCost = new Counter({
  name: "mcp_token_cost_eur_total",
  help: "Total token cost in EUR",
  labelNames: ["tool_name"]
});

Grafana Dashboard Essentials

  • Request rate (req/s) by tool
  • Error rate (%) by tool
  • P50, P95, P99 latency
  • Active connections
  • Token cost per hour/day
  • Cache hit rate
  • Queue depth (if async)

Security Hardening

Authentication

import jwt from "jsonwebtoken";

// JWT-based authentication
function authenticateRequest(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  
  if (!token) {
    return res.status(401).json({ error: "Missing auth token" });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.client = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: "Invalid token" });
  }
}

Input Validation

import { z } from "zod";

const searchSchema = z.object({
  query: z.string().min(1).max(500),
  limit: z.number().int().min(1).max(100).default(10),
  offset: z.number().int().min(0).default(0),
});

server.tool("search", searchSchema, async (params) => {
  // Params are validated by Zod schema
  const results = await searchIndex(params.query, params.limit, params.offset);
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
});

Cost Optimization

Tool-Level Cost Tracking

interface ToolMetrics {
  toolName: string;
  invocations: number;
  totalTokensIn: number;
  totalTokensOut: number;
  totalCostEUR: number;
  avgLatencyMs: number;
}

// Aggregate per-tool costs monthly
const monthlyReport = await db.query(`
  SELECT tool_name, 
         COUNT(*) as calls,
         SUM(input_tokens) as tokens_in,
         SUM(output_tokens) as tokens_out,
         SUM(cost_eur) as cost,
         AVG(latency_ms) as avg_latency
  FROM tool_metrics
  WHERE date_trunc('month', created_at) = date_trunc('month', NOW())
  GROUP BY tool_name
  ORDER BY cost DESC
`);

CDN for Static Resources

If your MCP server serves any static resources (schemas, documentation), use Cloudflare (free tier) to cache them at the edge, reducing server load.

Deployment Checklist

Before going to production:

  • TLS/SSL configured (Let's Encrypt or Cloudflare)
  • Authentication implemented (JWT or API keys)
  • Rate limiting active (per-client and global)
  • Input validation on all tools (Zod schemas)
  • Health check endpoint responds <100ms
  • Prometheus metrics exposed at /metrics
  • Grafana dashboard configured
  • Alerting rules set (error rate >5%, latency >5s)
  • Log aggregation (Loki, ELK, or CloudWatch)
  • Automated backups for stateful data
  • Cost tracking per tool invocation
  • Load tested to 2x expected peak traffic
  • Rollback procedure documented
  • Secrets in vault (not environment files)

Conclusion

MCP server hosting ranges from a €5 VPS to a multi-region Kubernetes fleet. The right choice depends on your traffic, reliability requirements, and budget. Start simple (single VPS with Docker Compose), add complexity only when needed, and always prioritize observability β€” you can't fix what you can't see.

For most creators, the optimal path is: VPS with Docker Compose β†’ Container orchestration with k3s β†’ Multi-region if needed. And publish your MCP server on SkillExchange to get distribution, billing, and discovery handled for you.

Newsletter

Enjoying this article?

Get weekly insights on building and selling AI skills, MCP tools, and creator economics. Join 2,000+ AI builders and creators.

No spam. Unsubscribe anytime.

Get the Free MCP Server Handbook

50+ pages of practical guides, code examples, and production-ready templates.

  • Complete MCP protocol reference
  • 15+ production-ready templates
  • Security best practices guide

No spam. Unsubscribe anytime. We respect your privacy.

Related Articles

Ready to try AI skills?

Browse the marketplace and discover skills for your AI agents.

Browse Skills