Configure MCP routing
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
GatewayClassnamedmcpgwclasscreated and bound to the controller (citrix.com/nscpxgw-controllerfor NetScaler CPX,citrix.com/nsgw-controllerfor 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
ExternalNameServicethat 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=<path-to-tls.crt> --key=<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.