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 CEL guardrails
Reject and mask MCP tool calls in-process with CEL expression guardrails.
Reject and mask Model Context Protocol (MCP) tool calls with Common Expression Language (CEL) expressions inside the agentgateway proxy. CEL guardrails require no external policy server.
For an overview, see Guardrail processors. To use an external policy server, see Set up remote guardrails.
In this guide, you configure three CEL guardrails for the echo tool of a sample MCP server:
- Reject messages containing
DO_NOT_SENDin the request phase. - Mask email addresses before sending the request to the MCP server.
- Mask demo secrets in the response before returning it to the client.
Before you begin
Install and set up an agentgateway proxy.Set up CEL guardrails
Step 1: Deploy an MCP server
Deploy the MCP everything reference server. Its echo tool returns the message you send.
The Service uses appProtocol: agentgateway.dev/mcp so that agentgateway can select it as an MCP target.
kubectl apply -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server-everything
labels:
app: mcp-server-everything
spec:
replicas: 1
selector:
matchLabels:
app: mcp-server-everything
template:
metadata:
labels:
app: mcp-server-everything
spec:
containers:
- name: mcp-server-everything
image: node:20-alpine
command: ["npx"]
args: ["-y", "@modelcontextprotocol/server-everything", "streamableHttp"]
ports:
- containerPort: 3001
readinessProbe:
tcpSocket:
port: 3001
initialDelaySeconds: 2
periodSeconds: 2
failureThreshold: 30
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server-everything
labels:
app: mcp-server-everything
spec:
selector:
app: mcp-server-everything
ports:
- protocol: TCP
port: 3001
targetPort: 3001
appProtocol: agentgateway.dev/mcp
type: ClusterIP
EOFStep 2: Create the backend for the MCP server
Create an AgentgatewayBackend that selects the MCP server by its labels.
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: mcp-backend
spec:
mcp:
targets:
- name: mcp-server-everything
selector:
services:
matchLabels:
app: mcp-server-everything
EOFStep 3: 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 4: Apply the guardrails policy
Create an AgentgatewayPolicy that attaches three CEL expression processors to the AgentgatewayBackend.
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: mcp-cel-guardrails
spec:
targetRefs:
- group: agentgateway.dev
kind: AgentgatewayBackend
name: mcp-backend
backend:
mcp:
guardrails:
processors:
# Reject before masking, so this rule sees the original message.
- methods:
tools/call: Request
expression:
condition: >-
mcp.params.name == 'echo' &&
has(mcp.params.arguments) &&
has(mcp.params.arguments.message) &&
mcp.params.arguments.message.contains('DO_NOT_SEND')
reject: "Messages containing DO_NOT_SEND may not be sent upstream"
# Mask every email address in the message before it reaches the MCP server.
- methods:
tools/call: Request
expression:
condition: >-
mcp.params.name == 'echo' &&
has(mcp.params.arguments) &&
has(mcp.params.arguments.message)
transform: >-
mcp.params.with(p, p.merge({
'arguments': p.arguments.merge({
'message': p.arguments.message.regexReplaceAll(
'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}',
'[EMAIL]'
)
})
}))
# Mask demo secrets in every text block that a tool returns.
- methods:
tools/call: Response
expression:
condition: has(mcp.result.content)
transform: >-
mcp.result.with(r, r.merge({
'content': r.content.map(item,
item.type == 'text'
? item.merge({'text': item.text.regexReplaceAll(
'demo-secret-[A-Za-z0-9]+', '[SECRET]'
)})
: item
)
}))
EOFReview the following table to understand the policy.
| Setting | Description |
|---|---|
methods | The MCP methods that run through the processor, and the phase for each. tools/call: Request runs the processor before the call reaches the MCP server. tools/call: Response runs the processor after the MCP server returns a result. |
expression.condition | Optional CEL expression that must be true for the processor to act. When the condition is false, the processor passes the message through unchanged. |
expression.reject | Rejection message for the client. The first processor checks for DO_NOT_SEND before masking changes the message. |
expression.transform | CEL expression that replaces mcp.params (request) or mcp.result (response). The merge function preserves other fields. Use regexReplaceAll to mask every match, or regexReplace to mask only the first. |
Check the policy’s status reason and message as well as Accepted. Invalid CEL can produce Accepted with the reason PartiallyValid. For admission requirements and error behavior, see Kubernetes validation.
Verify the guardrails
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
notifications/initializedto complete the MCP handshake before calling tools.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"}'Call the
echotool with a message that contains two email addresses and two demo secrets.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/call","params":{"name":"echo","arguments":{"message":"Contact alice@example.com and bob@example.org; tokens demo-secret-abc and demo-secret-xyz"}}}'Verify that the result masks both email addresses and both secrets. The request guardrail masks the addresses; the response guardrail masks the secrets. This response uses server-sent events (
Content-Type: text/event-stream).event: message data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Echo: Contact [EMAIL] and [EMAIL]; tokens [SECRET] and [SECRET]"}]}}Call the
echotool with a message that containsDO_NOT_SEND.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":"echo","arguments":{"message":"DO_NOT_SEND alice@example.com"}}}'Verify that the call is rejected before it reaches the MCP server. The tool result contains
isError: trueand the configured rejection message. Agentgateway returns this request-phase rejection as plain JSON (Content-Type: application/json) with HTTP 200.{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Messages containing DO_NOT_SEND may not be sent upstream"}],"isError":true}}
Cleanup
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy mcp-cel-guardrails
kubectl delete HTTPRoute mcp
kubectl delete AgentgatewayBackend mcp-backend
kubectl delete Deployment mcp-server-everything
kubectl delete Service mcp-server-everything