Configure MCP server access control

Last published : Nov 02, 2026
The NetScaler® MCP Gateway routes each client request to a backend MCP server based on the x-netscaler-target-mcp-server request header. Server access control lets an administrator decide which target servers a client may reach: it blocks named servers explicitly and denies any request whose target does not match a configured route, returning a clean JSON 403 instead of an unhelpful 503.
Note:
Server access control is supported on both NetScaler CPX (in-cluster) and NetScaler VPX/MPX/BLX (external) on NetScaler 14.1. The configuration is identical on both form factors.

Prerequisites

  • NetScaler Kubernetes Gateway Controller deployed with the desired configuration.
  • A GatewayClass (mcpgwclass) created and bound to the NetScaler instance, and an MCP Gateway (mcp-gw) with a front-end MCP profile.
  • Per-server MCP routes already defined for every server that clients are permitted to reach. Any target that has no route is denied by the catch-all.

How it works

Access control is built from two responder checks, evaluated in order and attached to a single catch-all route:
  • Explicit deny list — matches specific target server names in the x-netscaler-target-mcp-server header and responds with 403 Blocked MCP server.
  • Default deny — matches everything else that reached the catch-all and responds with 403 Unauthorized MCP server access.
Both checks are defined as responder policies in one rewritepolicy CRD, and the list order fixes their priority (explicit deny first, default deny last). The rewritepolicy is referenced from a catch-all mcproute through an extensionRef filter. The catch-all route uses default: true, which compiles to a lowest-priority -rule true content switching policy — so any request that matches no other route falls through to it and is denied.
Only requests that reach the catch-all are evaluated by these checks — that is, requests whose target header matches no configured MCP route. Requests that match a real route are served normally and are never seen by the deny list. To block a server name that is routed, remove its route instead of adding it to the deny list.
Important:
Attach the access-control responder policy to a route through extensionRef, not to the Gateway. A request-time responder policy that targets the Gateway is not evaluated, because the Gateway's content switching virtual server always carries an MCP front-end profile. Binding the policy to the catch-all route makes it fire on every unrouted request.
Important:
A Gateway can have exactly one catch-all route (one rule with default: true). Apply the catch-all route last, after all the per-server routes for the servers clients are allowed to reach.

Configuration

Define the access-control policy

Create a rewritepolicy that holds both responder checks. The first responder policy blocks named servers (here, any target whose name contains malicious or untrusted); the second denies everything else. Each responder returns a JSON body so an MCP client can surface a real error, and each ends with goto-priority-expression: END so evaluation stops after a match.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: rewritepolicy
metadata:
  name: mcp-default-deny
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  responder-policies:
    # Explicit deny list: block named target servers on the routing header.
    - responder-policy:
        respondwith:
          http-payload-string: '"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"error\":\"Blocked MCP server\"}"'
        respond-criteria: 'HTTP.REQ.HEADER("x-netscaler-target-mcp-server").CONTAINS("malicious") || HTTP.REQ.HEADER("x-netscaler-target-mcp-server").CONTAINS("untrusted")'
        comment: 'mcp_deny_explicit'
      goto-priority-expression: END
    # Default deny: everything else that reached the catch-all.
    - responder-policy:
        respondwith:
          http-payload-string: '"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"error\":\"Unauthorized MCP server access\"}"'
        respond-criteria: 'true'
        comment: 'mcp_default_deny'
      goto-priority-expression: END
EOF
Note:
The respond-criteria inspects the same x-netscaler-target-mcp-server header used for routing. Clients send that header with a trailing slash (for example, everything.mcp.example/); the CONTAINS match keys on a substring of the value, so a plain server name such as malicious matches regardless of the slash.

Attach the policy to a catch-all route

Create an mcproute that parents to the Gateway and carries a single rule with default: true and no matches. Reference the access-control rewritepolicy through an extensionRef filter. A rule needs a backendRef to compile into a load balancing virtual server, so reuse an existing backend Service — the responder terminates the request first, so no traffic ever reaches that backend.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-catchall
  namespace: default
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  rules:
    # default: true + no matches -> lowest-priority "-rule true" catch-all policy.
    - default: true
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: rewritepolicy
            name: mcp-default-deny
      # Reused only to compile the route into an lb vserver; the responder
      # terminates first, so nothing reaches this backend.
      backendRefs:
        - name: mcp-everything
          port: 3001
EOF
After the route is programmed, a request whose x-netscaler-target-mcp-server header does not match any configured server route receives 403 Unauthorized MCP server access, and a request targeting a blocked name receives 403 Blocked MCP server. Requests targeting a routed server are unaffected.

Block a routed server

The explicit deny list only sees requests that fall through to the catch-all, so it cannot block a target that already has its own route. To stop clients reaching a server that is currently routed, remove that server's mcproute. The target then no longer matches any route, falls through to the catch-all, and is denied.

Reference