Deploying MCP Servers on Kubernetes A developer detailed how to deploy Model Context Protocol (MCP) servers on Kubernetes, arguing the key change is switching the server's transport from stdio to Streamable HTTP before containerization, since a stdio process inside a container has no listener for a Service to route to. The writeup covers separate liveness and readiness probes, a 45-second termination grace period, and credential handling via IRSA or Workload Identity, noting the July 2026 MCP spec removed protocol-level sessions so no session affinity is needed. Most MCP servers start life as a local, stdio-based process — the client spawns the server as a child process, messages flow over stdin and stdout. The instinct once that server needs to serve more than one client is to containerize it and expose a port. That instinct produces a container that starts, passes a naive health check, and then does nothing useful. A process reading from stdin and writing to stdout inside a container has no listener, no port, nothing for a Kubernetes Service to route traffic toward. The fix isn't a networking change — it's a transport change in the server itself, from stdio to Streamable HTTP, before Kubernetes enters the picture at all. The Dockerfile is unremarkable by design — the interesting decisions happen in the manifests, not the image. What matters is that the process binds to HTTP transport, not stdio, as its entrypoint: FROM node:22-slim AS builder WORKDIR /app COPY package .json ./ RUN npm ci --omit=dev COPY . . RUN npm run build FROM node:22-slim WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node modules ./node modules ENV MCP TRANSPORT=streamable-http ENV MCP HTTP ADDR=0.0.0.0:8080 ENV MCP HTTP ENDPOINT=/mcp EXPOSE 8080 USER node ENTRYPOINT "node", "dist/server.js" The three MCP variables mirror the flags production-grade MCP server implementations expose for choosing transport mode at startup. Transport, bind address, and endpoint path all need to be explicit and set at deploy time, not hardcoded into the binary. An MCP server mid-tool-call is not the same thing as a hung process. The probes need to say so explicitly: livenessProbe: httpGet: path: /health answers: is the process itself alive? port: 8080 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 3 ~45s of true unresponsiveness before restart readinessProbe: httpGet: path: /ready answers: can this pod accept new work right now? port: 8080 periodSeconds: 5 failureThreshold: 2 /health should do nothing more than confirm the process's event loop is running — never block on the status of an in-progress tool call. /ready reflects capacity: a pod mid-reasoning can report itself not-ready for new work without being treated as dead. Two other details in the Deployment matter: terminationGracePeriodSeconds: 45 the default 30s is a poor fit for a process that might be mid-tool-call during a rolling update , and resource requests set deliberately modest relative to limits — MCP servers spend most of their wall-clock time waiting on outbound I/O, not computing locally. apiVersion: v1 kind: Service metadata: name: mcp-server spec: selector: app: mcp-server ports: - port: 443 targetPort: 8080 name: https No session affinity. No cookie-based stickiness annotation. Nothing beyond ordinary load balancing. This is the direct payoff of the July 2026 MCP spec removing the protocol-level session — any request can land on any pod, so the Service stays genuinely simple. An MCP server calling AWS APIs needs credentials. The deployment-time decision that matters most is avoiding a long-lived static credential baked into the image or mounted as a Secret. IAM Roles for Service Accounts IRSA binds a Kubernetes ServiceAccount to an IAM role via OIDC federation: apiVersion: v1 kind: ServiceAccount metadata: name: mcp-server annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/mcp-server-role The IAM role should be scoped narrowly to whatever the server's tools actually need — an MCP server's tool surface is exactly the kind of thing that grows without anyone revisiting the IAM policy that backs it. Workload Identity is AKS's equivalent — it federates a Kubernetes ServiceAccount token with an Entra ID application: apiVersion: v1 kind: ServiceAccount metadata: name: mcp-server annotations: azure.workload.identity/client-id: "11111111-2222-3333-4444-555555555555" labels: azure.workload.identity/use: "true" The azure.workload.identity/use: "true" label must be present on both the ServiceAccount and the pod template — a pod missing that label silently falls back to no identity at all rather than failing loudly, which makes it a frustrating thing to debug after the fact. CPU utilization doesn't track I/O-bound tool-call concurrency well, but a deployment needs something running from day one: spec: minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleUp: stabilizationWindowSeconds: 30 scaleDown: stabilizationWindowSeconds: 300 The asymmetric stabilization windows are deliberate: scale up quickly 30s to protect against bursts of concurrent tool-calling tasks, scale down slowly 300s to avoid flapping. This will under-react to the specific load pattern MCP servers produce, but it's a safe default while a better signal is put in place. strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 maxUnavailable: 0 guarantees the rollout never reduces capacity below the current replica count — a new pod must pass its readiness probe before the corresponding old pod receives SIGTERM and begins the 45-second grace period. The difference between a rollout that's invisible to callers and one that produces a visible error-rate blip every time code ships. A request that exercises the actual transport confirms the deployment is correctly speaking the current specification: curl -s -X POST https://mcp.example.com/mcp \ -H "Content-Type: application/json" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: mcp-server" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' A correctly deployed 2026-07-28-compliant server returns the tool list with no Mcp-Session-Id in the response headers and no initialize call required beforehand. If a session header shows up, the server or a proxy in front of it is still running the older stateful transport path. Worth catching in verification rather than discovering under load — and worth running as a smoke test on every deploy, not just at initial deployment. The summary covers the core patterns. The full article goes deeper on: maxUnavailable: 0 is the right default for this workload class