This guide explains how to deploy Frontegg's MCP Gateway Helm chart in your Kubernetes cluster. The chart packages two services that expose an MCP-compliant authorization and tool routing layer in front of your MCP servers.
| Component | Role |
|---|---|
mcp-auth | Serves OAuth, dynamic client registration, authorization, and callback endpoints for MCP clients. |
mcp-gw | Receives MCP requests and routes or authorizes them against Frontegg. |
Both components share a Redis backend for token and session caching and are configured against a Frontegg vendor using region, client credentials, and applicationId.
For each release, the chart renders:
Deployment/<release>-mcp-gateway-auth, which runs themcp-authimage.Deployment/<release>-mcp-gateway-gw, which runs themcp-gwimage.Service/<release>-mcp-gateway-auth, a ClusterIP service onmcpAuth.port.Service/<release>-mcp-gateway-gw, a ClusterIP service onmcpGw.port.- A shared
ServiceAccount, which is optional and enabled by default. - HorizontalPodAutoscaler resources per component, which are optional and disabled by default.
The chart does not ship an Ingress, API gateway, or other layer-7 router. You must front the two services with the path-matching ingress or API gateway you already use, such as NGINX Ingress, Traefik, Istio, Envoy, AWS ALB, GCP HTTPS LB, or Kong. See Routing for the path map you need to configure.
helm repo add frontegg https://frontegg.github.io/helm-charts/
helm repo update
helm upgrade --install mcp-gateway frontegg/mcp-gateway -f my-values.yamlEvery value under env must be filled in for a working deployment. Names are camelCase in values.yaml and are translated to UPPER_SNAKE_CASE environment variables on both containers. For example, vendorClientId becomes VENDOR_CLIENT_ID.
env:
# Redis shared cache for sessions and tokens.
redisHost: my-redis.example.com
redisPort: "6379"
redisPassword: "<redis-password>"
redisDb: "0"
redisTlsEnabled: "true"
cacheTtl: "300"
# Frontegg vendor.
fronteggRegion: "eu" # eu | us | ca | au | stg
vendorClientId: "<vendor-client-id>"
vendorClientSecret: "<vendor-client-secret>"
applicationId: "<application-id>"
# The hostname this gateway will be reached at, without scheme.
fronteggMcpGwHost: "tenant.mcp-gw.frontegg.com"
# The same host with scheme, used as the OAuth issuer and authorization URL.
externalAuthorizationUrl: "https://tenant.mcp-gw.frontegg.com"
# Your hybrid auth backend, with scheme.
hybridAuthHost: "https://hybrid-auth.customer.example.com"Optional environment values:
| Key | Purpose |
|---|---|
secretEncryptionKey | 32-character key used to decrypt integration secrets (OAuth client secrets and API keys) that Frontegg delivers encrypted. See Integration secret encryption. |
approvalFlowWebhookEndpoint | Webhook that receives tool-call approval requests. |
eventStdoutEnabled | Set to "true" to write events to the pod's stdout instead of a webhook. See Event delivery. |
eventWebhookProvider | Where to forward audit events: datadog, splunk, coralogix, or webhook. |
eventWebhookUrl | Destination URL for the event webhook. |
eventWebhookSecret | Shared secret for signing event webhook deliveries. |
These are commented out in the default values.yaml. Uncomment and set them if needed.
The two services must sit behind a path-matching HTTP router, such as an Ingress controller, service mesh gateway, or cloud API gateway. Configure it so that the following auth-related paths land on mcp-auth and everything else lands on mcp-gw:
| Path | Target service |
|---|---|
/.well-known/* | <release>-mcp-gateway-auth |
/authorize | <release>-mcp-gateway-auth |
/dcr/register | <release>-mcp-gateway-auth |
/token | <release>-mcp-gateway-auth |
/integration-callback | <release>-mcp-gateway-auth |
/security-stepup-verify | <release>-mcp-gateway-auth |
/external-mcp/authorize | <release>-mcp-gateway-auth |
/external-mcp/callback | <release>-mcp-gateway-auth |
Everything else, / | <release>-mcp-gateway-gw |
Your router must satisfy these requirements:
- Order and specificity: The auth paths above must win over the catch-all
/route. Most ingress controllers do this automatically based on prefix length. On routers that match in declared order, declare the auth rules first. - Host header preservation: The
Hostheader of the incoming request must matchenv.fronteggMcpGwHost. Both services use it to validate issuer URLs. If your gateway rewrites the upstream host, configure it to pass the original host. - Single external hostname: Both services are reached on the same external host. Only the path differs.
Image tags are pinned in the chart and are bumped by Frontegg as part of releasing a new chart version. Do not override mcpAuth.tag or mcpGw.tag. To pick up a new image, upgrade to a newer chart version:
helm repo update
helm upgrade mcp-gateway frontegg/mcp-gateway -f my-values.yamlEach component has independent resources blocks under mcpAuth.resources and mcpGw.resources. Defaults are conservative, with 200m CPU and 256Mi memory requests and a 512Mi memory limit, and are suitable for low-traffic tenants.
Autoscaling is disabled by default. Enable it per component:
autoscaling:
mcpGw:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
mcpAuth:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 70When autoscaling.<component>.enabled is true, the Deployment's replicas field is omitted so the HPA can manage it.
All three probes, liveness, readiness, and startup, hit GET /health on port http. Override per component if your environment needs different timings:
mcpGw:
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10nodeSelector, tolerations, affinity, podAnnotations, podLabels, podSecurityContext, and securityContext are top-level values and apply to both deployments. There is no per-component override today. To schedule the two components differently, install the chart twice with a nameOverride.
A single ServiceAccount is shared by both deployments. To use an existing one:
serviceAccount:
create: false
name: my-existing-saThe gateway emits audit events to a single sink. Choose one:
- Webhook: set
eventWebhookProvidertogether witheventWebhookUrl(andeventWebhookSecretif the provider requires it). Events are POSTed to your Datadog, Splunk, Coralogix, or generic webhook endpoint. - stdout: set
eventStdoutEnabled: "true". Each event is written to the pod's stdout as a single JSON line, tagged with a stablestreamfield so a log collector (such as the OpenTelemetry Collector, Fluent Bit, or Vector) can pick events out of the container log stream. This needs no external webhook infrastructure.
{"stream":"mcp-gw-events","event":{"eventDomain":"...","origin":"mcp-gw","context":{},"subject":{},"payload":{}}}For example, to keep only gateway events in an OpenTelemetry Collector pipeline:
processors:
filter/events:
logs:
log_record:
- 'body["stream"] != "mcp-gw-events"'Events are written directly to stdout, not through the application logger, so they are not affected by log-level settings.
By default, integration secrets (OAuth client secrets and API keys) are delivered to the gateway as plaintext. Set secretEncryptionKey to have Frontegg deliver them encrypted instead, so they are never stored as plaintext in your Redis cache. The gateway decrypts them at runtime, in memory, only when an integration is used.
- Algorithm: AES-256-GCM, encoded as
v2:<iv_hex>:<authTag_hex>:<ciphertext_hex>. secretEncryptionKeymust be a 32-character string, and identical to the key Frontegg encrypts with. Configure the same key in the Frontegg portal and here.- Values that are not in the
v2:format are passed through unchanged, so the feature can be enabled gradually.
The key is used as-is: its raw characters are the AES-256 key, so the string must be exactly 32 characters (a 32-byte UTF-8 key). Do not generate a 32-byte binary key and then Base64- or hex-encode it for the environment variable — that produces a 44- or 64-character string, which is no longer 32 bytes and fails with an
Invalid key lengtherror. Use a plain 32-character string, and use only ASCII characters (a multi-byte UTF-8 character would push the byte length past 32).
Source the key from a Kubernetes Secret rather than committing it to values.yaml.
Generate a valid 32-character key:
node -e "console.log(require('node:crypto').randomBytes(16).toString('hex'))"randomBytes(16) encoded as hex is 32 characters (each byte becomes two hex characters), and those 32 ASCII characters are exactly 32 bytes when used as the key. Use the printed string verbatim as secretEncryptionKey — do not encode it again.
To encrypt a secret with that key in the format the gateway expects:
const { createCipheriv, randomBytes } = require('node:crypto');
function encryptSecret(plaintext, key /* 32-character string */) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return `v2:${iv.toString('hex')}:${authTag.toString('hex')}:${ciphertext.toString('hex')}`;
}
console.log(encryptSecret(process.argv[2], process.env.SECRET_ENCRYPTION_KEY));SECRET_ENCRYPTION_KEY="<your-32-character-key>" node encrypt-secret.js "my-client-secret"| Key | Default | Description |
|---|---|---|
mcpAuth.repository | 527305576865.dkr.ecr.us-east-1.amazonaws.com/docker-hub/frontegg/hybrid-agen-co-auth | mcp-auth image repository. Override only for private registry mirrors. |
mcpAuth.tag | Pinned by chart | Managed by Frontegg and bumped by chart release. Do not override. |
mcpAuth.pullPolicy | IfNotPresent | Image pull policy. |
mcpAuth.replicaCount | 1 | Used when autoscaling.mcpAuth.enabled is false. |
mcpAuth.port | 8080 | Service port. The container always listens on 8080. |
mcpAuth.resources | requests: {cpu: 200m, memory: 256Mi}, limits: {memory: 512Mi} | Default resource requests and limits. |
mcpAuth.{liveness,readiness,startup}Probe | GET /health on port http | Default health probes. |
mcpGw.* | Mirrors mcpAuth.* defaults | mcp-gw deployment configuration. |
imagePullSecrets | [] | List of name entries for private registry credentials. |
nameOverride and fullnameOverride | "" | Name override values. |
serviceAccount.create | true | Whether to create a ServiceAccount. |
serviceAccount.automount | true | Whether to automount the ServiceAccount token. |
serviceAccount.annotations | {} | Useful for IRSA or Workload Identity. |
serviceAccount.name | "" | If empty, defaults to the chart fullname. |
service.type | ClusterIP | Applied to both services. |
podAnnotations and podLabels | {} | Applied to both deployments' pod templates. |
podSecurityContext | {} | Pod-level security context. |
securityContext | capabilities.drop: [NET_RAW] | Container-level security context. |
autoscaling.mcpAuth.enabled | false | Whether autoscaling is enabled for mcp-auth. |
autoscaling.mcpAuth.minReplicas and maxReplicas | 1 and 100 | Replica bounds for mcp-auth. |
autoscaling.mcpAuth.targetCPUUtilizationPercentage | 80 | CPU utilization target. |
autoscaling.mcpAuth.targetMemoryUtilizationPercentage | Unset | Set to enable memory-based scaling. |
autoscaling.mcpGw.* | Mirrors autoscaling.mcpAuth.* | Autoscaling configuration for mcp-gw. |
volumes and volumeMounts | [] | Applied to both deployments. |
nodeSelector, tolerations, and affinity | {}, [], and {} | Applied to both deployments. |
env.redisHost | "" | Hostname of the shared Redis instance. |
env.redisPort | "6379" | Redis port. |
env.redisPassword | "" | Redis password. |
env.redisDb | "0" | Redis database. |
env.redisTlsEnabled | "true" | Set to "false" for plain Redis. |
env.cacheTtl | "300" | Token and session cache TTL in seconds. |
env.fronteggRegion | "eu" | Supported values are eu, us, ca, au, and stg. |
env.vendorClientId | "" | Frontegg vendor client ID. |
env.vendorClientSecret | "" | Frontegg vendor client secret. |
env.applicationId | "" | Frontegg application ID. |
env.fronteggMcpGwHost | "" | External hostname of the gateway, without scheme. |
env.externalAuthorizationUrl | "" | Same hostname with scheme, published as the OAuth issuer. |
env.hybridAuthHost | "" | URL of your hybrid auth service, with scheme. |
env.secretEncryptionKey | Commented | 32-character key to decrypt integration secrets Frontegg delivers encrypted (AES-256-GCM). |
env.approvalFlowWebhookEndpoint | Commented | Webhook for approval-flow callbacks. |
env.eventStdoutEnabled | Commented | Set to "true" to emit events to stdout instead of a webhook. |
env.eventWebhookProvider | Commented | Example: datadog. |
env.eventWebhookUrl | Commented | Destination URL for event webhooks. |
env.eventWebhookSecret | Commented | Shared secret for signing event webhooks. |
Every key under .Values.env is converted from camelCase to UPPER_SNAKE_CASE and emitted as a container environment variable on both containers. To add a new environment variable, add a key under env.
# Watch the rollout.
kubectl rollout status deploy/<release>-mcp-gateway-auth
kubectl rollout status deploy/<release>-mcp-gateway-gw
# Health checks. Both should return 200.
kubectl port-forward svc/<release>-mcp-gateway-auth 8080:8080 &
curl -fsS http://localhost:8080/health
kubectl port-forward svc/<release>-mcp-gateway-gw 8081:8080 &
curl -fsS http://localhost:8081/healthThere are no CRDs and no persistent state owned by the chart. Redis lives outside it.
Image versions are tied to chart versions. Each chart release pins the matching mcp-auth and mcp-gw images. To roll out a new image, upgrade the chart:
helm repo update
helm upgrade mcp-gateway frontegg/mcp-gateway --version <new-chart-version> -f my-values.yamlDo not override mcpAuth.tag or mcpGw.tag in your values file. Those are managed by Frontegg as part of the chart release.