Configure MCP routing

Last published : Nov 02, 2026
NetScaler® MCP Gateway gives every Model Context Protocol (MCP) client a single endpoint and routes each request to the correct upstream MCP server based on the x-netscaler-target-mcp-server request header. Clients never need to know the backend service addresses or topology, and adding, moving, or removing an MCP server does not require client changes.
One Gateway maps to one NetScaler content switching (CS) virtual server. That CS virtual server inspects the routing header and fans out to a per-server load balancing (LB) virtual server. Each MCP server is modeled as one mcproute (header match to a backendRef) with a backend mcpprofile attached to the route through an ExtensionRef filter. The Gateway itself carries the frontend mcpprofile.

Prerequisites

  • NetScaler Kubernetes Gateway Controller deployed with the desired configuration.
  • A GatewayClass named mcpgwclass created and bound to the controller (citrix.com/nscpxgw-controller for NetScaler CPX, citrix.com/nsgw-controller for NetScaler VPX/MPX/BLX).
  • Each in-cluster MCP server deployed as a Kubernetes Service.
  • For a remote or hosted MCP server that runs outside the cluster, an ExternalName Service that resolves to the server's FQDN.

Configuration

MCP profiles

An mcpprofile defines the settings the NetScaler applies when it processes MCP traffic. A profile is either FRONTEND (applied to the client-facing CS virtual server that fronts the Gateway) or BACKEND (applied to the service group that connects to one MCP server). The mcpprofile and mcproute CRDs are in the API group citrix.com/v1.
Note:
Modifying an mcpprofile, or changing the mcpprofile attached to a Gateway or mcproute, causes the associated NetScaler entities to be recreated. Traffic on the affected route is impacted while they are rebuilt.

Frontend MCP profile

The frontend profile presents the Gateway to the client. Set profileType: FRONTEND and proxyMode: REVERSE — reverse proxy mode is what places the NetScaler in front of the MCP servers rather than acting as an egress proxy out of them. proxyMode defaults to FORWARD, so REVERSE must be set explicitly. A frontend profile programs only profileType, proxyMode, and comment; the replacement and injection fields are backend-only.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-frontend
  namespace: default
spec:
  config:
    profileType: FRONTEND
    proxyMode: REVERSE
    comment: "MCP gateway frontend"
EOF

Backend MCP profile

The backend profile controls how the NetScaler rewrites and forwards each request to one MCP server. Set profileType: BACKEND and proxyMode: FORWARD. The forward-proxy fields are:
Field Purpose
hostReplacement When ENABLED, replaces the client Host header with the target MCP server FQDN before forwarding. Keep ENABLED for backend profiles.
urlReplacement When ENABLED, rewrites the client URL to the backend MCP URL. Set ENABLED when the server is mounted at the root path (/), and DISABLED when the server is mounted at a subpath such as /mcp so that the path is preserved.
insertHeaderInClientRequest When ENABLED, the NetScaler injects a stored credential into upstream requests. Leave DISABLED for routing; credential injection is configured separately.
The following backend profile is for the everything server, which is mounted at /mcp, so urlReplacement is DISABLED to preserve the path.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-everything-backend
  namespace: default
spec:
  config:
    profileType: BACKEND
    proxyMode: FORWARD
    hostReplacement: ENABLED
    urlReplacement: DISABLED        # everything is mounted at /mcp -> preserve the path
    insertHeaderInClientRequest: DISABLED
    comment: "everything backend"
EOF
Note:
urlReplacement must mirror where the upstream actually mounts MCP: root (/) requires ENABLED, and a subpath (for example /mcp) requires DISABLED. A mismatch makes the upstream return 404 even though the NetScaler configuration looks healthy.

Gateway with the frontend MCP profile

The Gateway defines the single client-facing endpoint. Attach the frontend mcpprofile under infrastructure.parametersRef; the controller binds it to the CS virtual server that fronts all MCP traffic.
On NetScaler CPX the VIP is the CPX pod IP, so leave spec.addresses unset. The front end can be an HTTP listener (shown next) or an HTTPS listener that terminates TLS — the HTTPS form is identical to the VPX/MPX/BLX example that follows, only without spec.addresses.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mcp-gw
  namespace: default
spec:
  gatewayClassName: mcpgwclass
  infrastructure:
    parametersRef:
      group: citrix.com
      kind: mcpprofile
      name: mcp-frontend
  listeners:
    - name: mcp
      protocol: HTTP
      port: 8891
      allowedRoutes:
        namespaces:
          from: Same
EOF
Note:
Use an MCP listener port other than 80. These examples use 8891 for the HTTP front end on NetScaler CPX and 8443 for the HTTPS front end on NetScaler VPX/MPX/BLX, matching the complete MCP Gateway example.
To terminate TLS at the front end, use an HTTPS listener with tls.mode: Terminate and a certificateRefs Secret. The controller reads the listener protocol off the Gateway and emits an SSL CS virtual server with the certificate bound; nothing else in the routing configuration changes. The following example is for NetScaler VPX/MPX/BLX and sets spec.addresses to the static VIP; the same HTTPS listener works on NetScaler CPX — omit spec.addresses so the VIP stays the CPX pod IP.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mcp-gw
  namespace: default
spec:
  gatewayClassName: mcpgwclass
  addresses:
    - type: IPAddress
      value: <VIP>
  infrastructure:
    parametersRef:
      group: citrix.com
      kind: mcpprofile
      name: mcp-frontend
  listeners:
    - name: mcp-tls
      protocol: HTTPS
      port: 8443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: mcp-gateway-tls
            group: ""
      allowedRoutes:
        namespaces:
          from: Same
EOF
Note:
Create the TLS Secret referenced by the HTTPS listener before applying the Gateway. Replace the certificate and key paths with your own:
kubectl create secret tls mcp-gateway-tls --cert=&lt;path-to-tls.crt> --key=&lt;path-to-tls.key>
Note:
MCP responses are long-lived server-sent event (SSE) streams. Attach an httpprofile to the Gateway (through its targetRef) that disables the request timeout and keeps HTTP/2 and WebSocket upgrades off, so the stream is not reframed underneath the client:
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: httpprofile
metadata:
  name: mcp-httpprofile
  namespace: default
spec:
  gatewayClassName: mcpgwclass
  targetRef:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  config:
    reqtimeout: 0
    http2: DISABLED
    websocket: DISABLED
EOF

MCP route

An mcproute attaches to the Gateway through parentRefs and defines one rule per MCP server. The rule matches the x-netscaler-target-mcp-server header with match type MCPTargetServer, routes to the backend Service through backendRefs, and attaches the backend mcpprofile through an ExtensionRef filter.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-everything
  namespace: default
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - name: x-netscaler-target-mcp-server
              type: MCPTargetServer
              value: everything.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-everything-backend
      backendRefs:
        - name: mcp-everything
          port: 3001
EOF
Important:
The MCPTargetServer match compiles to the CS policy rule HTTP.REQ.HEADER("x-netscaler-target-mcp-server").BEFORE_STR("/").CONTAINS("everything.mcp.example"). The match value in the mcproute carries no trailing slash, but the client must send the header with a trailing slash:
x-netscaler-target-mcp-server: everything.mcp.example/
BEFORE_STR("/") strips everything from the first slash before the substring match. Without the trailing slash the expression yields nothing, the rule never matches, and the request falls through to the catch-all.
Add one mcproute and one backend mcpprofile per MCP server (for example everything, deepwiki, and github), each attached to the same Gateway with its own header value (everything.mcp.example, deepwiki.mcp.example, github.mcp.example). The single Gateway CS virtual server fans out to each per-server LB virtual server.

Backends and ExternalName

Each backendRefs entry points at a Kubernetes Service. For an in-cluster MCP server, use a standard Service whose port matches the pod's target port.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mcp-everything
  namespace: default
spec:
  selector:
    app: mcp-everything
  ports:
    - name: mcp
      port: 3001
      targetPort: 3001
EOF
Note:
Set the mcproute backendRefs[].port equal to the Service target port. The service group member and the appProtocol lookup both key on the backend port matched against the endpoint (target) ports, so differing values silently drop the binding.
For a remote or hosted MCP server, model the backend as an ExternalName Service that resolves to the remote FQDN. The service group member becomes the resolved remote address, and the mcproute backendRefs points at this Service exactly as it would an in-cluster one. Set appProtocol: ssl on the port so the NetScaler speaks TLS to the remote server on port 443.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mcp-deepwiki
  namespace: default
spec:
  type: ExternalName
  externalName: mcp.deepwiki.com
  ports:
    - name: mcps
      port: 443
      appProtocol: ssl
EOF
The backend mcpprofile for a remote server follows the same rules as an in-cluster one: keep hostReplacement: ENABLED so the upstream receives its own FQDN in the Host header, and set urlReplacement to match where the remote server mounts MCP (DISABLED for a /mcp mount such as deepwiki, ENABLED for a root mount).
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-deepwiki-backend
  namespace: default
spec:
  config:
    profileType: BACKEND
    proxyMode: FORWARD
    hostReplacement: ENABLED
    urlReplacement: DISABLED        # deepwiki is mounted at /mcp -> preserve the path
    insertHeaderInClientRequest: DISABLED
    comment: "deepwiki remote backend"
EOF
The mcproute for the remote server mirrors the in-cluster route, with header value deepwiki.mcp.example, a backendRefs to the mcp-deepwiki Service on port 443, and an ExtensionRef filter to mcp-deepwiki-backend.

Platform support

Header-based routing and both MCP profile types work identically on NetScaler CPX and on NetScaler VPX/MPX/BLX — the CRDs, profiles, routes, and header match are the same on either form factor.
Capability NetScaler CPX (in-cluster) NetScaler VPX/MPX/BLX (external)
Header-based routing (mcproute) Supported Supported
Frontend and backend mcpprofile Supported Supported
ExternalName remote backends Supported Supported
Front-end TLS termination Supported (HTTPS listener with certificateRefs; VIP is the CPX pod IP) Supported (HTTPS listener with certificateRefs; VIP is a static or IPAM-allocated address)
Note:
OAuth 2.1 client authentication (the AAA-governed path) is available on the external NetScaler VPX/MPX/BLX form factor only. TLS termination at the front end is supported on both form factors — the difference is the VIP: on NetScaler CPX it is the CPX pod IP (leave spec.addresses unset), and on NetScaler VPX/MPX/BLX it is a static or IPAM-allocated address.

Reference