Knowledge Base MCP - Deployment
4 min read
Prerequisites
kb-mcp has no knowledge-base data of its own: every tool call is a live proxy to your tenant's Unique API, so a Unique platform subscription is a hard requirement. Where kb-mcp itself runs is flexible: the Helm chart below targets Kubernetes, but the same Docker image runs on any other cloud or locally. Only the environment variables change. Zitadel is likewise fixed: it's always the same instance your Unique AI tenant already uses, never one you stand up yourself.
Kubernetes 1.25+ with a Gateway API implementation (the chart exposes itself via
HTTPRoute, not a classicIngress)Helm 3.x with OCI support
Postgres 14+ for OAuth-proxy state
The public (PKCE) OIDC client id for
kb-mcp, registered in your tenant's existing Zitadel instanceNetwork access from
kb-mcp's pod to the Unique API instance it calls
Helm Chart
kb-mcp ships as a standalone Helm chart, published as an OCI artifact alongside every release:
helm upgrade --install kb-mcp oci://ghcr.io/unique-ag/connectors/helm/kb-mcp \
--version <version> \
--namespace <namespace> --create-namespace \
--values values.yamlThe chart lives in Unique-AG/connectors under services/kb-mcp/deploy/helm-charts/kb-mcp. Unique's own deployments reference it from ArgoCD by targetRevision: kb-mcp@<version>.
Required Secrets
Two values, generated with openssl rand -hex 32, neither with a chart default:
Secret | Purpose |
|---|---|
| Signs |
| Encrypts OAuth-proxy state at rest in Postgres |
Deliver them through envVars[].valueFrom.secretKeyRef, or, at Unique, through ExternalSecrets via extraEnvSecrets.
You'll also need mcpConfig.zitadel.clientId, the public (PKCE) OIDC client id for kb-mcp, registered in Zitadel with redirect URI {publicBaseUrl}/auth/callback. It is public, so it sits in plain chart values; see Configuration for the full required-variable reference.
Minimal Values
mcpConfig:
enabled: true
app:
publicBaseUrl: https://kb-mcp.<tenant>.unique.app # must match routes.hostname
zitadel:
baseUrl: https://id.<tenant>.example.com
clientId: <public PKCE client id> # PKCE, not secret
envVars:
- name: UNIQUE_API_BASE_URL
value: http://unique-api.<namespace>.svc.cluster.local
- name: ZITADEL_JWT_SIGNING_KEY
valueFrom:
secretKeyRef: { name: kb-mcp-secrets, key: zitadel-jwt-signing-key }
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef: { name: kb-mcp-secrets, key: encryption-key }
routes:
hostname: kb-mcp.<tenant>.unique.app
pathPrefix: / # chart default is /kb-mcp; this keeps the URL at the bare /mcp this guide uses
auth:
jwt: false # kb-mcp runs its own OIDC, don't double-gate at the gateway
paths:
default: { enabled: true }
probe: { enabled: true }DATABASE_URL isn't listed here: postgresql.enabled: true is the chart default, but postgresql.connection is required either way and ships empty, so it's never actually minimal. See PostgreSQL below for both paths (bring your own CloudNativePG Cluster, or an external instance).
PostgreSQL
Postgres holds OAuth-proxy state only: client registrations, tokens, and JTI mappings. It never holds knowledge-base data. postgresql.enabled: true is the chart default, and postgresql.connection is required whenever it's on, whichever Postgres you use: the chart neither creates nor owns one.
In-cluster: bring your own CloudNativePG Cluster and point at it:
postgresql:
enabled: true
connection:
host:
fromKubernetesService:
name: <release-name>-pg-cluster-rw
namespace: <namespace>
podPort: 5432
podSelectorLabels:
cnpg.io/cluster: <release-name>-pg-cluster
url:
fromSecret:
name: <release-name>-pg-cluster-app
key: fqdn-urihost and url do different jobs here: host only shapes the network-policy egress rule (same toEndpoints mechanism as internalServices.dependencies, see Network Policies below), while url is the actual DSN, generated by your Cluster into its own secret, and is what becomes DATABASE_URL.
External: point at a managed instance instead by supplying DATABASE_URL as a secret env var and setting postgresql.enabled: false, bypassing the postgresql.connection contract entirely.
Ephemeral storage is local-development only
ALLOW_EPHEMERAL_OAUTH_STORAGE=true skips Postgres entirely using a per-process file store. State does not survive a restart and is not shared across replicas, so every user is logged out on each pod restart.
Network Policies
networkPolicy.enabled defaults to false (flavor: cilium); set it to true for a default-deny CiliumNetworkPolicy. kb-mcp needs egress to DNS, the Unique API, Zitadel and the platform gateway, and its Postgres host if external. The chart's baseline only allows toFQDNs: ["*.unique.app"] on 443, so an in-cluster Unique API address (the UNIQUE_API_BASE_URL in Minimal Values) is not covered: add its egress rule in your overlay, under networkPolicy.egress[] or as an internalServices.dependencies.<key> entry, which builds the rule from servicePort and podPort. The chart does not emit UNIQUE_API_BASE_URL from it, so keep setting that under envVars.
Cilium's eBPF Service DNAT happens before a CiliumNetworkPolicy's toEndpoints rule is evaluated, so the rule must allow the container's actual listening port. When the Service port and the container port differ, set podPort to the container port.
The allowlist is bidirectional
Egress from kb-mcp is only half of it. The Unique API must also allow kb-mcp in its own ingress policy. Otherwise it drops the connection silently, with no application error, just a timeout.
Health Checks
GET /probe serves all three probes (liveness, readiness, startup).
Monitoring
Logs: pino-json on stderr, scraped by Loki via the
logging.unique.app/format=pino-jsonpod label the chart sets by default. Access-log lines for/probe,/health, and/metricsare silenced.Metrics/tracing: opt-in via OpenTelemetry; unset in the chart defaults.
Troubleshooting
Startup hangs under a default-deny network policy: FastMCP pings
pypi.orgon startup. SetFASTMCP_CHECK_FOR_UPDATES: "off".Tool calls hang or time out: there is probably no egress rule for the Unique API, or it targets the Service port instead of the pod port. See Network Policies.
Server refuses to boot:
kb-mcprequires bothDATABASE_URLandENCRYPTION_KEYunlessALLOW_EPHEMERAL_OAUTH_STORAGE=true.