# What is an MCP gateway?

*Disclosure: I work at Solo.io, which makes agentgateway.*

> An MCP gateway sits between AI agents and MCP servers: one endpoint, sign-in, per-tool CEL rules and audit. How it differs from LLM and API gateways.

- Canonical URL: https://webofmike.com/what-is-an-mcp-gateway/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-26
- Last modified: 2026-10-26
- Tags: AI Gateways, MCP, Kubernetes, Platform Engineering, Security
- Cite as: Mike Moore, "What Is an MCP Gateway? agentgateway Examples", Web of Mike (webofmike.com), 2026-10-26. https://webofmike.com/what-is-an-mcp-gateway/


An MCP gateway is a proxy between AI clients and MCP servers, so agents reach many tool servers through one endpoint and every tool list and tool call passes one policy point. It signs users in, decides per tool who may see and call it, and logs each call, which is a different job from an LLM gateway (model calls) or an API gateway (ordinary HTTP APIs).

If you want the wider picture, with model and agent-to-agent traffic in the same gateway, see [what an AI agent gateway is](/what-is-an-ai-agent-gateway/).

Every config and output on this page comes from open source agentgateway v1.6.0, run on 2026-10-26: the standalone binary on a Linux box, and the v1.6.0 Helm charts on a k3s cluster. Nothing here needs Solo Enterprise.

## What an MCP gateway does

| Job | What it means | How agentgateway does it |
|---|---|---|
| One endpoint for many servers | Clients connect once and get one tool list | Several `targets` in one MCP backend ([virtual MCP](https://agentgateway.dev/docs/standalone/latest/integrations/mcp/servers/virtual/)) |
| Sign-in | The gateway acts as an OAuth resource server and turns away calls without a valid token | `mcpAuthentication` ([MCP authentication](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/mcp-authn/)) |
| Per-tool authorization | Rules decide who may call which tool | `mcpAuthorization` rules in CEL ([MCP authorization](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/mcp-authz/)) |
| Tool list filtering | Callers never see tools they cannot use | The same rules drop denied tools from `tools/list` |
| Per-target credentials and TLS | Each upstream server gets its own auth and TLS settings | `backendAuth` and `backendTLS` per target ([MCP target policies](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/mcp-target-policies/)) |
| Logs and metrics | Every call is recorded with the server and tool name | MCP access log fields and an MCP request counter ([MCP observability](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/mcp-observability/)) |
| Spec version translation | Clients and servers on different MCP versions can still talk | Translation between versions ([spec compatibility](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/spec-compatibility/)) |

## How it works, with agentgateway

### Standalone: one binary and a YAML file

The simplest form is the top-level `mcp` section. This is the two-target config I ran:

```yaml
mcp:
  port: 3000
  targets:
  - name: time
    stdio:
      cmd: uvx
      args: ["--with", "mcp<2", "mcp-server-time"]
  - name: everything
    stdio:
      cmd: npx
      args: ["-y", "@modelcontextprotocol/server-everything"]
```

`agentgateway -f config.yaml` starts it, and clients connect to `http://localhost:3000/mcp`. The `routes` section is the other way to write it when you need more control. Either way you can put every server behind one endpoint or give each its own path ([configuration modes](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/configuration-modes/)).

### Kubernetes: Gateway API and CRDs

On Kubernetes, the same proxy runs under a controller that reads Gateway API resources and agentgateway's own CRDs. An AgentgatewayBackend can pick MCP servers by label instead of by name. This is the one I applied:

```yaml
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
  name: mcp-fleet
  namespace: agentgateway-system
spec:
  mcp:
    targets:
    - name: fleet
      selector:
        namespaces:
          matchLabels: {kubernetes.io/metadata.name: mcp}
        services:
          matchLabels: {mcp-fleet: demo}
```

An HTTPRoute on the Gateway sends `/mcp` to that backend. Two Services in the `mcp` namespace carried the label, and the gateway listed 26 tools from both, each prefixed with its Service name and port, such as `everything-3001_echo` and `second-3001_echo`. The docs note that label selectors support streamable HTTP only. A server that speaks SSE needs a [static backend](https://agentgateway.dev/docs/kubernetes/latest/documentation/mcp/static-mcp/). The full walkthrough is [virtual MCP on Kubernetes](https://agentgateway.dev/docs/kubernetes/latest/documentation/mcp/virtual/).

## One endpoint, many servers

The client sees one MCP server. This is part of the `tools/list` from the standalone config above, which returned 15 tools, 2 from `time` and 13 from `everything`:

```text
time_get_current_time
time_convert_time
everything_echo
everything_get-env
everything_get-sum
```

With more than one target, agentgateway prefixes each tool with its target name, so two servers can both have an `echo` without a clash. Calls go back by prefix: `time_get_current_time` reached the time server and `everything_echo` came back with "Echo: hello". The `prefixMode` setting has three values: `conditional` (the default, which prefixes only when there is more than one target), `always` and `never`.

The underscore is the join character, which is why target names cannot contain one. I tried a target called `my_time` and v1.6.0 refused to start, saying "'+' and '_' are reserved MCP delimiters".

A target can also carry a CEL `condition` that decides whether that server joins the request at all. The v1.6 docs evaluate it before the gateway initializes or contacts the target. I put a condition on the time target that checks for an `x-include-time: true` header. A client without the header got 13 tools. A client with it got 15.

For one endpoint shared across customers, with each one seeing its own tools, see [multi-tenant MCP federation](/multi-tenant-mcp-federation/).

## Per-tool authorization with CEL

Authorization rules are CEL expressions. These are the rules from the v1.6 docs, with the second tool swapped for one the test server has:

```yaml
mcp:
  policies:
    mcpAuthorization:
      rules:
      - 'mcp.tool.name == "echo"'
      - 'jwt.sub == "test-user" && mcp.tool.name == "get-sum"'
```

I ran it with two signed tokens. The caller with `sub` set to `test-user` got `echo` and `get-sum` in `tools/list`. The caller with `sub` set to `other-user` got only `echo`. The blocked tool is not marked as off limits. It is gone from the list. When that second caller called `get-sum` anyway, the gateway answered HTTP 400 with JSON-RPC error -32602, "Unknown tool: get-sum". It does not confirm that the tool exists.

On Kubernetes the same idea is an AgentgatewayPolicy on the backend ([tool access](https://agentgateway.dev/docs/kubernetes/latest/documentation/mcp/tool-access/)):

```yaml
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
  name: mcp-tool-rule
  namespace: agentgateway-system
spec:
  targetRefs:
  - group: agentgateway.dev
    kind: AgentgatewayBackend
    name: mcp-fleet
  backend:
    mcp:
      authorization:
        action: Allow
        policy:
          matchExpressions:
          - 'mcp.tool.name == "echo"'
```

With it applied, the 26-tool list shrank to the two echo tools, and a call to `second-3001_get-sum` got the same "Unknown tool" error. The rule matched the tool name without the prefix. The docs also show expressions that mix a JWT claim with a tool name, such as `jwt.sub == "alice" && mcp.tool.name == "get_me"`.

There is one limit worth knowing. Per the v1.6 docs, `mcp.tool.arguments` is not set when `mcpAuthorization` rules run, so those rules cannot look at arguments. A route-level `authorization` policy can. I tested this deny rule:

```yaml
mcp:
  policies:
    authorization:
      rules:
      - deny: 'mcp.tool.name == "echo" && mcp.tool.arguments.message.contains("secret")'
```

`echo` with "hello" went through. `echo` with a message containing "secret" got HTTP 403, "authorization failed". The access log can record the arguments too: adding `tool_args: 'mcp.tool.arguments'` to the log fields wrote `tool_args={"message": "hello"}`. More traps are in [CEL authorization gotchas](/agentgateway-cel-authorization-gotchas/).

## Sign-in for MCP servers

`mcpAuthentication` makes the gateway an OAuth 2.0 resource server. It checks the JWT on each request, and when there is no valid token it answers 401 and points the client at its protected resource metadata. This is the docs' identity provider example, trimmed, which I ran against a local test realm:

```yaml
mcp:
  policies:
    mcpAuthentication:
      issuer: http://localhost:7080/realms/mcp
      audiences: ["http://localhost:3000/mcp"]
      jwks:
        url: http://localhost:7080/realms/mcp/protocol/openid-connect/certs
      provider:
        keycloak: {}
      resourceMetadata:
        resource: http://localhost:3000/mcp
        scopesSupported: [read:all]
        bearerMethodsSupported: [header]
```

The first request, with no token:

```text
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"

{"error":"unauthorized","error_description":"JWT token required"}
```

That URL returned the protected resource metadata, and the gateway also served the provider's authorization server metadata at `/.well-known/oauth-authorization-server/mcp`. That is how an MCP client finds where to sign in. I tested the 401 and both metadata documents, not a full browser sign-in. The `provider` field has adapters for six identity providers in v1.6 ([MCP authentication](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/mcp-authn/)).

Credentials from the gateway to each upstream server are a separate setting, `backendAuth`, set per target, so the agent never holds them. For the cross-app access flow, see [MCP OAuth and XAA](/mcp-oauth-xaa/).

## MCP gateway vs LLM gateway

An LLM gateway sits in front of model providers. It routes completions, holds provider keys, counts tokens and applies prompt guardrails. An MCP gateway sits in front of tool servers. It decides which tools a caller can discover and call, and records each tool call. They see different traffic.

| | LLM gateway | MCP gateway |
|---|---|---|
| Upstream | Model providers | MCP tool servers |
| Unit of policy | Model, key, token budget | Server, tool name, caller |
| Typical controls | Routing, fallback, cost limits, prompt guards | Tool list filtering, per-tool rules, sign-in |
| What it logs | Model, tokens, cost | Method, server, tool name |

You do not need two products. In agentgateway standalone, the `llm` and `mcp` sections can attach to one gateway. I ran both on port 3000: a `tools/call` at `/mcp` and a chat completion at `/v1/chat/completions` went through the same process. Setting the top-level `llm.port` and `mcp.port` fields both to 3000 does not work. v1.6.0 refuses and says to attach both to the same gateway instead. Cost controls on the model side are in [capping LLM spend per team](/llm-cost-controls-ai-gateway/).

## MCP gateway vs API gateway

An API gateway routes and protects HTTP APIs by host, path and method. MCP runs over HTTP too, but every call is a POST to the same path. The method (`tools/list`, `tools/call`) and the tool name sit inside the JSON-RPC body, and older spec versions add a session on top. A gateway that only reads paths sees one route. An MCP gateway parses the body, so it can filter a tool list and decide on each tool.

agentgateway is also a general HTTP and gRPC proxy ([introduction](https://agentgateway.dev/docs/standalone/latest/about/introduction/)), so the same binary can front ordinary APIs as well.

Spec versions matter here too. The 2026-07-28 MCP revision is stateless and sessionless. agentgateway translates between versions, and when it federates servers it offers the intersection of the versions every target supports ([spec compatibility](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/spec-compatibility/)).

## What to check in any MCP gateway

- Does `tools/list` change per caller, or does every caller see every tool? ([MCP authorization](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/mcp-authz/))
- Can a rule name one tool on one server, and does a denied call reveal whether the tool exists?
- Does it answer 401 with `WWW-Authenticate` and serve protected resource metadata? ([MCP authentication](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/mcp-authn/))
- Can each upstream server get its own credentials, so the agent never holds them? ([MCP target policies](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/mcp-target-policies/))
- What happens when two servers expose the same tool name? ([virtual MCP](https://agentgateway.dev/docs/standalone/latest/integrations/mcp/servers/virtual/))
- Which MCP spec versions does it support on each side? ([spec compatibility](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/spec-compatibility/))
- Do the logs and metrics carry the tool name? On v1.6.0 my access log lines carried `mcp.target=everything` and `gen_ai.tool.name=echo`, and the metrics endpoint counted `agentgateway_mcp_requests_total` with `method="tools/call"`, `server="time"` and `resource="get_current_time"` labels. ([MCP observability](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/mcp-observability/))
- Can you find the servers nobody put behind it? Those are [shadow MCP servers](/shadow-mcp-servers/).
- Can a server change a tool description after you approved it? See [why a name allowlist is not enough](/mcp-tool-poisoning-pin-definitions/) and [keeping server instructions out of the system prompt](/mcp-discovery-prompt-injection/).

When you are ready to compare gateways on the controls they enforce, [how to choose an AI agent gateway](/how-to-choose-an-ai-agent-gateway/) has the checklist and the tests.

Tested on agentgateway OSS v1.6.0 (git ea56086), standalone binary and v1.6.0 Helm charts, 2026-10-26. The outputs above are copied from those runs.

