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 .
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
) |
| Sign-in | The gateway acts as an OAuth resource server and turns away calls without a valid token | mcpAuthentication (MCP authentication
) |
| Per-tool authorization | Rules decide who may call which tool | mcpAuthorization rules in CEL (MCP authorization
) |
| 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
) |
| Logs and metrics | Every call is recorded with the server and tool name | MCP access log fields and an MCP request counter (MCP observability ) |
| Spec version translation | Clients and servers on different MCP versions can still talk | Translation between versions (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:
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
).
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:
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
. The full walkthrough is virtual MCP on Kubernetes
.
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:
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 .
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:
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 ):
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:
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
.
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:
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:
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
).
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 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
.
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 ), 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 ).
What to check in any MCP gateway
- Does
tools/listchange per caller, or does every caller see every tool? (MCP authorization ) - 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-Authenticateand serve protected resource metadata? (MCP authentication ) - Can each upstream server get its own credentials, so the agent never holds them? (MCP target policies )
- What happens when two servers expose the same tool name? (virtual MCP )
- Which MCP spec versions does it support on each side? (spec compatibility )
- Do the logs and metrics carry the tool name? On v1.6.0 my access log lines carried
mcp.target=everythingandgen_ai.tool.name=echo, and the metrics endpoint countedagentgateway_mcp_requests_totalwithmethod="tools/call",server="time"andresource="get_current_time"labels. (MCP observability ) - Can you find the servers nobody put behind it? Those are shadow MCP servers .
- Can a server change a tool description after you approved it? See why a name allowlist is not enough and keeping server instructions out of the system prompt .
When you are ready to compare gateways on the controls they enforce, 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.