MCP Gateway complete example

Last published : Nov 02, 2026
This page is a single, copy-pasteable walkthrough that builds a complete Model Context Protocol (MCP) Gateway with the NetScaler® Kubernetes Gateway Controller. You start from empty backends and finish with a governed, observable MCP front door that routes to several MCP servers, enforces access control and rate limits, injects upstream credentials, authenticates clients, and exports analytics to Splunk.
The example fronts three MCP servers behind one Gateway endpoint:
Server Where it runs What it demonstrates
everything In-cluster pod Standard in-cluster backend, host and MCP session-id handling
deepwiki Remote (ExternalName) Reaching an external MCP server over TLS with SNI
github Remote (ExternalName) Credential injection and client authentication in front of a protected server
Clients reach every server through the same virtual server. NetScaler selects the target server from a request header, so client applications never need to know backend addresses or topology.
The walkthrough is written for two form factors. Follow the track that matches the NetScaler you deploy:
  • On NetScaler CPX — NetScaler CPX and the Gateway Controller run together inside the cluster. This example uses an HTTP front door for simplicity (NetScaler CPX can also terminate TLS), and the Gateway virtual server IP is the CPX pod IP.
  • On NetScaler VPX — the Gateway Controller runs in the cluster and configures an external NetScaler VPX/MPX/BLX. The MCP front door is HTTPS with TLS termination, and the Gateway uses a routable virtual server IP.
Steps that are identical on both platforms are shown once. Steps that differ show a labelled track for each.

Platform support

Capability NetScaler CPX NetScaler VPX/MPX/BLX
Header-based routing to MCP servers Yes Yes
Front-end and back-end MCP profiles Yes Yes
Remote servers over ExternalName Yes Yes
Server access control (responder) Yes Yes
Tool-based rate limiting Yes Yes
Credential injection to upstream server Yes Yes
OAuth 2.1 client authentication No Yes
Splunk observability Yes Yes
Important:
OAuth 2.1 client authentication for MCP traffic runs on the AAA-governed path, which is available only on NetScaler VPX/MPX/BLX. It is not available for MCP on NetScaler CPX. Every other step in this example works on both platforms.

Prerequisites

  • A Kubernetes cluster with kubectl configured.
  • The NetScaler Kubernetes Gateway Controller installed for your platform, and the Gateway API CRDs installed from the official standard channel. See Deploy NetScaler Kubernetes Gateway Controller for MCP.
  • On NetScaler VPX/MPX/BLX: a routable virtual server IP for the Gateway listener, and a TLS certificate and key for the MCP endpoint.
  • This example runs NetScaler 14.1.
Note:
All resources in this example are created in the default namespace. Change the namespace to suit your environment, and keep it consistent across every manifest.

Configuration

1. Deploy the MCP backends

Deploy the three MCP servers that the Gateway routes to. The everything server runs in the cluster. The deepwiki and github servers are remote and are represented by ExternalName Services.

everything (in-cluster)

Deploy the everything MCP server. It listens on port 3001 and is mounted at /mcp.
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-everything
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mcp-everything
  template:
    metadata:
      labels:
        app: mcp-everything
    spec:
      containers:
        - name: everything
          image: node:22-alpine
          command: ["npx", "-y", "@modelcontextprotocol/server-everything", "streamableHttp"]
          ports:
            - containerPort: 3001
          readinessProbe:
            tcpSocket:
              port: 3001
EOF
Now expose the Deployment. The Service you create depends on how the NetScaler reaches the pod.
On NetScaler CPX, the NetScaler runs inside the cluster and reaches the pod directly. Create a normal ClusterIP Service.
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
On NetScaler VPX/MPX/BLX, the external NetScaler cannot reach the pod network directly. Expose the pod through a NodePort, then publish it to the Gateway Controller with a selector-less Service and matching Endpoints that point at a node IP and the node port. Replace <node-ip> with a reachable cluster node IP.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mcp-everything-np
  namespace: default
spec:
  type: NodePort
  selector:
    app: mcp-everything
  ports:
    - name: mcp
      port: 3001
      targetPort: 3001
      nodePort: 31191
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-everything
  namespace: default
spec:
  ports:
    - name: mcp
      port: 31191
      targetPort: 31191
---
apiVersion: v1
kind: Endpoints
metadata:
  name: mcp-everything
  namespace: default
subsets:
  - addresses:
      - ip: <node-ip>
    ports:
      - name: mcp
        port: 31191
EOF
Note:
The Service port and targetPort must be equal for MCP backends. The MCP route sends traffic to the Service port, and a mismatch sends traffic to the wrong upstream port.

deepwiki (remote server)

Represent the remote deepwiki server with an ExternalName Service. The appProtocol: ssl setting tells the NetScaler to build an SSL service group and connect over TLS, with SNI derived from the external name.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mcp-deepwiki
  namespace: default
spec:
  type: ExternalName
  externalName: mcp.deepwiki.com
  ports:
    - name: https
      port: 443
      appProtocol: ssl
EOF

github (remote server)

Represent the remote github MCP server the same way. This server is protected, and later steps put credential injection and client authentication in front of it.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mcp-github
  namespace: default
spec:
  type: ExternalName
  externalName: api.githubcopilot.com
  ports:
    - name: https
      port: 443
      appProtocol: ssl
EOF

2. Create the Gateway

Create the GatewayClass, an httpprofile to tune the streaming front door, and the Gateway itself. The Gateway references a front-end MCP profile (created in step 3) through infrastructure.parametersRef. The controller programs the front-end virtual server once that reference resolves.
The httpprofile disables request/response validation and connection idle timeouts that interfere with long-lived MCP streams, and is bound to the Gateway with targetRef.
Follow the track for your platform.

On NetScaler CPX

The GatewayClass binds to the CPX controller. The Gateway uses an HTTP listener, and no address is set — the front-end virtual server IP is the CPX pod IP.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: mcpgwclass
spec:
  controllerName: citrix.com/nscpxgw-controller
---
apiVersion: citrix.com/v1
kind: httpprofile
metadata:
  name: mcp-httpprofile
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  targetRef:
    - name: mcp-gw
      namespace: default
      kind: Gateway
      group: gateway.networking.k8s.io
  config:
    dropinvalreqs: "DISABLED"
    markhttp09inval: "DISABLED"
    reqtimeout: 0
    http2: "DISABLED"
    websocket: "DISABLED"
---
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: All
EOF
Note:
NetScaler CPX can also terminate TLS at the front end. Create the TLS Secret, then replace the HTTP listener above with an HTTPS listener — keep spec.addresses unset so the virtual server IP stays the CPX pod IP. Everything else in this example is unchanged.
kubectl create secret tls mcp-gateway-tls --cert=&lt;path-to-tls.crt> --key=&lt;path-to-tls.key>listeners:
  - name: mcp
    protocol: HTTPS
    port: 8443
    tls:
      mode: Terminate
      certificateRefs:
        - group: ""
          kind: Secret
          name: mcp-gateway-tls
    allowedRoutes:
      namespaces:
        from: All
The client endpoint then becomes https://<cpx-pod-ip>:8443/mcp.

On NetScaler VPX/MPX/BLX

The GatewayClass binds to the VPX controller. The Gateway uses an HTTPS listener that terminates TLS with a Kubernetes TLS Secret, and sets a routable virtual server IP under addresses.
Create the TLS Secret for the MCP endpoint first. 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>
Then create the class, profile, and Gateway. Replace <vpx-vip> with the virtual server IP on the NetScaler.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: mcpgwclass
spec:
  controllerName: citrix.com/nsgw-controller
---
apiVersion: citrix.com/v1
kind: httpprofile
metadata:
  name: mcp-httpprofile
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  targetRef:
    - name: mcp-gw
      namespace: default
      kind: Gateway
      group: gateway.networking.k8s.io
  config:
    dropinvalreqs: "DISABLED"
    markhttp09inval: "DISABLED"
    reqtimeout: 0
    http2: "DISABLED"
    websocket: "DISABLED"
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mcp-gw
  namespace: default
spec:
  gatewayClassName: mcpgwclass
  addresses:
    - type: IPAddress
      value: <vpx-vip>
  infrastructure:
    parametersRef:
      group: citrix.com
      kind: mcpprofile
      name: mcp-frontend
  listeners:
    - name: mcp
      protocol: HTTPS
      port: 8443
      tls:
        mode: Terminate
        certificateRefs:
          - group: ""
            kind: Secret
            name: mcp-gateway-tls
      allowedRoutes:
        namespaces:
          from: All
EOF
Note:
Set spec.gatewayClassName to mcpgwclass (the metadata.name of the GatewayClass) on the Gateway — a standard Gateway API field — and on the policy CRDs that carry it: ratelimit, rewritepolicy, and httpprofile. The authpolicy, mcpprofile, and mcproute CRDs have no gatewayClassName field: authpolicy attaches to an mcproute through an ExtensionRef filter, mcpprofile attaches to the Gateway through infrastructure.parametersRef (front end) or to an mcproute through an ExtensionRef filter (back end), and mcproute attaches to the Gateway through parentRefs. This example uses listener port 8891 for HTTP on CPX and 8443 for HTTPS on VPX; any listener port works.

3. Create the MCP profiles

An MCP profile defines how the NetScaler handles MCP traffic. A FRONTEND profile applies to the client-facing virtual server; a BACKEND profile applies to the service group that connects to each MCP server.
Create the front-end profile that the Gateway referenced in step 2.
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 front door"
EOF
Create one back-end profile per server. hostReplacement: ENABLED rewrites the upstream Host so the remote server accepts the request. urlReplacement is DISABLED for these servers because each one is mounted at /mcp and expects the path unchanged.
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
    insertHeaderInClientRequest: DISABLED
    comment: "everything backend"
---
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-deepwiki-backend
  namespace: default
spec:
  config:
    profileType: BACKEND
    proxyMode: FORWARD
    hostReplacement: ENABLED
    urlReplacement: DISABLED
    insertHeaderInClientRequest: DISABLED
    comment: "deepwiki backend"
---
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-github-backend
  namespace: default
spec:
  config:
    profileType: BACKEND
    proxyMode: FORWARD
    hostReplacement: ENABLED
    urlReplacement: DISABLED
    insertHeaderInClientRequest: DISABLED
    comment: "github backend"
EOF

4. Create the MCP routes with header routing

Create one mcproute per server. Each route attaches to the Gateway, selects its server by matching the x-netscaler-target-mcp-server header, attaches the back-end profile through an ExtensionRef filter, and forwards to the server's Service.
The header match uses type: MCPTargetServer. The value in the CR is the server identity with no trailing slash. On the wire, clients send the value with a trailing slash (for example everything.mcp.example/); the NetScaler matches the portion before the first slash.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-everything
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - type: MCPTargetServer
              name: x-netscaler-target-mcp-server
              value: everything.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-everything-backend
      backendRefs:
        - name: mcp-everything
          port: 3001
---
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-deepwiki
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - type: MCPTargetServer
              name: x-netscaler-target-mcp-server
              value: deepwiki.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-deepwiki-backend
      backendRefs:
        - name: mcp-deepwiki
          port: 443
---
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-github
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - type: MCPTargetServer
              name: x-netscaler-target-mcp-server
              value: github.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-github-backend
      backendRefs:
        - name: mcp-github
          port: 443
EOF
Note:
The mcp-everything route above uses port: 3001, which matches the ClusterIP Service on NetScaler CPX. On NetScaler VPX/MPX/BLX, the everything backend is published through the node port, so set the mcp-everything route backendRefs[].port to 31191 (the node port from step 1). The deepwiki and github routes are identical on both platforms.

5. Add server access control

Add a policy that inspects each MCP request and blocks disallowed servers or content before the request reaches a backend. Use a rewritepolicy with responder rules that returns a 403 JSON error, and attach it to a catch-all route so it evaluates for every request.
The example blocks any request whose target-server header contains malicious or untrusted, then denies every other request that reaches the catch-all — that is, any target that matches no configured route. Each rule ends with goto-priority-expression: END so evaluation stops after the first decision.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: rewritepolicy
metadata:
  name: mcp-access-control
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  responder-policies:
    # Explicit deny list: block named target servers on the routing header.
    - goto-priority-expression: "END"
      responder-policy:
        respond-criteria: 'HTTP.REQ.HEADER("x-netscaler-target-mcp-server").CONTAINS("malicious") || HTTP.REQ.HEADER("x-netscaler-target-mcp-server").CONTAINS("untrusted")'
        respondwith:
          http-payload-string: '"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n{\"error\":\"server not allowed\"}"'
        comment: "block disallowed MCP servers"
    # Default deny: anything else that reaches the catch-all route.
    - goto-priority-expression: "END"
      responder-policy:
        respond-criteria: 'true'
        respondwith:
          http-payload-string: '"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n{\"error\":\"unauthorized MCP server access\"}"'
        comment: "deny unrouted MCP servers"
EOF
Attach the policy to a catch-all route. A route with default: true matches all traffic on the Gateway, so the access control policy runs for every request.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-catchall
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - default: true
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: rewritepolicy
            name: mcp-access-control
      backendRefs:
        - name: mcp-everything
          port: 3001
EOF
Note:
On NetScaler VPX/MPX/BLX, set the catch-all route backendRefs[].port to the everything node port (31191), the same as the mcp-everything route.

6. Add tool-based rate limiting

Add rate limits to protect the MCP servers. This example uses two limits: a Gateway-wide limit per target server, and a per-tool limit on a specific route.
The Gateway-wide limit caps how many requests each target server accepts. It uses targetRef to bind to the Gateway and keys on the target-server header (the portion before the first slash).
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: ratelimit
metadata:
  name: mcp-rl-target
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  targetRef:
    - name: mcp-gw
      namespace: default
      kind: Gateway
      group: gateway.networking.k8s.io
  selector_keys:
    advanced:
      # Bucket per MCP server: the routing header value before the trailing slash.
      rules:
        - 'HTTP.REQ.HEADER("x-netscaler-target-mcp-server").BEFORE_STR("/")'
      include_base_selectors: false
  goto_priority_expression: END
  ratelimits:
    - req_threshold: 30
      timeslice: 60000
      limittype: BURSTY
      mode: REQUEST_RATE
      throttle_action: "RESPOND"
      respond_with: '"HTTP/1.1 429 Too Many Requests\r\nContent-Type: application/json\r\nRetry-After: 60\r\nConnection: close\r\n\r\n{\"error\":\"Rate limit exceeded for MCP backend - retry after 60s\"}"'
EOF
The per-tool limit caps how often a single MCP tool can be invoked. It attaches to one route through an ExtensionRef filter, matches only tools/call requests, and keys on the tool name inside the JSON-RPC body. Over-quota calls receive a JSON-RPC error.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: ratelimit
metadata:
  name: mcp-rl-tool
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  selector_keys:
    advanced:
      # Bucket per tool name from the JSON-RPC body.
      rules:
        - 'HTTP.REQ.BODY(100000).XPATH_JSON(xp%/params/name%)'
      include_base_selectors: false
      # Count only tools/call, so the initialize and tools/list handshake stays outside the quota.
      match: 'http.req.header("Content-Type").EQ("application/json") && http.req.body(100000).xpath_json(xp%boolean(//method)%) && http.req.body(100000).xpath_json(xp%/method%).eq("tools/call")'
  goto_priority_expression: END
  ratelimits:
    - req_threshold: 3
      timeslice: 120000
      limittype: BURSTY
      mode: REQUEST_RATE
      throttle_action: "RESPOND"
      respond_with: '"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32003,\"message\":\"rate limit exceeded\"},\"id\":null}"'
EOF
Attach the per-tool limit to the everything route by adding it as a second ExtensionRef filter. Reapply the route with both filters. On NetScaler VPX/MPX/BLX, set port to 31191.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-everything
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - type: MCPTargetServer
              name: x-netscaler-target-mcp-server
              value: everything.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-everything-backend
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: ratelimit
            name: mcp-rl-tool
      backendRefs:
        - name: mcp-everything
          port: 3001
EOF
Important:
goto_priority_expression: END is required whenever throttle_action is RESPOND. A RESPOND action is a terminating responder, and NetScaler rejects a terminating responder bound with the default NEXT. Set it at the spec level, as shown.

7. Add credential injection and client authentication

Protected servers such as github require an upstream credential, and you can also require clients to authenticate before the Gateway forwards their traffic. This step adds both in front of the github server.

Credential injection (NetScaler CPX and VPX/MPX/BLX)

The Gateway can attach an upstream credential to every request it forwards to a server, so clients never handle the server's token. Store the credential in a Secret and turn on header injection in the server's back-end profile.
The Secret key is token, and its value is the complete header line the NetScaler injects. Replace <github-pat> with a valid token.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: mcp-github-token
  namespace: default
type: Opaque
stringData:
  token: "Authorization: Bearer <github-pat>"
EOF
Reapply the github back-end profile with injection enabled and the Secret referenced through mcpTokenOrApi.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcpprofile
metadata:
  name: mcp-github-backend
  namespace: default
spec:
  config:
    profileType: BACKEND
    proxyMode: FORWARD
    hostReplacement: ENABLED
    urlReplacement: DISABLED
    insertHeaderInClientRequest: ENABLED
    mcpTokenOrApi:
      name: mcp-github-token
      key: token
    comment: "github backend with injected token"
EOF
Important:
The token value must be the full header line, for example Authorization: Bearer <token>, and must not exceed 255 characters. A bare scheme such as Bearer alone is rejected. To pass a distinct per-user token instead of a shared one, enable insertHeaderInClientRequest without mcpTokenOrApi; the NetScaler then forwards the token the client supplies in the X-Netscaler-Backend-Auth-Token header.

OAuth 2.1 client authentication (NetScaler VPX/MPX/BLX only)

Important:
This section applies only to NetScaler VPX/MPX/BLX. Client authentication for MCP runs on the AAA-governed path and is not available on NetScaler CPX. Skip this section if you deploy on NetScaler CPX.
Require clients to present a valid OAuth 2.1 bearer token before the Gateway forwards their traffic. Create an authpolicy that validates tokens against your identity provider, then attach it to the github route through an ExtensionRef filter. The NetScaler validates the client token, strips it, and then the back-end profile injects the upstream credential toward github.
The following authpolicy validates JWT access tokens against a JWKS endpoint. Replace the issuer, JWKS URI, and audience with your identity provider's values. authentication_host_cert.tls_secret references the Gateway's TLS Secret.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: authpolicy
metadata:
  name: mcp-authjwks
  namespace: default
spec:
  authentication_mechanism:
    using_request_header: "ON"
    authentication_host_cert:
      tls_secret: mcp-gateway-tls
  authentication_providers:
    - name: "mcp-oauth"
      oauth:
        issuer: "https://<issuer>/"
        jwks_uri: "https://<issuer>/.well-known/jwks.json"
        audience:
          - "<audience>"
        signature_algorithms:
          - "RS256"
        user_field: "sub"
  authentication_policies:
    - expression: "true"        # authenticate every request that reaches the tagged route
      provider: ["mcp-oauth"]
  authorization_policies:
    - expression: "true"        # allow any authenticated caller (tighten with a claim or path rule)
EOF
Attach the policy to the github route by adding it as an ExtensionRef filter alongside the back-end profile. Reapply the route.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-github
  namespace: default
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - type: MCPTargetServer
              name: x-netscaler-target-mcp-server
              value: github.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-github-backend
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: authpolicy
            name: mcp-authjwks
      backendRefs:
        - name: mcp-github
          port: 443
EOF
Note:
An authorization_policies block is required to admit authenticated requests. If it is absent, authenticated clients are denied by default. Start with the permissive rule shown here, then tighten it with claim matches for production.

8. Enable Splunk observability

Export MCP transaction records to Splunk over HTTP Event Collector (HEC). Create a Secret with the HEC token, then a ConfigMap that turns on analytics and points the NetScaler at your Splunk endpoint.
Create the HEC token Secret. The key is authToken. Replace <hec-token> with your Splunk HEC token.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: mcp-splunk-hec
  namespace: default
type: Opaque
stringData:
  authToken: "<hec-token>"
EOF
Add NS_ANALYTICS_CONFIG to the ConfigMap the Gateway Controller watches. Replace cic-configmap with that ConfigMap's name, and set <splunk-host> and the port to your Splunk HEC endpoint. mcpSummary: ENABLED turns on the MCP transaction summary records.
kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: cic-configmap
  namespace: default
data:
  NS_ANALYTICS_CONFIG: |
    splunk:
      enable: 'true'
      servers:
        - name: mcp-splunk
          endpoint: '<splunk-host>'
          port: 8088
          authToken:
            name: mcp-splunk-hec
          profiles:
            webinsight:
              httpURL: ENABLED
              httpHost: ENABLED
              dataFormatFile: splunk_format.txt
              mcpSummary: ENABLED
              analyticsEndpointUrl: "/services/collector"
              analyticsEndpointContentType: "application/json"
EOF
Important:
NS_ANALYTICS_CONFIG is read only from the ConfigMap that the controller's --configmap argument names. Applied under any other name, it has no effect and raises no error. Applying a ConfigMap replaces its data section in full, so merge NS_ANALYTICS_CONFIG into the existing ConfigMap rather than overwriting it. The dataFormatFile value must name a format file that exists on the NetScaler; the stock splunk_format.txt ships with NetScaler.

9. Drive traffic and verify

Send MCP traffic through the Gateway and confirm it reaches each server. MCP is stateful: begin a session with initialize, capture the mcp-session-id response header, send notifications/initialized, and then call tools.
Direct the request to a server with the x-netscaler-target-mcp-server header. The value ends with a trailing slash on the wire.

Send a request

Set the endpoint for your platform:
  • On NetScaler CPX, the endpoint is HTTP on the CPX pod IP and the listener port. Get the virtual server IP from the Gateway status:
    kubectl get gateway mcp-gw -o jsonpath='{.status.addresses[0].value}'
    The endpoint is http://<gateway-ip>:8891/mcp.
  • On NetScaler VPX/MPX/BLX, the endpoint is HTTPS on the virtual server IP and the listener port: https://<vpx-vip>:8443/mcp. Add -k to curl if the certificate is self-signed.
Initialize a session against the everything server (CPX shown; for VPX use the HTTPS endpoint):
curl -i http://<gateway-ip>:8891/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'x-netscaler-target-mcp-server: everything.mcp.example/' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
The response is 200 OK and includes an mcp-session-id header. Reuse that value on the follow-up requests:
curl -i http://<gateway-ip>:8891/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'x-netscaler-target-mcp-server: everything.mcp.example/' \
  -H 'mcp-session-id: <session-id>' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

curl -i http://<gateway-ip>:8891/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'x-netscaler-target-mcp-server: everything.mcp.example/' \
  -H 'mcp-session-id: <session-id>' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Expected results:
Request Expected result
initialize to everything.mcp.example/ 200 OK with an mcp-session-id header
tools/list to everything.mcp.example/ 200 OK with the server's tool list
initialize to deepwiki.mcp.example/ 200 OK; the remote server responds over TLS
Header without the trailing slash (everything.mcp.example) 403 from the access control policy; the target does not match
Same tool called more than 3 times in 2 minutes JSON-RPC -32003 rate limit error
github.mcp.example/ with no valid client token (VPX) 401 Unauthorized from the AAA path
github.mcp.example/ with a valid client token (VPX) 200 OK; the Gateway injects the upstream token

Verify on NetScaler

Confirm the control-plane objects the controller programmed and that traffic counters move.
On NetScaler CPX, run NetScaler CLI commands through the CPX pod:
kubectl exec <cpx-pod> -c cpx-ingress -- cli_script.sh "show cs vserver"
kubectl exec <cpx-pod> -c cpx-ingress -- cli_script.sh "show lb vserver"
kubectl exec <cpx-pod> -c cpx-ingress -- cli_script.sh "show servicegroup"
kubectl exec <cpx-pod> -c cpx-ingress -- cli_script.sh "show mcpprofile"
On NetScaler VPX/MPX/BLX, run the same show commands from the NetScaler CLI or NITRO API.
Confirm that:
  • One content switching virtual server fronts the MCP traffic and carries the front-end MCP profile.
  • One load balancing virtual server and service group exist per routed server, each carrying its back-end MCP profile.
  • The ExternalName servers (deepwiki, github) resolve to DNS-based SSL service group members.
  • The content switching and load balancing virtual server hit counters increase as you send traffic, and the responder and rate limit counters increase when you trigger those policies.

Reference