teachyou.ai academy
← All posts
MCPmcp-gatewayai-securityenterprise-aiaudit-logging

MCP Gateways for the Enterprise: Routing, Auth and Audit

Pramod Dutta · Jul 7, 2026 · 20 min read

An MCP gateway is a protocol-aware control point that sits between every AI client in your organization (Claude Desktop, Claude Code, IDE agents, internal copilots) and the MCP servers those clients call, giving you one place to route requests, enforce authentication and authorization, and keep an audit trail of every tool invocation. If teams across your company are already wiring Model Context Protocol servers into their editors and agents, the question has moved past "does MCP work" to "who called which tool, with whose credentials, and can we prove it in an incident review". This guide covers how an enterprise MCP gateway handles routing and virtual servers, how the MCP authorization spec maps onto your identity provider, what a defensible audit log looks like, and which gateways are worth evaluating in 2026, including a minimal runnable gateway skeleton so the moving parts stop being abstract.

Why You Need an MCP Gateway Once Servers Multiply

MCP went from an Anthropic announcement in late 2024 to the default integration layer for agents by the end of 2025, with OpenAI, Google and Microsoft all adopting it alongside Anthropic. Every MCP server speaks JSON-RPC 2.0 over one of two transports: stdio for local processes, or streamable HTTP for remote servers. That uniformity is exactly why sprawl happens so fast. Adding a new capability to an agent is a five-line config edit, so engineers add them constantly.

The direct wiring model looks like this: each developer machine has a claude_desktop_config.json, a .mcp.json, a Cursor config and maybe a VS Code config, each listing servers with credentials inlined as environment variables. Multiply clients by servers and you get an N x M mesh of connections, each one invisible to the platform team. The concrete failure modes:

  • Secrets on laptops. GitHub PATs, database connection strings and Jira API keys sit in plaintext JSON in home directories, copied between machines over Slack.
  • No inventory. Anyone can npx an MCP server published yesterday by an unknown author and hand it filesystem access. Nobody knows which servers are in use until something breaks.
  • Offboarding pain. When someone leaves, you rotate every credential that ever touched their laptop, if you can even enumerate them.
  • Zero central logs. If an agent deleted rows in production, your only forensic evidence is whatever that one MCP server printed to stderr on someone's machine.
  • Version drift. A server the security team reviewed in January silently updates in March with new tool descriptions, and no review fires.

A gateway collapses the N x M mesh to N + M. Clients hold exactly one endpoint and one credential (their SSO identity). Servers register once, behind the gateway, with their secrets held server-side. Everything in between becomes observable and enforceable.

What an Enterprise MCP Gateway Actually Does

An MCP gateway is not a generic reverse proxy with a new name. It terminates the MCP protocol on both sides, which means it can make decisions at the level that matters: the individual tool call. The core jobs:

  1. Routing and aggregation. Map many upstream MCP servers onto one client-facing endpoint, including composing "virtual servers" that expose a curated subset of tools from several upstreams.
  2. Authentication. Act as the OAuth 2.1 resource server the MCP spec expects, delegating login to your IdP (Okta, Entra ID, Keycloak) so agents authenticate the same way humans do.
  3. Authorization. Enforce per-user, per-tool policy: which roles may even see a tool in tools/list, and which may execute it via tools/call.
  4. Credential brokering. Hold upstream secrets in a vault and inject them per request, so no API key ever lands in a client config again.
  5. Audit and observability. Emit a structured record of every session, every tool listing served, and every tool call with its decision, latency and outcome.

Protocol awareness is what separates a real MCP gateway from an HTTP proxy with path rules. The gateway has to handle the initialize handshake and version negotiation, manage Mcp-Session-Id headers, stream Server-Sent Events responses without buffering them to death, propagate notifications/tools/list_changed correctly after it has filtered the tool list, and decide what to do with server-initiated features like elicitation (a server asking the user for input mid-call) and sampling (a server asking the client's model to complete something). It also needs to know that JSON-RPC batching was removed in the 2025-06-18 revision of the spec, and that servers must validate Origin headers to prevent DNS rebinding attacks against localhost deployments. Generic L7 routing gets none of this right by default.

Routing: Virtual Servers, Namespacing and Sessions

The simplest routing scheme is path-based: the gateway exposes /mcp/github, /mcp/postgres, /mcp/jira, and forwards each to the registered upstream. That alone is a big win, but mature gateways go further with virtual servers: a named bundle like support-agent that pulls search_issues from the GitHub server, get_ticket from the Zendesk server and query_readonly from the Postgres server, and presents them as one MCP server. This matters because models degrade when you dump hundreds of tools into context. Curating a few dozen tools per virtual server keeps both the model sharp and the blast radius small.

Aggregation forces two mechanical decisions. First, namespacing: two upstreams can both export a search tool, so gateways prefix tool names (github.search_issues or github_search_issues) and rewrite them transparently in both directions. Second, list filtering: if a user's role hides a tool, the gateway must strip it from tools/list responses, not just block it at call time. A model that can see a tool will eventually try to call it, and a pile of policy-denied errors in your logs is noise you do not want. When an upstream emits notifications/tools/list_changed, the gateway re-applies the filter before propagating the notification.

Sessions are the part that surprises teams coming from stateless REST. Streamable HTTP servers may assign an Mcp-Session-Id header during initialize, and every subsequent request must carry it. If the upstream holds session state in memory and you run several replicas behind the gateway, requests must stick to the replica that owns the session. That means consistent hashing on the session header, or upstreams that externalize state to Redis. Microsoft's open source MCP Gateway project exists mostly to solve exactly this session-aware routing problem on Kubernetes. Resumability matters too: SSE streams can drop mid-tool-call, and the spec allows clients to reconnect with Last-Event-ID, so the gateway should pass those through rather than eat them.

Transport bridging is the final routing job. Plenty of useful MCP servers are stdio-only. The enterprise pattern is to run each one in a container and front it with a stdio-to-HTTP bridge, either natively (Docker's MCP Gateway does this for its whole catalog) or with a small adapter like supergateway. From the client's point of view, everything behind the gateway is just streamable HTTP.

Talking to a gateway looks like talking to any MCP server. Here is the handshake through a gateway endpoint, with response headers dumped so you can see the session id:

curl -sS -D - https://mcp.corp.example.com/mcp/eng-default \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18",
        "capabilities":{},
        "clientInfo":{"name":"curl-probe","version":"1.0"}}}'

Subsequent requests echo the returned Mcp-Session-Id header and add MCP-Protocol-Version: 2025-06-18. Client rollout is equally boring, which is the point. One entry replaces a dozen:

{
  "mcpServers": {
    "corp": {
      "type": "http",
      "url": "https://mcp.corp.example.com/mcp/eng-default"
    }
  }
}

Or for Claude Code users, a one-liner you can put in an onboarding script:

claude mcp add --transport http corp https://mcp.corp.example.com/mcp/eng-default

Auth for MCP Gateways: OAuth 2.1 and Resource Metadata

Authorization is where MCP grew up fastest. The 2025-03-26 spec revision introduced OAuth 2.1 as the authorization framework for HTTP transports. The 2025-06-18 revision fixed the architecture: an MCP server is an OAuth 2.0 resource server, not an authorization server. It advertises where tokens come from via Protected Resource Metadata (RFC 9728), clients discover the authorization server from that metadata (RFC 8414), register dynamically if supported (RFC 7591), and must use Resource Indicators (RFC 8707) so every token is bound to the specific server it was minted for. The same revision states plainly that servers must not accept tokens that were not issued for them, which kills the lazy pattern of passing a client's token straight through to a downstream API.

This design is a gift to gateway builders. Make the gateway the resource server, your IdP the authorization server, and the whole flow becomes standard enterprise SSO. An unauthenticated request gets:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata=
  "https://mcp.corp.example.com/.well-known/oauth-protected-resource"

And the metadata document tells the client everything it needs:

{
  "resource": "https://mcp.corp.example.com/mcp",
  "authorization_servers": ["https://auth.corp.example.com"],
  "scopes_supported": ["mcp:use"],
  "bearer_methods_supported": ["header"]
}

The client runs an authorization code flow with PKCE against your IdP (a browser pops for the human behind the agent), comes back with an access token audience-bound to the gateway, and every request thereafter carries it. The gateway validates issuer, audience, expiry and signature against the IdP's JWKS, then maps the user's groups onto tool policy.

Keep scopes coarse at the IdP (something like mcp:use) and do fine-grained authorization in the gateway's policy engine, where you can reason about tool names and even arguments. A small OPA policy (Rego, OPA 1.x syntax) makes the shape clear:

package mcp.authz

default allow := false

allow if input.method != "tools/call"

allow if {
  input.method == "tools/call"
  input.tool in data.roles[input.role].tools
}

With role data like:

{
  "roles": {
    "support": {
      "tools": ["github.search_issues", "zendesk.get_ticket"]
    },
    "eng": {
      "tools": ["github.search_issues", "github.create_pull_request",
                "postgres.query_readonly"]
    }
  }
}

Downstream of the gateway, three credential patterns cover almost everything:

  • Vault injection. For API-key upstreams (most of them), the gateway pulls the key from Vault, AWS Secrets Manager or similar at request time and injects it. Laptops never see it, rotation happens in one place.
  • Token exchange. For upstreams that need to act as the end user (Google Drive, Microsoft Graph), the gateway exchanges the inbound token for a downstream token via RFC 8693, preserving user identity without passing the original token through.
  • Scoped service accounts. For internal services, per-virtual-server service accounts with least privilege, so the blast radius of a compromised bundle is bounded.

Headless agents deserve their own paragraph because they are where auth shortcuts go to die. A CI agent or scheduled workflow has no human to click through PKCE, so it uses the client credentials grant with its own identity, ideally backed by workload identity (SPIFFE, cloud IAM roles) instead of long-lived secrets. Give every agent its own principal. One shared "agents" service account calling everything is how you end up unable to answer the only question that matters after an incident: which agent did this.

Audit: Logging Every Tool Call Without Leaking Secrets

Auth decides what can happen; audit proves what did happen. The unit of audit for MCP is the JSON-RPC message, and the events worth recording are: session establishment (who, from which client, negotiated which protocol version), every tools/list response actually served to that user (what the model could see), every tools/call with its policy decision, and every change in upstream tool definitions.

A workable audit record, one JSON line per event:

{"ts":"2026-07-09T08:14:22Z","user":"asha@corp.example.com",
 "client":"claude-code/2.1","server":"github",
 "method":"tools/call","tool":"github.create_pull_request",
 "args_sha256":"9f2c41d0a8...","decision":"allow",
 "policy":"eng-write-v3","latency_ms":412,"is_error":false,
 "result_bytes":2113,"session":"b7e3aa10","rpc_id":42,
 "trace":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}

Design choices that matter in practice:

  • Hash arguments by default, store payloads selectively. Tool arguments routinely contain customer data, file contents and query strings. Log a digest plus a redacted preview to the long-retention stream, and if you need full payloads for high-risk tools, write them to a separate restricted store with short retention.
  • Log what the model saw, not just what it did. Serving tools/list snapshots and hashing each tool's name, description and input schema gives you the evidence trail for tool-tampering investigations.
  • Record the decision and the policy version. "Denied by eng-write-v3" is auditable; a bare 403 is not.
  • Propagate trace context. Accept and forward traceparent so a tool call in the gateway joins the same distributed trace as the agent framework's model spans. Map fields onto the OpenTelemetry GenAI semantic conventions where they fit.
  • Make it tamper-resistant. Ship to your SIEM (Splunk, Datadog, Sentinel) in near real time and mirror to an object-lock bucket. Gateway hosts should not be able to rewrite history.

Then wire alerts to the questions you already know an incident review will ask: spikes in denied calls per user, first-time use of a destructive tool, tool definition drift on any approved server, a new upstream appearing outside change management, and any session where result sizes suggest bulk data movement.

Security Controls an MCP Gateway Should Enforce

The MCP threat model earned its scars in 2025. Tool poisoning, first demonstrated publicly by security researchers that spring, hides instructions in a tool's description so the model exfiltrates data while appearing to do its job. The rug pull is its time-delayed variant: a server ships clean descriptions, passes review, then swaps them later. A gateway is the natural chokepoint for both: pin the hash of every approved tool definition, serve only pinned definitions to clients, and turn any upstream drift into an alert and a block rather than a silent update.

Prompt injection through tool results is harder. Researchers showed that an agent with access to the official GitHub MCP server could be steered by a hostile public issue into leaking private repository data, and the pattern generalizes to any tool that reads attacker-controllable content. A gateway cannot fully solve this (it is fundamentally a model-behavior problem), but it can strip invisible Unicode and terminal escape sequences from results, run content scanners on responses from untrusted servers, and enforce egress rules so a poisoned agent has no unreviewed tool through which to leak.

The rest of the checklist is classic least privilege applied to a new protocol:

  • Audience validation everywhere. Enforce the spec's ban on token passthrough; the gateway mints or exchanges credentials downstream, never forwards inbound tokens.
  • Rate limits and budgets. Per-user, per-tool call quotas, plus concurrency caps on expensive tools. An agent in a retry loop against a paid API is a cost incident waiting to happen.
  • Approval gates for destructive tools. Anything that writes, deletes, spends or emails can require human confirmation, either via MCP elicitation passed through to the client or an out-of-band approval queue.
  • Supply chain discipline. Source servers from the official MCP Registry (in preview since September 2025) or an internal mirror of it, pin container image digests for self-hosted servers, and rescan on every version bump. SaaS vendors have already shipped MCP endpoints with cross-tenant authorization bugs, so treat "official vendor server" as reviewable, not trusted.

A Minimal MCP Gateway You Can Run

Nothing demystifies gateway architecture like fifty lines of it. This skeleton does path routing, tool-level authorization and JSONL audit logging for streamable HTTP upstreams. Save as gateway.mjs, run npm install express, then node gateway.mjs (Node 20 or newer):

import express from "express";

const UPSTREAMS = {
  github: "http://127.0.0.1:9001/mcp",
  postgres: "http://127.0.0.1:9002/mcp",
};

const POLICY = {
  eng: { github: ["*"], postgres: ["query_readonly"] },
  support: { github: ["search_issues"], postgres: [] },
};

const app = express();
app.use(express.json({ limit: "4mb" }));

// Demo only: derive role from a header. Production validates a JWT
// (issuer, audience, expiry) against your IdP's JWKS, e.g. with jose.
const roleOf = (req) => req.headers["x-debug-role"] || "support";

app.post("/mcp/:server", async (req, res) => {
  const upstream = UPSTREAMS[req.params.server];
  if (!upstream) return res.status(404).json({ error: "unknown server" });

  const body = req.body;
  const role = roleOf(req);

  if (body.method === "tools/call") {
    const tool = body.params?.name;
    const allowed = POLICY[role]?.[req.params.server] ?? [];
    const ok = allowed.includes("*") || allowed.includes(tool);
    console.log(JSON.stringify({
      ts: new Date().toISOString(), role, server: req.params.server,
      tool, decision: ok ? "allow" : "deny", rpc_id: body.id,
    }));
    if (!ok) {
      return res.json({ jsonrpc: "2.0", id: body.id, error:
        { code: -32001, message: "Tool blocked by gateway policy" } });
    }
  }

  const r = await fetch(upstream, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      accept: "application/json, text/event-stream",
      "mcp-session-id": req.headers["mcp-session-id"] ?? "",
    },
    body: JSON.stringify(body),
  });
  res.status(r.status);
  res.set("content-type", r.headers.get("content-type") ?? "application/json");
  const sid = r.headers.get("mcp-session-id");
  if (sid) res.set("mcp-session-id", sid);
  res.send(Buffer.from(await r.arrayBuffer()));
});

app.listen(8080, () => console.log("mcp gateway on :8080"));

Exercise the policy without any upstream by watching the audit line and the denial:

curl -s http://localhost:8080/mcp/postgres \
  -H "Content-Type: application/json" \
  -H "x-debug-role: support" \
  -d '{"jsonrpc":"2.0","id":7,"method":"tools/call",
       "params":{"name":"query_readonly","arguments":{"sql":"select 1"}}}'

Be clear about what this skeleton omits, because the omissions are the product: it buffers responses instead of streaming SSE through, it does no real token validation, no tools/list filtering, no session affinity across replicas, no retries or circuit breaking, no vault integration. Those gaps are exactly what you are paying for (in money or operational effort) when you adopt a real gateway. Returning a JSON-RPC error for denials is also a design choice worth revisiting: some gateways instead return a normal tools/call result with isError: true and a human-readable message, so the model can read the denial and adjust instead of retrying blindly.

The MCP Gateway Landscape in 2026

The build-vs-buy conversation has real options on both sides now. Names worth shortlisting, grouped by lineage:

  • Open source, purpose-built. IBM's ContextForge MCP Gateway (Python; federation, virtual servers, an admin UI, and REST-to-MCP conversion), Docker MCP Gateway (container-native, pairs with the Docker MCP Catalog and its secrets handling, good developer-laptop story), Lasso Security's mcp-gateway (security-plugin oriented, PII masking), agentgateway (Rust data plane built for agent traffic, speaks MCP and A2A, ties into the kgateway/Kubernetes ecosystem), and Microsoft's MCP Gateway (session-aware routing on Kubernetes).
  • API gateway vendors extending down. Kong, Traefik, Tyk and friends now route and observe MCP traffic; Azure API Management can expose existing REST APIs as MCP servers and gate remote ones; Amazon Bedrock AgentCore Gateway converts Lambda functions and OpenAPI services into MCP tools; Cloudflare gives you remote MCP hosting with an OAuth provider library; Envoy-based AI gateways have added native MCP routing.
  • Managed MCP platforms. MintMCP, Lunar.dev's MCPX, Obot and similar products sell the gateway, catalog and audit story as a service, which is a sane starting point if you have no platform team to spare.

Evaluation criteria that actually separate them, from running these conversations repeatedly: protocol currency (streamable HTTP plus sane handling of sessions, elicitation and list-changed notifications, with spec revisions tracked promptly), depth of IdP integration (real OIDC group mapping, not a static API key list), policy granularity (tool-level minimum, argument-level ideally), audit export formats your SIEM ingests without a custom parser, secret vaulting, stdio server wrapping, multi-tenancy, and honest latency numbers under streaming load. Measure that last one yourself; a gateway that buffers SSE will feel broken in interactive clients regardless of what the datasheet says.

Rolling Out an MCP Gateway Without Breaking Teams

A rollout order that has worked, phrased as one step per sprint rather than a big bang:

  1. Inventory. Grep managed laptops for mcpServers blocks in known config paths and scan egress logs for /mcp endpoints. Expect surprises.
  2. Stand up the gateway with SSO in front of two or three high-value, read-only servers (GitHub search, docs search, read-only SQL). Read-only first buys trust cheaply.
  3. Move secrets server-side. Register upstream credentials in the vault, then revoke every laptop-resident key you found in step 1.
  4. Write tool policy as code. Default-deny writes, explicit role grants, approval gates on destructive tools. Review policy in pull requests like any other code.
  5. Repoint clients. Publish the one-entry client config and the internal catalog page. Make the gateway path easier than the ad hoc path, or people will route around you.
  6. Turn on audit export and alerts. SIEM stream, object-lock archive, alerts on denials, drift and first-use of dangerous tools.
  7. Close the side doors. Once adoption is real, block direct egress to known MCP endpoints from managed devices so the gateway is the only path.
  8. Operate it. Latency SLOs, quarterly access reviews, failover tests for session handling, and a standing review for newly requested servers.

The pattern is the same one enterprises ran for web APIs fifteen years ago, compressed into a much shorter timeline because agents touch more sensitive systems with less human review per action. An MCP gateway does not make agents safe by itself, but it is the piece of infrastructure that makes every other safety measure (identity, least privilege, monitoring, incident response) possible to apply to agents at all.

FAQ

Is an MCP gateway just an API gateway with a new name?

No. An API gateway routes on URLs, methods and headers; an MCP gateway terminates the MCP protocol itself: the initialize handshake, session headers, SSE streaming, tool list filtering and per-tool-call authorization inside JSON-RPC bodies. You can build MCP awareness onto an existing API gateway (several vendors have), but URL-level rules alone cannot express "support agents may call search_issues but not create_pull_request".

Does the MCP specification require a gateway?

No. The spec defines clients, servers and an OAuth 2.1 authorization model, and it works fine point-to-point. A gateway is an enterprise deployment pattern that happens to fit the spec well: because the 2025-06-18 revision made servers plain OAuth resource servers with discoverable metadata, a gateway can stand in as the resource server for a whole fleet without breaking conformant clients.

How does an MCP gateway handle stdio-only servers?

By running them server-side and bridging transports. The common pattern is one container per stdio server with a stdio-to-streamable-HTTP adapter in front, either built into the gateway (Docker MCP Gateway does this natively) or via a small bridge like supergateway. Clients then see an ordinary HTTP endpoint, and the server process runs in infrastructure you patch and monitor instead of on laptops.

What latency does a gateway add to tool calls?

One network hop plus policy evaluation, which is typically small relative to what the tool itself does (an API call, a database query) and tiny relative to model inference between tool calls. The real latency risk is a gateway that buffers streaming responses instead of piping SSE through; test that specifically during evaluation with a long-running tool.

How do agents authenticate without a human in the loop?

Interactive clients use the OAuth authorization code flow with PKCE, with a browser step for the human. Headless agents use the client credentials grant with their own identity, preferably backed by workload identity rather than a static secret, and each agent gets its own principal so audit logs can distinguish them. What you should not do is share one long-lived token across a fleet of agents.

What changed in MCP authorization during 2025 that gateways rely on?

The 2025-03-26 revision adopted OAuth 2.1. The 2025-06-18 revision then separated roles cleanly: MCP servers became OAuth resource servers that publish RFC 9728 protected resource metadata, clients discover the authorization server from it and must bind tokens to a specific server using RFC 8707 resource indicators, and token passthrough to downstream services was explicitly prohibited. That combination is what lets a gateway centralize auth for many servers while staying spec-conformant.

Should we build our own MCP gateway?

Prototype one to learn the protocol (the skeleton above is an afternoon), but think hard before operating one in production. The undifferentiated heavy lifting (streaming passthrough, session affinity, vaulting, policy engines, audit pipelines) is exactly what the open source and commercial options listed above already do. Build only if you have unusual requirements, like custom data-loss-prevention on tool arguments, and even then consider building as plugins on an existing gateway rather than from scratch.