For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Set up MCP guardrails
Gate and mutate MCP method calls with an external ExtMCP policy server.
Gate and mutate Model Context Protocol (MCP) method calls with an external policy server. For more information about how MCP guardrails work, see About MCP guardrails.
In this guide, you route tools/call and tools/list requests through a sample ExtMCP server that denies any tool whose name contains forbidden and annotates each tool description in tools/list responses.
Before you begin
Install and set up an agentgateway proxy.Set up MCP guardrails
Step 1: Deploy an MCP server
Deploy an MCP server for agentgateway to proxy traffic to. The following example sets up a simple MCP server with one tool, fetch, that retrieves the content of a website URL.
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-website-fetcher
spec:
selector:
matchLabels:
app: mcp-website-fetcher
template:
metadata:
labels:
app: mcp-website-fetcher
spec:
containers:
- name: mcp-website-fetcher
image: ghcr.io/peterj/mcp-website-fetcher:main
imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: mcp-website-fetcher
labels:
app: mcp-website-fetcher
spec:
selector:
app: mcp-website-fetcher
ports:
- port: 80
targetPort: 8000
appProtocol: agentgateway.dev/mcp
EOFStep 2: Deploy a sample ExtMCP server
Deploy a gRPC ExtMCP policy server. This example uses a prebuilt sample server that denies tools/call when the tool name contains forbidden, and appends [extmcp] to every tool description in tools/list responses.
The Service uses appProtocol: kubernetes.io/h2c so that agentgateway connects to the policy server over cleartext HTTP/2 (gRPC).
Build your own ExtMCP server: The sample server is for demonstration only. To build your own, implement the ExtMcp gRPC service from the ExtMCP protocol definition. The service has two methods:
CheckRequest: Called in the request phase, before the call reaches the MCP backend. Return the request unchanged, return mutatedparams, or return anAuthorizationErrorto deny the call.CheckResponse: Called in the response phase, after the MCP backend returns a result. Return the response unchanged, return a mutatedresult, or return anAuthorizationErrorto deny the call.
Generate gRPC bindings from the proto file in your language, implement the two methods, and serve them over cleartext HTTP/2 (h2c) on the port that your Service targets. For more information about the request and response messages, outcomes, and error codes, see About MCP guardrails.
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ext-mcp-server
spec:
selector:
matchLabels:
app: ext-mcp-server
template:
metadata:
labels:
app: ext-mcp-server
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_unprivileged_port_start
value: "0"
containers:
- name: ext-mcp-server
image: ghcr.io/agentgateway/testbox:0.0.1
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: ext-mcp
labels:
app: ext-mcp
spec:
selector:
app: ext-mcp-server
ports:
- port: 4445
targetPort: 9001
protocol: TCP
appProtocol: kubernetes.io/h2c
EOFStep 3: Create the backend for the MCP server
Create an AgentgatewayBackend that sets up the agentgateway target details for the MCP server.
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: mcp-backend
spec:
mcp:
targets:
- name: mcp-target
static:
host: mcp-website-fetcher.default.svc.cluster.local
port: 80
protocol: SSE
EOFStep 4: Route to the backend
Create an HTTPRoute that routes /mcp requests to the AgentgatewayBackend.
kubectl apply -f- <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mcp
spec:
parentRefs:
- name: agentgateway-proxy
namespace: agentgateway-system
rules:
- matches:
- path:
type: PathPrefix
value: /mcp
backendRefs:
- name: mcp-backend
group: agentgateway.dev
kind: AgentgatewayBackend
EOFStep 5: Apply the guardrails policy
Create an AgentgatewayPolicy that attaches MCP guardrails to the AgentgatewayBackend.
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: mcp-guardrails
spec:
targetRefs:
- group: agentgateway.dev
kind: AgentgatewayBackend
name: mcp-backend
backend:
mcp:
guardrails:
processors:
- remote:
backendRef:
name: ext-mcp
port: 4445
failureMode: FailClosed
methods:
tools/call: Request
tools/list: Response
EOFReview the following table to understand the policy.
| Setting | Description |
|---|---|
remote.backendRef | The ExtMCP policy server that agentgateway calls. This example points to the ext-mcp Service from Step 2. |
failureMode: FailClosed | Deny requests if the policy server is unreachable or returns an error. To allow requests instead, set FailOpen. |
methods | The MCP methods to route through the policy server, and the phase for each. tools/call: Request sends each tool call to the server before it reaches the MCP backend, so the server can allow, mutate, or deny the call. tools/list: Response sends the tool listing to the server after the backend returns it, so the server can filter or annotate the list. For the full list of phases and method matching, see About MCP guardrails. |
Verify the guardrails
Verify that the policy server gates tools/call and mutates tools/list responses.
Set the agentgateway address. The following steps use the
MCP_ADDRvariable for the MCP endpoint.export INGRESS_GW_ADDRESS=$(kubectl get gateway agentgateway-proxy -n agentgateway-system -o=jsonpath="{.status.addresses[0].value}") export MCP_ADDR=http://$INGRESS_GW_ADDRESS/mcp echo $MCP_ADDRInitialize an MCP session and save the session ID.
export MCP_SESSION_ID=$(curl -s -D - $MCP_ADDR \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}' \ | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r') echo $MCP_SESSION_IDSend the
notifications/initializednotification to complete the MCP handshake. The MCP server does not answer other requests, such astools/list, until initialization is complete.curl -s $MCP_ADDR \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'List the available tools. Verify that the
fetchtool description ends with[extmcp], which the policy server added in the response phase.curl -s $MCP_ADDR \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'Example output:
data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"fetch","description":"Fetches a website and returns its content [extmcp]","inputSchema":{...}}]}}Call a tool whose name contains
forbidden. Verify that the policy server denies the call with a JSON-RPC error.curl -s $MCP_ADDR \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"forbidden-tool","arguments":{}}}'Example output:
{"jsonrpc":"2.0","id":3,"error":{"code":-32001,"message":"tool forbidden-tool is not allowed"}}Call the allowed
fetchtool. Verify that the call succeeds and returns the fetched content.curl -s $MCP_ADDR \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"fetch","arguments":{"url":"https://example.com"}}}'Example output:
data: {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"<!doctype html><html lang=\"en\">..."}]}}
Troubleshooting
MCP requests hang when the policy server is slow or cold
What’s happening:
A tools/call or tools/list request hangs instead of returning a result or an error.
Why it’s happening:
The guardrails callout has no deadline by default. If the ExtMCP server is slow or its connection is cold, the request waits on the callout instead of letting failureMode engage.
How to fix it:
Apply a request timeout to the ExtMCP server. When the timeout is reached, agentgateway applies the processor’s failureMode configuration. If failureMode is set to FailClosed, the client receives a JSON-RPC error. If the mode is set to FailOpen, the request proceeds without the guardrail.
The timeout policy must be configured in an AgentgatewayPolicy resource that targets the ext-mcp Service. A backend policy that targets a Service attaches only after the Service becomes part of the proxy data plane configuration, which happens when a route references it. The HTTPRoute in the following example exists only to bring the ext-mcp Service into the data plane configuration so that the timeout policy attaches. The ext-mcp.internal hostname is a placeholder for the ExtMCP server. The hostname is not used for traffic to your MCP servers, and clients continue to call the gateway on the /mcp path from Step 4.
kubectl apply -f- <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ext-mcp-route
spec:
parentRefs:
- name: agentgateway-proxy
namespace: agentgateway-system
hostnames:
- "ext-mcp.internal"
rules:
- backendRefs:
- name: ext-mcp
port: 4445
---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: ext-mcp-timeout
spec:
targetRefs:
- group: ""
kind: Service
name: ext-mcp
backend:
http:
requestTimeout: 5s
EOFCleanup
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy mcp-guardrails
kubectl delete AgentgatewayPolicy ext-mcp-timeout --ignore-not-found
kubectl delete HTTPRoute mcp
kubectl delete HTTPRoute ext-mcp-route --ignore-not-found
kubectl delete AgentgatewayBackend mcp-backend
kubectl delete Deployment mcp-website-fetcher ext-mcp-server
kubectl delete Service mcp-website-fetcher ext-mcp