.. _guide-helm-chart:
Kubernetes (Helm Chart)
=======================
Deploy the Zscaler MCP Server to **any** Kubernetes cluster via Helm — EKS, GKE, AKS, OpenShift, Rancher, k3s, Talos, kind / minikube for local dev. The chart is cluster-vendor-agnostic and never calls ``aws``, ``az``, or ``gcloud``. You bring the cluster; the chart brings the workload.
.. note::
**Need a hyperscaler-managed deploy instead?** This isn't for you. Use:
- **AWS Bedrock AgentCore** — :doc:`amazon-bedrock-agentcore`
- **Azure Container Apps / VM / AKS-Preview script** — :doc:`azure-deployment`
- **GCP Cloud Run / GKE-script / Compute Engine** — :doc:`gcp-cloud-run`
Those scripts provision and manage the underlying cloud infrastructure (clusters, networks, IAM, Key Vaults, etc.) end-to-end. **This chart assumes you already have a Kubernetes cluster** and want to install one more workload into it.
When to Use This vs. Other Deploy Options
-----------------------------------------
.. list-table::
:header-rows: 1
:widths: 60 40
* - You want to…
- Use
* - Install into an existing K8s cluster (any cloud, any distro, on-prem)
- **This chart**
* - Wire into ArgoCD / Flux / a corporate GitOps pipeline
- **This chart** (Helm-source ``Application`` or ``HelmRelease``)
* - Run locally on Claude Code / Cursor / Gemini CLI without containers
- ``uvx zscaler-mcp``
* - Run a single container without Kubernetes
- :doc:`docker`
* - Have AWS host the runtime for you on Bedrock
- :doc:`amazon-bedrock-agentcore`
* - Stand up brand-new Azure infra and deploy on top
- :doc:`azure-deployment`
* - Stand up brand-new GCP infra and deploy on top
- :doc:`gcp-cloud-run`
This chart is the right answer when **the cluster is already a fact** and your operating model treats every workload as a Helm release.
Why a Helm chart at all?
------------------------
The MCP server is an HTTP service that needs credentials, an ingress, a few ``kubectl``-flavoured knobs (replicas, resources, probes), and the option to bring its own pre-existing Kubernetes ``Secret``. Helm encodes that contract once and lets it run identically on:
- **EKS** with IRSA-fed Secrets via `External Secrets Operator (ESO) `__ → AWS Secrets Manager
- **GKE** with Workload Identity + Secret Manager via ESO
- **AKS** with Workload Identity Federation + Azure Key Vault via ESO **or** the Key Vault CSI driver
- **OpenShift** with ``Secret`` provisioned by the OpenShift secret-injection operator
- **Vanilla / on-prem K8s** with `HashiCorp Vault Agent Injector `__, `SealedSecrets `__, or sops-encrypted GitOps
- **Local dev** (kind / minikube / colima) with an inline ``Secret`` rendered by the chart
In each of those cases, the cluster-side Helm command is **identical**; only the source-of-credentials story differs.
Prerequisites
-------------
- Kubernetes **1.24+**
- `Helm 3.0+ `__
- A Zscaler OneAPI client — ``ZSCALER_CLIENT_ID``, ``ZSCALER_CLIENT_SECRET`` (or ``ZSCALER_PRIVATE_KEY``), ``ZSCALER_VANITY_DOMAIN``, and ``ZSCALER_CUSTOMER_ID`` (for ZPA tools)
- (Optional) `cert-manager `__ for auto-issued TLS certs
- (Optional) `External Secrets Operator `__ or another secret-injection mechanism for production credential storage
- (Optional) An Ingress controller (NGINX, Traefik, ALB, etc.) **or** Gateway API v1 if you want to expose the MCP endpoint outside the cluster
Credential Setup — Choose Your Path
-----------------------------------
The chart never asks you to translate your ``.env`` into ``values.yaml`` syntax. Pick the path that matches how your team already manages secrets:
.. list-table::
:header-rows: 1
:widths: 5 35 60
* - #
- Path
- When to use
* - 1
- **Interactive script** (``helm_mcp_operations.py deploy``)
- Local dev, kind / minikube, day-1 walkthroughs. **Recommended starting point.**
* - 2
- **Manual ``kubectl + helm``** with ``.env``
- CI pipelines, GitOps reconcilers (Argo, Flux).
* - 3
- **Inline ``--set`` credentials**
- Quick local smoke tests, templating pipelines. Never use for production.
* - 4
- **Pre-existing ``Secret``** (GitOps)
- ArgoCD / Flux + SealedSecrets / sops-encrypted manifests.
* - 5
- **External Secrets Operator**
- Production with AWS Secrets Manager / Azure Key Vault / GCP Secret Manager / Vault / 1Password.
All five paths converge on the same chart contract: the Deployment uses ``envFrom: secretRef:`` to bulk-import every key in the Secret as an environment variable. Whatever ``ZSCALER_MCP_*`` / ``ZSCALER_*`` variable you put in your ``.env`` (or remote-secret backend) flows into the container without translation.
Quick Start
-----------
1. Interactive guided install via ``helm_mcp_operations.py`` (recommended)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The chart ships an interactive deployment script that mirrors the GCP / Azure ones. It reads your existing ``.env``, materialises it into a Kubernetes ``Secret``, runs ``helm upgrade --install``, waits for the rollout, starts ``kubectl port-forward``, and writes Cursor / Claude Desktop entries automatically.
.. code-block:: bash
python integrations/helm-chart/helm_mcp_operations.py deploy
You'll be asked, in order: which kubectl context to target, the path to your ``.env`` file (defaults to the project root), namespace name, Helm release name, image tag, and how to expose the endpoint (port-forward / Ingress / none).
Follow-up commands:
.. code-block:: bash
python integrations/helm-chart/helm_mcp_operations.py status # release + pods + svc + port-forward
python integrations/helm-chart/helm_mcp_operations.py logs # tail Deployment logs
python integrations/helm-chart/helm_mcp_operations.py configure # re-write Cursor / Claude configs
python integrations/helm-chart/helm_mcp_operations.py test # run `helm test` smoke probe
python integrations/helm-chart/helm_mcp_operations.py destroy # uninstall + optional ns deletion
2. Manual install with raw ``helm`` + an existing ``.env``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
kubectl create namespace zscaler-mcp
kubectl -n zscaler-mcp create secret generic zscaler-mcp-creds \
--from-env-file=/path/to/.env
helm install zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
--namespace zscaler-mcp \
--set secret.create=false \
--set secret.existingName=zscaler-mcp-creds
kubectl -n zscaler-mcp rollout status deployment/zscaler-mcp-zscaler-mcp-server
kubectl -n zscaler-mcp port-forward svc/zscaler-mcp-zscaler-mcp-server 8000:80 &
curl http://localhost:8000/health
3. Local dev with inline ``--set`` credentials
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Not for production.**
.. code-block:: bash
helm install zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
--namespace zscaler-mcp --create-namespace \
--set secret.values.clientId=$ZSCALER_CLIENT_ID \
--set secret.values.clientSecret=$ZSCALER_CLIENT_SECRET \
--set secret.values.vanityDomain=$ZSCALER_VANITY_DOMAIN \
--set secret.values.customerId=$ZSCALER_CUSTOMER_ID
4. Production with pre-existing Secret (GitOps-friendly)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create the Secret out-of-band (External Secrets Operator, Vault Agent Injector, SealedSecrets — your choice). The chart will reference it by name:
.. code-block:: bash
kubectl create namespace zscaler-mcp
kubectl -n zscaler-mcp create secret generic zscaler-mcp-creds \
--from-literal=ZSCALER_CLIENT_ID="$ZSCALER_CLIENT_ID" \
--from-literal=ZSCALER_CLIENT_SECRET="$ZSCALER_CLIENT_SECRET" \
--from-literal=ZSCALER_VANITY_DOMAIN="$ZSCALER_VANITY_DOMAIN" \
--from-literal=ZSCALER_CUSTOMER_ID="$ZSCALER_CUSTOMER_ID"
helm install zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
--namespace zscaler-mcp \
--values - <<'EOF'
secret:
create: false
existingName: zscaler-mcp-creds
ingress:
enabled: true
className: nginx
hosts:
- host: zscaler-mcp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: zscaler-mcp-tls
hosts:
- zscaler-mcp.example.com
certificate:
enabled: true
secretName: zscaler-mcp-tls
commonName: zscaler-mcp.example.com
dnsNames:
- zscaler-mcp.example.com
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
EOF
5. Production with External Secrets Operator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Assumes ESO is installed and a ``ClusterSecretStore`` is wired to your secret backend (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault, 1Password, etc.).
.. code-block:: yaml
# external-secret.yaml — apply this BEFORE helm install
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: zscaler-mcp-creds
namespace: zscaler-mcp
spec:
refreshInterval: 1h
secretStoreRef:
name: my-cluster-secret-store
kind: ClusterSecretStore
target:
name: zscaler-mcp-creds
data:
- secretKey: ZSCALER_CLIENT_ID
remoteRef: { key: zscaler/mcp/client_id }
- secretKey: ZSCALER_CLIENT_SECRET
remoteRef: { key: zscaler/mcp/client_secret }
- secretKey: ZSCALER_VANITY_DOMAIN
remoteRef: { key: zscaler/mcp/vanity_domain }
- secretKey: ZSCALER_CUSTOMER_ID
remoteRef: { key: zscaler/mcp/customer_id }
.. code-block:: bash
kubectl apply -f external-secret.yaml
helm install zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
--namespace zscaler-mcp \
--set secret.create=false \
--set secret.existingName=zscaler-mcp-creds
Configuration Reference
-----------------------
Every key below is also documented inline in `charts/zscaler-mcp-server/values.yaml `__.
Image
~~~~~
.. list-table::
:header-rows: 1
:widths: 30 25 45
* - Key
- Default
- Description
* - ``image.repository``
- ``zscaler/zscaler-mcp-server``
- Docker Hub repo. Override to point at a private mirror / Marketplace ECR.
* - ``image.tag``
- ``latest``
- Docker Hub currently publishes only the ``latest`` floating tag. Pin in production via ``image.digest``.
* - ``image.digest``
- ``""``
- Pin by digest (``sha256:...``). When set, wins over ``image.tag``. Recommended for production.
* - ``image.pullPolicy``
- ``IfNotPresent``
-
* - ``imagePullSecrets``
- ``[]``
- Image pull Secrets for private registries.
Service / Ingress / HTTPRoute
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``ingress.enabled`` and ``httproute.enabled`` are **mutually exclusive** — picking both fails the install with a clear error.
.. list-table::
:header-rows: 1
:widths: 30 25 45
* - Key
- Default
- Description
* - ``service.type``
- ``ClusterIP``
- ``ClusterIP`` / ``NodePort`` / ``LoadBalancer``.
* - ``service.port``
- ``80``
- Service port.
* - ``service.targetPort``
- ``8000``
- Container port — matches the MCP server default.
* - ``ingress.enabled``
- ``false``
- Generate a ``networking.k8s.io/v1`` Ingress.
* - ``ingress.className``
- ``""``
- e.g. ``nginx``, ``traefik``, ``alb``.
* - ``httproute.enabled``
- ``false``
- Generate a Gateway API v1 HTTPRoute instead.
* - ``certificate.enabled``
- ``false``
- Generate a cert-manager ``Certificate``.
MCP runtime (``mcp.*``)
~~~~~~~~~~~~~~~~~~~~~~~
These map 1:1 to the ``ZSCALER_MCP_*`` env vars the server already reads.
.. list-table::
:header-rows: 1
:widths: 40 20 40
* - Key
- Default
- Maps to
* - ``mcp.transport``
- ``streamable-http``
- ``--transport``
* - ``mcp.host``
- ``0.0.0.0``
- ``--host``
* - ``mcp.port``
- ``8000``
- ``--port``
* - ``mcp.auth.enabled``
- ``true``
- ``ZSCALER_MCP_AUTH_ENABLED``
* - ``mcp.auth.mode``
- ``zscaler``
- ``ZSCALER_MCP_AUTH_MODE``
* - ``mcp.toolsets.enabled``
- ``""``
- ``ZSCALER_MCP_TOOLSETS``
* - ``mcp.writeTools.enabled``
- ``false``
- ``ZSCALER_MCP_WRITE_ENABLED``
* - ``mcp.tls.allowHttp``
- ``true``
- ``ZSCALER_MCP_ALLOW_HTTP``. Required when TLS terminates at the Ingress / Gateway.
MCP Client Configuration
------------------------
Once the chart is installed, point your MCP client at the Service / Ingress hostname. The endpoint is ``/mcp``.
Recommended — ``helm_mcp_operations.py configure``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you installed the chart via Quick Start path 1, the script already wrote the right entry into Cursor + Claude Desktop. To rebuild those configs at any time:
.. code-block:: bash
python integrations/helm-chart/helm_mcp_operations.py configure
Manually — derive the auth header from the cluster Secret
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
kubectl --namespace zscaler-mcp get secret zscaler-mcp-creds \
-o jsonpath='{.data.ZSCALER_CLIENT_ID}:{.data.ZSCALER_CLIENT_SECRET}' \
| base64 -d | tr -d '\n' | base64
Prefix the resulting string with the literal ``Basic`` (followed by a space) to form the ``Authorization`` header value.
Port-forwarded local dev (no Ingress)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
kubectl -n zscaler-mcp port-forward svc/zscaler-mcp-zscaler-mcp-server 8000:80
# Then point your MCP client at http://localhost:8000/mcp
Operations
----------
Smoke-test the install:
.. code-block:: bash
helm test zscaler-mcp -n zscaler-mcp
Inspect the rendered manifests without installing:
.. code-block:: bash
helm template zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
--set secret.create=false \
--set secret.existingName=zscaler-mcp-creds \
--set ingress.enabled=true \
--set ingress.className=nginx
Upgrade in place:
.. code-block:: bash
helm upgrade zscaler-mcp \
./integrations/helm-chart/charts/zscaler-mcp-server \
-n zscaler-mcp \
-f my-values.yaml
Uninstall:
.. code-block:: bash
helm uninstall zscaler-mcp -n zscaler-mcp
Troubleshooting
---------------
.. list-table::
:header-rows: 1
:widths: 50 50
* - Symptom
- Likely cause / Fix
* - ``helm install`` fails with *"ingress.enabled and httproute.enabled are mutually exclusive"*
- Pick one and set the other to ``false``.
* - ``helm install`` fails with *"secret.create is false but secret.existingName is empty"*
- Set ``secret.existingName`` or flip ``secret.create: true``.
* - Pod CrashLoopBackOff with ``ZSCALER_VANITY_DOMAIN missing``
- Confirm ``secret.envKeys.vanityDomain`` matches the actual key name in your pre-existing Secret.
* - ``/health`` returns 200 but ``/mcp`` returns 401
- Auth header missing or wrong format. ``zscaler`` expects ``Authorization: Basic``; ``jwt`` / ``api-key`` expect ``Authorization: Bearer``.
* - MCP client sees zero tools
- Entitlement filter trimmed everything. Your OneAPI client isn't entitled to the loaded toolsets. Either request entitlements, or set ``mcp.disableEntitlementFilter: true`` (emergency override only).
For deeper debugging:
.. code-block:: bash
kubectl -n zscaler-mcp logs deploy/zscaler-mcp-zscaler-mcp-server --tail=200 -f
kubectl -n zscaler-mcp describe pod -l app.kubernetes.io/name=zscaler-mcp-server
kubectl -n zscaler-mcp get events --sort-by='.lastTimestamp' | tail -30
For the full chart contract — every value key, every template, the deployer script reference, GitOps integration recipes, and the complete troubleshooting matrix — see `integrations/helm-chart/README.md `__.