Running an MCP gateway on Kubernetes for AI agents

· September 9, 2026

mcpai-infrastructurekuberneteshelmsecurity
TL;DRDon't let agents connect straight to every MCP server. Put one gateway in front: it aggregates the tool catalog, holds the downstream credentials, authorizes every tool call, and writes one audit line per call. On Kubernetes that's a Deployment + Ingress for the gateway, ClusterIP-only MCP servers, and NetworkPolicy so nothing else can reach them.

Every team that starts building with AI agents hits the same wall around the third tool integration. The agent needs to read tickets, query a database, open a pull request, and check a dashboard. Each of those is an MCP server. Each one has its own credentials, its own deployment, its own idea of logging. Nobody can answer the question security asks first: which agent called which tool, with whose permissions, and what came back?

Our answer is a gateway. One endpoint the agents talk to, many MCP servers behind it, and the policy in the middle. This post is how we run that on Kubernetes.

What an MCP gateway actually does

The Model Context Protocol lets a client (an agent, an IDE, a chat app) discover and call tools exposed by a server. Out of the box, each client connects to each server directly. That’s fine on a laptop. In production it means:

A gateway sits between clients and servers and does four jobs:

  1. Aggregate. Clients connect once and see a merged tool catalog, namespaced by server (jira.search_issues, github.create_pr).
  2. Authenticate. The client proves who it is to the gateway. Downstream credentials stay inside the cluster.
  3. Authorize. Per-tool allow/deny rules, evaluated on every call, not just at connect time.
  4. Observe. Every tools/call becomes a structured log line and a trace span.

The shape on Kubernetes

agents ──HTTPS──▶ Ingress ──▶ mcp-gateway (Deployment, 2+ replicas)

                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
          mcp-jira (Svc)   mcp-github (Svc)   mcp-postgres (Svc)

Each MCP server is its own Deployment and ClusterIP Service. None of them are exposed outside the cluster. The gateway is the only thing with an Ingress.

We package the gateway and every server as Helm charts, versioned from CI, and let ArgoCD reconcile them. Adding a tool is a pull request that adds a chart and a route. Removing one is a revert.

Transport: use Streamable HTTP, and mind the session

MCP servers speak either stdio (local processes) or Streamable HTTP. Inside a cluster you want HTTP: servers become normal Services with health checks, autoscaling and network policy.

The detail that bites people: Streamable HTTP can be stateful. The server returns an Mcp-Session-Id header on initialization and the client sends it back on every request. If the gateway runs more than one replica and the session lives in memory, a request that lands on the wrong pod fails.

Two ways out:

We start with sticky routing and move to external state when a workload needs it.

Authentication: keep downstream secrets out of clients

The MCP authorization spec is built on OAuth 2.1. In practice we do this:

The result: rotate a GitHub key and no agent config changes. Revoke an agent and it loses every tool at once.

Authorization: per tool, per call

Connection-level auth isn’t enough, because one server usually exposes both harmless and dangerous tools. A GitHub server can list_pull_requests and delete_branch.

The gateway evaluates a policy on every tools/call:

roles:
  support-agent:
    allow:
      - jira.search_issues
      - jira.get_issue
      - github.list_pull_requests
  release-agent:
    allow:
      - github.*
    deny:
      - github.delete_*

Two rules we don’t bend:

Guardrails that belong in the gateway

Beyond allow/deny, the gateway is the natural place for:

Observability: log the call, not just the request

HTTP access logs tell you a POST happened. They don’t tell you which tool ran. We emit one structured event per tool call:

{
  "ts": "2026-10-07T13:02:11Z",
  "agent": "release-agent",
  "session": "b1c9…",
  "tool": "github.create_pull_request",
  "decision": "allow",
  "latency_ms": 412,
  "status": "ok",
  "result_bytes": 1830
}

Ship those to whatever you already use, and add OpenTelemetry spans that carry the session ID through gateway → server → downstream API. When someone asks “what did the agent do on Tuesday?”, the answer is a query, not an archaeology project.

We deliberately do not log full arguments and results by default. They can contain customer data. Log hashes and sizes, and turn on full capture per tool only when debugging.

Network policy: the last line

Even with a gateway, assume a server will eventually be misconfigured. Kubernetes NetworkPolicy makes the gateway the only thing allowed to reach MCP servers, and restricts each server’s egress to the one API it wraps:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mcp-servers-ingress
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/component: mcp-server
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: mcp-gateway

What we’d tell a team starting today

We do this for US teams.Senior nearshore engineers, US hours, AWS Partner, Vanta Partner.

Book a 30-minute architecture review