Enterprise·
advanced
·15 min read·Sep 12, 2026·Updated Oct 24, 2026
·Lead AI Systems Architect·Verification Charter

Deploying Remote MCP Servers on Kubernetes at Scale

Production guide to deploying containerized remote Model Context Protocol (MCP) servers on Kubernetes with Helm, JSON-RPC queue-based HPA, and secure Ingress.

kuberneteshelmmcpautoscalingcontainersdevops
Config Target: claude_desktop_config.json
{
  "mcpServers": {
    "kubernetes": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-server-kubernetes"
      ]
    }
  }
}
Interactive Tool
1-Click Export

Generate & Test Kubernetes MCP Server Config

One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.

Open in Generator

Deploying Remote MCP Servers on Kubernetes at Scale

Moving Model Context Protocol (MCP) servers from local developer workstations running over stdio to shared enterprise Kubernetes clusters introduces unique orchestration challenges. Unlike standard stateless REST APIs, remote MCP servers communicate over long-lived Server-Sent Events (SSE) connections, process computationally expensive tool executions, and maintain in-memory protocol state during complex multi-step agent reasoning loops.

Deploying remote MCP servers in mission-critical environments requires declarative Helm packaging, hardened container security contexts, zero-downtime rolling updates with connection draining, and custom Horizontal Pod Autoscaling (HPA) based on active JSON-RPC queue depth rather than generic CPU or memory utilization.

This guide provides a production-tested blueprint for architecting, packaging, and operating remote MCP clusters on Kubernetes.


1. Architectural Topology: Remote MCP on Kubernetes

In a Kubernetes environment, client agents (such as Cursor, Windsurf, or autonomous agent clusters) connect via Ingress controllers that route streaming SSE traffic to backend pods. Each MCP server pod exposes an HTTP server terminating SSE streams and accepting POST requests for JSON-RPC 2.0 tool execution.

diagram

Kubernetes Namespace: ai-infrastructure

HTTPS Streamable SSE

Sticky Session / Session Affinity

Scrape JSON-RPC Queue Depth

Scale In / Out

Read Replica Pool

Read Replica Pool

Read Replica Pool

AI Client / Host IDE

Ingress Controller: Traefik / NGINX

mcp-database-service

MCP Pod 1: Tool Worker

MCP Pod 2: Tool Worker

MCP Pod N: Tool Worker

Prometheus Exporter

Custom Metrics HPA

Enterprise Database

Core Operational Requirements

  • ▸Sticky Sessions: Because MCP SSE connections decouple the downstream event stream (GET /sse) from the upstream tool execution requests (POST /messages?sessionId=...), the Ingress must enforce sticky sessions via HTTP cookies or routing headers.
  • ▸Non-Blocking Streaming: Upstream Ingress controllers must disable response buffering to prevent JSON-RPC chunks from being held until buffer flush.
  • ▸Process Sandboxing: Pods must run as non-root users with read-only root filesystems, preventing compromised tools from modifying container binaries.

2. Remote Transport Wire Protocol: SSE Handshake

Remote MCP servers establish communication through the Server-Sent Events transport. Understanding the exact wire payloads is essential for configuring Kubernetes readiness probes and debugging proxy dropouts.

Step 1: Initial SSE Connection Request (GET /sse)

The client opens a persistent SSE connection:

http
GET /sse HTTP/1.1
Host: mcp-k8s.internal.example.com
Authorization: Bearer <JWT_TOKEN>
Accept: text/event-stream
Cache-Control: no-cache

Step 2: Server Session Assignment

The remote MCP server immediately emits an endpoint event containing a unique session identifier and URI where subsequent JSON-RPC messages must be sent:

http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

event: endpoint
data: /messages?sessionId=k8s-pod-9c8e1-4b72-91ef

Step 3: Tool Execution Post Request (POST /messages)

The client delivers JSON-RPC requests to the assigned endpoint:

http
POST /messages?sessionId=k8s-pod-9c8e1-4b72-91ef HTTP/1.1
Host: mcp-k8s.internal.example.com
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "req-9812",
  "method": "tools/call",
  "params": {
    "name": "cluster_inspect_pod_logs",
    "arguments": {
      "namespace": "production",
      "pod_name": "checkout-service-7bb8",
      "lines": 50
    }
  }
}

Step 4: Streamed JSON-RPC Response

The server pushes the execution result back down the established SSE stream:

http
event: message
data: {"jsonrpc":"2.0","id":"req-9812","result":{"content":[{"type":"text","text":"[2026-09-12 18:22:01] INFO Connection pool initialized\n[2026-09-12 18:22:04] ERROR Connection timeout: payment-gateway:443"}]}}

3. Production Helm Chart Architecture

Below is a production-grade Helm chart layout for deploying remote MCP servers with hardened security profiles, probes, and resource bounds.

values.yaml

yaml
# Helm values for enterprise-mcp-server
replicaCount: 3

image:
  repository: registry.enterprise.internal/ai-infra/mcp-database-server
  pullPolicy: IfNotPresent
  tag: "1.4.2"

imagePullSecrets:
  - name: enterprise-registry-creds

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  runAsGroup: 10001
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

resources:
  limits:
    cpu: "2"
    memory: "2Gi"
  requests:
    cpu: "500m"
    memory: "512Mi"

env:
  NODE_ENV: "production"
  MCP_PORT: "8080"
  MAX_CONCURRENT_SESSIONS: "100"
  STATEMENT_TIMEOUT_MS: "5000"

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-enterprise"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "MCP_K8S_ROUTE"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
  hosts:
    - host: mcp-database.ai.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: mcp-database-tls
      hosts:
        - mcp-database.ai.example.com

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 25
  targetQueueDepth: 15

templates/deployment.yaml

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  labels:
    app.kubernetes.io/name: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Release.Name }}
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      securityContext:
        fsGroup: 10001
      containers:
        - name: mcp-server
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          ports:
            - name: http-mcp
              containerPort: 8080
              protocol: TCP
          env:
            {{- range $key, $val := .Values.env }}
            - name: {{ $key }}
              value: {{ $val | quote }}
            {{- end }}
          livenessProbe:
            httpGet:
              path: /healthz
              port: http-mcp
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: http-mcp
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - name: tmp-volume
              mountPath: /tmp
      volumes:
        - name: tmp-volume
          emptyDir: {}

4. Autoscaling on Active JSON-RPC Queue Depth

Standard CPU and memory metrics fail to reflect MCP server workload spikes. An MCP server awaiting a complex 30-second database query or vector search consumes negligible CPU while its thread pool and connection budget are fully saturated.

To solve this, instrument the MCP server with a Prometheus metric tracking active pending tool invocations: mcp_pending_tool_invocations.

Prometheus Instrumentation in TypeScript Server

typescript
import express from 'express';
import { Counter, Gauge, collectDefaultMetrics, Registry } from 'prom-client';

const register = new Registry();
collectDefaultMetrics({ register });

export const pendingToolGauge = new Gauge({
  name: 'mcp_pending_tool_invocations',
  help: 'Number of active JSON-RPC tool executions currently in flight.',
  labelNames: ['tool_name'],
  registers: [register]
});

export const toolInvocationDuration = new Counter({
  name: 'mcp_tool_execution_total',
  help: 'Total count of executed tools',
  labelNames: ['tool_name', 'status'],
  registers: [register]
});

const app = express();

// Expose Prometheus endpoint for Kubernetes scraping
app.get('/metrics', async (req, res) => {
  res.setHeader('Content-Type', register.contentType);
  res.send(await register.metrics());
});

Kubernetes HorizontalPodAutoscaler with Custom Metric

Using the Prometheus Adapter (custom.metrics.k8s.io), scale the deployment when the average pending tool queue per pod exceeds 15 concurrent calls:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mcp-database-hpa
  namespace: ai-infrastructure
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mcp-database-server
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Pods
      pods:
        metric:
          name: mcp_pending_tool_invocations
        target:
          type: AverageValue
          averageValue: "15"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300 # Prevent thrashing during LLM tool pauses
      policies:
        - type: Percent
          value: 20
          periodSeconds: 60

5. Graceful Connection Draining for SSE Streams

Because SSE connections are persistent, terminating a pod during a Kubernetes deployment rollout immediately severs active client agent sessions. Implement a preStop lifecycle hook and SIGTERM handler that allows in-flight tool calls to finish while signaling the client to reconnect.

yaml
# Inside Deployment.spec.template.spec.containers[0]
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10 && touch /tmp/draining"]

In the MCP server application logic:

typescript
let isShuttingDown = false;

process.on('SIGTERM', () => {
  console.log('SIGTERM received: initiating MCP graceful shutdown...');
  isShuttingDown = true;

  // 1. Notify connected SSE clients of server relocation
  for (const session of activeSseSessions.values()) {
    session.res.write(`event: notification\ndata: {"jsonrpc":"2.0","method":"notifications/server_draining","params":{"reconnect_seconds":3}}\n\n`);
  }

  // 2. Wait for active in-flight tool calls to resolve
  const drainInterval = setInterval(() => {
    if (activeToolExecutionsCount === 0) {
      clearInterval(drainInterval);
      server.close(() => {
        console.log('All MCP sessions drained. Process exiting cleanly.');
        process.exit(0);
      });
    }
  }, 500);
});

6. Verification and Health Check Runbook

Once deployed, verify cluster readiness and session routing using curl:

bash
# 1. Establish SSE Connection and verify session assignment
curl -i -N -H "Accept: text/event-stream" \
  https://mcp-database.ai.example.com/sse

# Expected Response:
# HTTP/1.1 200 OK
# Content-Type: text/event-stream
# Set-Cookie: MCP_K8S_ROUTE=...
# event: endpoint
# data: /messages?sessionId=550e8400-e29b-41d4-a716-446655440000

# 2. Test JSON-RPC Tool Invocation over HTTP POST
curl -X POST -H "Content-Type: application/json" \
  -b "MCP_K8S_ROUTE=..." \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "health_check",
      "arguments": {}
    }
  }' \
  https://mcp-database.ai.example.com/messages?sessionId=550e8400-e29b-41d4-a716-446655440000

Related Kubernetes & Enterprise Guides

Upcoming Feature

MCP Developer Newsletter (In Planning)

We are preparing an occasional technical dispatch covering new servers, benchmarks, and protocol updates. Sign up early to receive issues when we launch.

Zero marketing spam. 1-click unsubscribe anytime.

Ready to Deploy?

Build your full agent toolstack in the Visual Generator

Combine Kubernetes with databases, search APIs, and memory graphs in a single configuration file.

Customize in Generator
Developer Verification & Feedback
0 reports

Did this setup guide work with your AI host?

Help test this configuration across Claude, Cursor, and Codex CLI. Be the first to report:

RT✓

Written by Rad Tome

Lead AI Systems Architect & Maintainer, MCP Codex

Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.

Editorial Integrity: Package names verified via npm registry API. Repositories verified via GitHub REST API.
Verification Charter →

Related Guides