Skip to content
agentgateway has joined the Agentic AI Foundation — Learn more

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

Page as Markdown

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 agentgateway. 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_SEND in 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 the agentgateway binary.

Set up CEL guardrails

  1. Create a config.yaml file with a local stdio MCP server and three CEL processors in the route’s mcpGuardrails policy.

    # yaml-language-server: $schema=https://agentgateway.dev/schema/config
    gateways:
      default:
        port: 3000
    routes:
    - policies:
        cors:
          allowOrigins:
          - "*"
          allowHeaders:
          - mcp-protocol-version
          - content-type
          - mcp-session-id
          exposeHeaders:
          - "Mcp-Session-Id"
        mcpGuardrails:
          processors:
          # Reject before masking, so this rule sees the original message.
          - kind: expression
            methods:
              tools/call: request
            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.
          - kind: expression
            methods:
              tools/call: request
            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.
          - kind: expression
            methods:
              tools/call: response
            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
                )
              }))
      backends:
      - mcp:
          targets:
          - name: everything
            stdio:
              cmd: npx
              args: ["@modelcontextprotocol/server-everything"]

    Review the following table to understand the mcpGuardrails policy.

    SettingDescription
    kind: expressionRun the processor in-process as a CEL expression.
    methodsThe 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.
    conditionOptional CEL expression that must be true for the processor to act. When the condition is false, the processor passes the message through unchanged.
    rejectRejection message for the client. The first processor checks for DO_NOT_SEND before masking changes the message.
    transformCEL 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.
  2. Run agentgateway with the configuration file.

    agentgateway -f config.yaml

Verify the guardrails

  1. Initialize an MCP session and save the session ID.

    export MCP_SESSION_ID=$(curl -s -D - http://localhost:3000/mcp \
      -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_ID
  2. Send notifications/initialized to complete the MCP handshake before calling tools.

    curl -s http://localhost:3000/mcp \
      -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"}'
  3. Call the echo tool with a message that contains two email addresses and two demo secrets.

    curl -s http://localhost:3000/mcp \
      -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 [email protected] and [email protected]; 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]"}]}}
    
  4. Call the echo tool with a message that contains DO_NOT_SEND.

    curl -s http://localhost:3000/mcp \
      -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 [email protected]"}}}'

    Verify that the call is rejected before it reaches the MCP server. The tool result contains isError: true and 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.
  1. Stop the agentgateway process with Ctrl+C.
  2. Delete the configuration file.
    rm config.yaml
Was this page helpful?
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate ↵ select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.