MCP Guardrails: Gate Tool Calls at the Method Layer With Agentgateway

MCP Guardrails: Gate Tool Calls at the Method Layer With Agentgateway

A typical gateway comes with a lot of policy enforcement, authentication/authorization implementations, and filtering for both agentic and API-centric traffic. However, there may come a time when you need:

  • Custom auth, inspection, or mutation logic
  • Integration of custom policy engines
  • Business-specific services and logic

And that's where ExtProc comes into play.

It, ironically enough, is also where ExtMCP for MCP Guardrails comes into play.

In this blog post, you'll learn what ExtProc does, how it typically works in gateways, and how you can carry similar primitives and concepts over to MCP Guardrails.

Prerequisites

To follow along with this blog post in a hands-on fashion, you will need:

  1. A Kubernetes cluster (local is totally fine).
  2. Agentgateway OSS installed, and you can learn how to do so here.
  3. A GitHub Personal Access Token (PAT).

ExtProc For Gateway Works

Before diving into how guardrails with MCP work, it's important to understand how ExtProc works. Conceptually, it allows your gateway to offload custom business logic that's not built into the gateway via high-performant, bidirectional gRPC streams.

Tldr; ExtProc lets a gateway delegate HTTP request and response processing to an external service.

A

When implementing MCP Guardrails, you'll most likely see ExtMcp. When thinking about the key differences, you can conceptualize it as instead of implementing standard ExtProc, ExtMCP is almost like a primitive tying back to how ExtProc works in nature. It provides a similar external extension point, but operates on parsed MCP method calls rather than raw HTTP.

What Is ExtMCP?

It's not, in itself, ExtProc or a thin wrapper around ExtProc. It's a third-party callout primitive at the MCP method layer. It does similar jobs that ext_authz and ExtProc do without making the policy server speak in native HTTP. ExtMCP is, however, modeled around ext_authz, but it borrows mutations from ExtProc.

Why a new protocol? MCP is natively JSON-RPC over Streamable HTTP or stdio. ExtProc is HTTP. It sees frames, Content-Type, and SSE data to decide “is issue_write allowed?” The ExtProc server would reassemble the body, parse JSON-RPC, and handle MCP framing. ExtMCP skips that as the gateway already parses the method (the policy server gets method, service_names, raw params/result, selected headers).

So the "family" is the same in terms of what they do and how they do it, but ExtMCP is MCP-native, not “ExtProc with MCP types.

In the sections to follow, you'll see, from a hands-on perspective, how to implement guardrails for an MCP Server.

Configuring An MCP Gateway

With the theory of the "why" complete, let's begin implementing a gateway. For the purposes of this blog post, the GitHub Copilot MCP Server will be used.

  1. Create a k8s Secret for auth to the GitHub Copilot MCP Server.
export GITHUB_PAT=

kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: github-pat
  namespace: agentgateway-system
type: Opaque
stringData:
  Authorization: "Bearer ${GITHUB_PAT}"
EOF
  1. Create a Gateway using the agentgateway Gateway Class.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mcp-gateway
  namespace: agentgateway-system
  labels:
    app: github-mcp-server
spec:
  gatewayClassName: agentgateway
  listeners:
    - name: mcp
      port: 3000
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Same
EOF
  1. Create a backend that points to GitHubs MCP.
kubectl apply -f - <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
  name: github-mcp-server
  namespace: agentgateway-system
spec:
  mcp:
    targets:
      - name: github-copilot
        static:
          host: api.githubcopilot.com
          port: 443
          path: /mcp/
          protocol: StreamableHTTP
          policies:
            tls: {}
            auth:
              secretRef:
                name: github-pat
EOF
  1. Capture the Gateway for use across this blog post.
export GATEWAY_IP=$(kubectl get svc mcp-gateway -n agentgateway-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

export MCP_ADDR=http://${GATEWAY_IP}:3000/mcp
  1. Test to ensure that the MCP Server is reachable via the Gateway.
curl -s "$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":"guardrails-demo","version":"1.0.0"}}}'

Implementing The ExtMCP Policy Server

With the MCP Gateway up and operational, it's tme to deploy a custom ExtMCP policy server.

Within the agentic-demo-repo under agentgateway-oss-k8s/mcp-guardrails/extmcp-server, you'll see:

  1. ext_mcp.proto: ExtMCP gRPC contract agentgateway calls. Defines CheckRequest / CheckResponse and the Pass / Mutate / Deny results.
  2. server.py: Policy server that implements that contract. Denies GitHub write tools on tools/call, strips those tools from tools/list and tags the rest with [guarded].

Create the ConfigMap and the deploy.yaml so agentgateway can read the ExtMCP configuration for agentgateway and the ExtMCP policy server is running in the cluster:

kubectl -n agentgateway-system create configmap extmcp-github-policy \
  --from-file=server.py=guardrails/extmcp-server/server.py \
  --from-file=ext_mcp.proto=guardrails/extmcp-server/ext_mcp.proto \
  --from-file=requirements.txt=guardrails/extmcp-server/requirements.txt \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f guardrails/extmcp-server/deploy.yaml

Then, create the policy that attaches the guardrails to the GitHub Copilot MCP Server backend.

kubectl apply -f - <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
  name: mcp-guardrails
  namespace: agentgateway-system
spec:
  targetRefs:
    - group: agentgateway.dev
      kind: AgentgatewayBackend
      name: github-mcp-server
  backend:
    mcp:
      guardrails:
        processors:
        - remote:
            backendRef:
              name: ext-mcp
              port: 4445
            failureMode: FailClosed
          methods:
            tools/call: Request
            tools/list: Response
EOF

Add The Deadline

The guardrails policy above tells agentgateway "who" to call, it does not set a deadline. By default, the gRPC call to the policy server waits forever. A cold or stuck ExtMCP Pod then hangs tools/call / tools/list instead of failing closed after a few seconds.

The object below defines the deadline.

kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: ext-mcp-route
  namespace: agentgateway-system
spec:
  parentRefs:
    - name: mcp-gateway
  hostnames:
    - "ext-mcp.internal"
  rules:
    - backendRefs:
        - name: ext-mcp
          port: 4445
---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
  name: ext-mcp-timeout
  namespace: agentgateway-system
spec:
  targetRefs:
    - group: ""
      kind: Service
      name: ext-mcp
  backend:
    http:
      requestTimeout: 5s
EOF

Verify The ExtMCP Server

With the configurations in place, let's verify that ExtProc will work as expected.

  1. Open MCP Inspector.
npx modelcontextprotocol/inspector#0.18.0
  1. Connect to your Gateway with http://$GATEWAY_IP:3000/mcp.

You'll notice that:

  • All of the write tools are gone.
  • get_me succeeds, but issue_write is not allowed.

And that's specified in the server.py and in the AgentgatewayPolicy as it only wires methods to that server (tools/calls: Request, tools/list:Response) instead of tool names.

Congrats! You've successfully implemented MCP Guardrails.