# kubectl-mcp-server Read-only Kubernetes RCA (root cause analysis) MCP server. A diagnosis-only tool surface for an RCA bot / AI client (Bifrost) — it can **never** mutate cluster state. The image entrypoint is `k8s-rca-mcp`, configured **entirely via env vars** (no CLI args), and exposes only an allowlisted read-only toolset over streamable-http. > The chart/release keeps the name `kubectl-mcp-server` for continuity with > the existing ArgoCD apps and overrides. The underlying server is the > in-house env-driven RCA build (`prd/.../kubectl-mcp-server:v2`). ## Two modes (`mcp.mode`) | | `single` | `multi` (hub) | |---|---|---| | Scope | one cluster, in-cluster SA | federation router | | `cluster` tool param | none | **required** on every tool | | Deploy where | every cluster | one hub | | Credentials | own ServiceAccount | none for remotes — forwards MCP calls with a per-backend bearer token | In `multi` mode every tool gains a required `cluster` argument. `cluster == self` is served natively via the hub's in-cluster SA; any other registered name is resolved to a DNS endpoint and the identical tool call is forwarded over streamable-http to that cluster's `single`-mode instance. The hub holds **no remote kubeconfigs** — only endpoints and bearer tokens. ## Authentication Inbound: the MCP endpoint requires a bearer token. On http transport the server **refuses to start** unless `MCP_AUTH_TOKEN` is present (or `auth.allowAnonymous: true`). Clients send `Authorization: Bearer `. All bearer tokens (the inbound `MCP_AUTH_TOKEN` and, on a hub, every backend token) are stored in **Vault** and synced via an `ExternalSecret` — never in values or git. One ExternalSecret extracts a Vault path into the Secret `kubectl-mcp-server-creds`, which the deployment injects with `envFrom`, so each Vault key becomes a container env var verbatim. ```yaml externalSecrets: enabled: true refreshInterval: "150s" secretStoreRef: name: vault-backend kind: ClusterSecretStore dataFrom: secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server" # Vault path (configurable) ``` Store these **keys** at that Vault path (key names map 1:1 to env vars, so they must be valid env var names): | Vault key | Meaning | When | |---|---|---| | `MCP_AUTH_TOKEN` | inbound bearer the RCA agent must present | always | | `` per backend, e.g. `SUPPLY_MCP_TOKEN` | bearer the hub uses to call that backend | multi mode | ## Single-mode deploy Per cluster, set the Vault path and (optionally) the ingress host. Example override (`helm-overrides//kubectl-mcp-server/custom-values.yaml`): ```yaml mcp: mode: single transport: http host: "0.0.0.0" port: 8000 auth: allowAnonymous: false externalSecrets: enabled: true dataFrom: secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server" # must hold MCP_AUTH_TOKEN ``` Seed the Vault path: ```bash vault kv put meesho/stg/cntr/devop/kubectl-mcp-server \ MCP_AUTH_TOKEN=$(openssl rand -hex 32) ``` ## Multi-mode (hub): adding a cluster + its token Adding a backend is exactly two coordinated steps — a **registry entry** that names an env var, and a **Vault key of that same name** holding the value. ### 1. Add the cluster to the registry (`federation.clusters`) In the hub's override: ```yaml mcp: mode: multi externalSecrets: enabled: true dataFrom: secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server-hub" federation: forwardTimeout: 30 clusters: - name: self # the hub's own cluster, served natively self: true - name: supply # the name the RCA bot passes as cluster= endpoint: https://kubectl-mcp-server.supply.stg.meesho.int/mcp # must end in /mcp tokenEnv: SUPPLY_MCP_TOKEN # env var the hub reads this backend's bearer from - name: payments endpoint: https://kubectl-mcp-server.payments.stg.meesho.int/mcp tokenEnv: PAYMENTS_MCP_TOKEN ``` `tokenEnv` is a name you choose (convention: `_MCP_TOKEN`). It is the env var the hub looks up when forwarding to that cluster. ### 2. Add the matching token to Vault Add the backend tokens as keys at the hub's Vault path, named **exactly** like each `tokenEnv`: ```bash vault kv put meesho/stg/cntr/devop/kubectl-mcp-server-hub \ MCP_AUTH_TOKEN=$(openssl rand -hex 32) \ SUPPLY_MCP_TOKEN= \ PAYMENTS_MCP_TOKEN= ``` > **Critical:** the value of `SUPPLY_MCP_TOKEN` must equal the > `MCP_AUTH_TOKEN` that the **supply cluster's own** single-mode deployment > uses (from *its* Vault path). That is how the hub authenticates to it. Nothing else changes — `envFrom: secretRef` injects every Vault key automatically, so the new `*_MCP_TOKEN` env var just appears in the container. **Adding a cluster = one registry entry + one Vault key of the same name.** ### Request flow for one backend ``` RCA bot calls a tool with cluster="supply" │ ▼ hub registry: supply → endpoint + tokenEnv=SUPPLY_MCP_TOKEN │ └── value comes from Vault key SUPPLY_MCP_TOKEN (via envFrom) ▼ hub POSTs to https://...supply.../mcp with Authorization: Bearer ▼ supply single-mode pod checks it == its own MCP_AUTH_TOKEN ✓ → serves the tool result returned tagged with cluster="supply" ``` Structured errors (never silent): missing/empty `cluster` → `missing_cluster`; unknown name → `unknown_cluster` (both list available clusters); backend down or slow → `backend_unreachable` / `backend_timeout`, tagged with the cluster. ## Key values | Key | Default | Notes | |---|---|---| | `mcp.mode` | `single` | `single` or `multi` | | `mcp.transport` | `http` | streamable-http | | `mcp.port` | `8000` | matches probes + service | | `auth.allowAnonymous` | `false` | skip the bearer (testing only) | | `externalSecrets.enabled` | `true` | sync tokens from Vault | | `externalSecrets.dataFrom.secretKey` | `""` | **Vault path (set per cluster)** | | `externalSecrets.secretStoreRef.name` | `vault-backend` | ClusterSecretStore | | `externalSecrets.refreshInterval` | `150s` | | | `envFrom` | `[]` | extra envFrom sources (non-Vault testing) | | `federation.forwardTimeout` | `30` | seconds per forwarded call (multi) | | `federation.clusters` | `[{name: self, self: true}]` | registry (multi) | RBAC: a read-only ClusterRole (`get/list/watch` only, `secrets` excluded) is created for the pod ServiceAccount — the enforcement layer behind the server's own deny-by-default tool allowlist.