added repo

This commit is contained in:
Your Name
2026-08-26 03:39:42 +05:30
parent 45c25a95af
commit b8575bb8b9
6889 changed files with 1217125 additions and 0 deletions
@@ -0,0 +1,11 @@
apiVersion: v2
name: kubectl-mcp-server
description: >-
Read-only Kubernetes RCA MCP server (single-cluster + multi-cluster
federation). Env-driven; exposes only an allowlisted diagnostic toolset
over streamable-http for AI clients (Bifrost). Diagnosis only — never
mutates cluster state.
version: 2.1.0
appVersion: "0.1.0"
sources:
- https://github.com/Meesho/kubernetes-mcp
@@ -0,0 +1,178 @@
# 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 <token>`.
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 |
| `<TOKENENV>` 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/<cluster>/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: `<CLUSTER>_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=<the supply cluster's own MCP_AUTH_TOKEN> \
PAYMENTS_MCP_TOKEN=<the payments cluster's own MCP_AUTH_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_MCP_TOKEN>
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.
@@ -0,0 +1,19 @@
{{- define "kubectl-mcp-server.name" -}}
{{- .Values.fullnameOverride | default .Release.Name }}
{{- end }}
{{- define "kubectl-mcp-server.labels" -}}
app: {{ include "kubectl-mcp-server.name" . }}
{{- with .Values.labels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "kubectl-mcp-server.serviceAccountName" -}}
{{- if .Values.serviceAccount.name }}{{ .Values.serviceAccount.name }}{{ else }}{{ include "kubectl-mcp-server.name" . }}{{ end }}
{{- end }}
{{- /* Secret synced from Vault by the ExternalSecret; holds all bearer tokens. */ -}}
{{- define "kubectl-mcp-server.credsSecretName" -}}
{{- .Values.externalSecrets.secretName | default (printf "%s-creds" (include "kubectl-mcp-server.name" .)) }}
{{- end }}
@@ -0,0 +1,160 @@
{{- if .Values.rbac.create -}}
# Read-only ClusterRole for kubectl-mcp-server.
#
# * get/list/watch ONLY — no create/update/patch/delete anywhere. This is
# the enforcement layer behind the server's own deny-by-default allowlist
# (the server never registers any write/destructive tool).
# * `secrets` is EXCLUDED from core resources — the RCA bot has no business
# reading Secret payloads, and the server omits get_secrets entirely.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kubectl-mcp-server.name" . }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
rules:
# Core API resources - read access (secrets intentionally omitted)
- apiGroups: [""]
resources:
- pods
- pods/log
- pods/status
- services
- endpoints
- persistentvolumeclaims
- persistentvolumes
- configmaps
- serviceaccounts
- namespaces
- nodes
- nodes/status
- events
- resourcequotas
- limitranges
- replicationcontrollers
verbs: ["get", "list", "watch"]
# Apps API resources
- apiGroups: ["apps"]
resources:
- deployments
- daemonsets
- replicasets
- statefulsets
verbs: ["get", "list", "watch"]
# Batch API resources
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["get", "list", "watch"]
# Networking API resources
- apiGroups: ["networking.k8s.io"]
resources:
- ingresses
- networkpolicies
- ingressclasses
verbs: ["get", "list", "watch"]
# Storage API resources
- apiGroups: ["storage.k8s.io"]
resources:
- storageclasses
- volumeattachments
- csidrivers
- csinodes
verbs: ["get", "list", "watch"]
# RBAC API resources
- apiGroups: ["rbac.authorization.k8s.io"]
resources:
- roles
- rolebindings
- clusterroles
- clusterrolebindings
verbs: ["get", "list", "watch"]
# Autoscaling API resources
- apiGroups: ["autoscaling"]
resources:
- horizontalpodautoscalers
verbs: ["get", "list", "watch"]
# Policy API resources
- apiGroups: ["policy"]
resources:
- poddisruptionbudgets
verbs: ["get", "list", "watch"]
# API extensions
- apiGroups: ["apiextensions.k8s.io"]
resources:
- customresourcedefinitions
verbs: ["get", "list", "watch"]
# Admissionregistration API
- apiGroups: ["admissionregistration.k8s.io"]
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs: ["get", "list", "watch"]
# Certificates API
- apiGroups: ["certificates.k8s.io"]
resources:
- certificatesigningrequests
verbs: ["get", "list", "watch"]
# Coordination API
- apiGroups: ["coordination.k8s.io"]
resources:
- leases
verbs: ["get", "list", "watch"]
# Metrics API for resource usage (kubectl top)
- apiGroups: ["metrics.k8s.io"]
resources:
- nodes
- pods
verbs: ["get", "list"]
# Discovery API
- apiGroups: ["discovery.k8s.io"]
resources:
- endpointslices
verbs: ["get", "list", "watch"]
# KEDA + Argo Rollouts CRs (read-only) — common at Meesho, used by the
# server's keda_* / rollouts_* inspection tools
- apiGroups: ["keda.sh"]
resources:
- scaledobjects
- scaledjobs
- triggerauthentications
verbs: ["get", "list", "watch"]
- apiGroups: ["argoproj.io"]
resources:
- rollouts
- analysisruns
- analysistemplates
verbs: ["get", "list", "watch"]
# Contour CRs (read-only) — Meesho ingress is Contour HTTPProxy almost
# everywhere; needed to answer "what's the route/hostname for service X"
- apiGroups: ["projectcontour.io"]
resources:
- httpproxies
- tlscertificatedelegations
- extensionservices
verbs: ["get", "list", "watch"]
# Flagger canaries (read-only) — canary.enabled is standard in the 2.0.0
# chart; lets the server explain canary rollout state
- apiGroups: ["flagger.app"]
resources:
- canaries
- metrictemplates
verbs: ["get", "list", "watch"]
{{- end }}
@@ -0,0 +1,16 @@
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubectl-mcp-server.name" . }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubectl-mcp-server.name" . }}
subjects:
- kind: ServiceAccount
name: {{ include "kubectl-mcp-server.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
@@ -0,0 +1,18 @@
{{- /*
Federation registry (multi mode only). Mounted at /etc/k8s-rca-mcp and read
by the hub at startup. The hub stores no kubeconfigs — only endpoints and the
names of the env vars holding each backend's bearer token.
*/ -}}
{{- if eq .Values.mcp.mode "multi" -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "kubectl-mcp-server.name" . }}-registry
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
data:
clusters.yaml: |
clusters:
{{- toYaml .Values.federation.clusters | nindent 6 }}
{{- end }}
@@ -0,0 +1,112 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "kubectl-mcp-server.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ include "kubectl-mcp-server.name" . }}
template:
metadata:
labels:
app: {{ include "kubectl-mcp-server.name" . }}
{{- with .Values.labels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- if eq .Values.mcp.mode "multi" }}
checksum/registry: {{ toYaml .Values.federation.clusters | sha256sum }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if .Values.serviceAccount.create }}
serviceAccountName: {{ include "kubectl-mcp-server.serviceAccountName" . }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: kubectl-mcp-server
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
# Image ENTRYPOINT is `k8s-rca-mcp`, configured ENTIRELY via env vars
# (no CLI args). The server registers only an allowlisted read-only
# toolset — writes are blocked at the app layer and again by RBAC.
env:
- name: MCP_MODE
value: {{ .Values.mcp.mode | quote }}
- name: MCP_TRANSPORT
value: {{ .Values.mcp.transport | quote }}
- name: MCP_HOST
value: {{ .Values.mcp.host | quote }}
- name: MCP_PORT
value: {{ .Values.mcp.port | quote }}
{{- if .Values.auth.allowAnonymous }}
- name: MCP_ALLOW_ANONYMOUS
value: "true"
{{- end }}
{{- if eq .Values.mcp.mode "multi" }}
- name: MCP_CLUSTER_REGISTRY
value: /etc/k8s-rca-mcp/clusters.yaml
- name: MCP_FORWARD_TIMEOUT
value: {{ .Values.federation.forwardTimeout | quote }}
{{- end }}
# Bearer tokens come from Vault via the ExternalSecret-synced Secret:
# MCP_AUTH_TOKEN (inbound) + each backend's <TOKENENV> (multi mode).
{{- if or .Values.externalSecrets.enabled .Values.envFrom }}
envFrom:
{{- if .Values.externalSecrets.enabled }}
- secretRef:
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
{{- end }}
{{- with .Values.envFrom }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.mcp.port }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if eq .Values.mcp.mode "multi" }}
volumeMounts:
- name: registry
mountPath: /etc/k8s-rca-mcp
readOnly: true
{{- end }}
{{- if eq .Values.mcp.mode "multi" }}
volumes:
- name: registry
configMap:
name: {{ include "kubectl-mcp-server.name" . }}-registry
{{- end }}
@@ -0,0 +1,38 @@
{{- /*
ExternalSecret (external-secrets.io) — pulls every bearer token from one Vault
path and syncs them into the K8s Secret `<name>-creds`. The deployment injects
that Secret with `envFrom`, so each Vault key becomes an env var verbatim.
Store these keys at the Vault path (externalSecrets.dataFrom.secretKey):
* MCP_AUTH_TOKEN — inbound bearer the RCA agent must present
* <TOKENENV> per backend — e.g. SUPPLY_MCP_TOKEN, referenced by the
registry's `tokenEnv:` (multi mode only)
Keys MUST be valid env var names (they map 1:1 to container env).
*/ -}}
{{- if .Values.externalSecrets.enabled -}}
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
annotations:
argocd.argoproj.io/sync-wave: "-5"
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind }}
target:
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
creationPolicy: Owner
deletionPolicy: Retain
dataFrom:
- extract:
conversionStrategy: Default
key: {{ required "externalSecrets.dataFrom.secretKey (Vault path) is required when externalSecrets.enabled" .Values.externalSecrets.dataFrom.secretKey }}
{{- if .Values.externalSecrets.dataFrom.version }}
version: {{ .Values.externalSecrets.dataFrom.version | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,29 @@
{{- if .Values.ingress.enabled -}}
{{- if .Values.createContourGateway -}}
{{- if or ( eq "contour-internal" .Values.ingress.ingressClassName ) ( eq "contour-external" .Values.ingress.ingressClassName ) ( eq "contour-internal-0" .Values.ingress.ingressClassName ) ( eq "contour-internal-1" .Values.ingress.ingressClassName ) ( eq "contour-external-0" .Values.ingress.ingressClassName ) ( eq "contour-external-1" .Values.ingress.ingressClassName ) }}
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
namespace: {{ .Release.Namespace }}
name: {{ $serviceName }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
annotations:
projectcontour.io/ingress.class: {{ .Values.ingress.ingressClassName }}
spec:
ingressClassName: {{ .Values.ingress.ingressClassName }}
routes:
- services:
- name: {{ $serviceName }}
port: {{ $servicePortNumber }}
{{- if .Values.ingress.slowStart.enabled }}
slowStartPolicy:
window: {{ .Values.ingress.slowStart.window }}
aggression: {{ .Values.ingress.slowStart.aggression | float64 | squote }}
minWeightPercent: {{ .Values.ingress.slowStart.minPercent }}
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}
@@ -0,0 +1,39 @@
{{- if .Values.ingress.enabled -}}
{{- if .Values.createContourGateway -}}
{{- if or (eq "contour-internal" .Values.ingress.ingressClassName) (eq "contour-internal-0" .Values.ingress.ingressClassName) (eq "contour-internal-1" .Values.ingress.ingressClassName) }}
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
{{- $intraIngressClass := "" -}}
{{- if eq .Values.ingress.ingressClassName "contour-internal" -}}
{{- $intraIngressClass = "contour-internal-intra" -}}
{{- else if eq .Values.ingress.ingressClassName "contour-internal-0" -}}
{{- $intraIngressClass = "contour-internal-intra-0" -}}
{{- else if eq .Values.ingress.ingressClassName "contour-internal-1" -}}
{{- $intraIngressClass = "contour-internal-intra-1" -}}
{{- end -}}
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
namespace: {{ .Release.Namespace }}
name: {{ $serviceName }}-intra
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
annotations:
projectcontour.io/ingress.class: {{ $intraIngressClass }}
spec:
ingressClassName: {{ $intraIngressClass }}
routes:
- services:
- name: {{ $serviceName }}
port: {{ $servicePortNumber }}
{{- if .Values.ingress.slowStart.enabled }}
slowStartPolicy:
window: {{ .Values.ingress.slowStart.window }}
aggression: {{ .Values.ingress.slowStart.aggression | float64 | squote }}
minWeightPercent: {{ .Values.ingress.slowStart.minPercent }}
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}
@@ -0,0 +1,48 @@
{{- if .Values.ingress.enabled -}}
{{- if .Values.createContourGateway -}}
{{- if or (eq "contour-internal" .Values.ingress.ingressClassName) (eq "contour-internal-0" .Values.ingress.ingressClassName) (eq "contour-internal-1" .Values.ingress.ingressClassName) }}
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
{{- $intraIngressClass := "" -}}
{{- if eq .Values.ingress.ingressClassName "contour-internal" -}}
{{- $intraIngressClass = "contour-internal-intra" -}}
{{- else if eq .Values.ingress.ingressClassName "contour-internal-0" -}}
{{- $intraIngressClass = "contour-internal-intra-0" -}}
{{- else if eq .Values.ingress.ingressClassName "contour-internal-1" -}}
{{- $intraIngressClass = "contour-internal-intra-1" -}}
{{- end -}}
{{ $count := 0 | int }}
{{- range .Values.ingress.hosts }}
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
namespace: {{ $.Release.Namespace }}
name: {{ $serviceName }}-intra-{{ $count }}
labels:
{{- include "kubectl-mcp-server.labels" $ | nindent 4 }}
annotations:
projectcontour.io/ingress.class: {{ $intraIngressClass }}
spec:
ingressClassName: {{ $intraIngressClass }}
virtualhost:
fqdn: "{{ .host }}"
includes:
{{- range .paths }}
- conditions:
{{- if or (eq (lower .pathType) "prefix") (eq (lower .pathType) "implementationspecific") }}
- prefix: {{ .path }}
{{- end }}
{{- if eq (lower .pathType) "exact" }}
- exact: {{ .path }}
{{- end }}
name: {{ $serviceName }}-intra
namespace: {{ $.Release.Namespace }}
{{- end }}
---
{{ $count = add1 $count }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
@@ -0,0 +1,39 @@
{{- if .Values.ingress.enabled -}}
{{- if .Values.createContourGateway -}}
{{- if or ( eq "contour-internal" .Values.ingress.ingressClassName ) ( eq "contour-external" .Values.ingress.ingressClassName ) ( eq "contour-internal-0" .Values.ingress.ingressClassName ) ( eq "contour-internal-1" .Values.ingress.ingressClassName ) ( eq "contour-external-0" .Values.ingress.ingressClassName ) ( eq "contour-external-1" .Values.ingress.ingressClassName ) }}
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
{{- $ingressClassName := .Values.ingress.ingressClassName -}}
{{ $count := 0 | int }}
{{- range .Values.ingress.hosts }}
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
namespace: {{ $.Release.Namespace }}
name: {{ $serviceName }}-{{ $count }}
labels:
{{- include "kubectl-mcp-server.labels" $ | nindent 4 }}
annotations:
projectcontour.io/ingress.class: {{ $ingressClassName }}
spec:
ingressClassName: {{ $ingressClassName }}
virtualhost:
fqdn: "{{ .host }}"
includes:
{{- range .paths }}
- conditions:
{{- if or ( eq ( lower .pathType ) "prefix" ) ( eq ( lower .pathType ) "implementationspecific") }}
- prefix: {{ .path }}
{{- end }}
{{- if ( eq ( lower .pathType ) "exact" ) }}
- exact: {{ .path }}
{{- end }}
name: {{ $serviceName }}
namespace: {{ $.Release.Namespace }}
{{- end }}
---
{{ $count = add1 $count }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
@@ -0,0 +1,36 @@
{{- if .Values.ingress.enabled -}}
{{- if or ( eq "nginx-internal" .Values.ingress.ingressClassName ) ( eq "nginx-external" .Values.ingress.ingressClassName ) -}}
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $serviceName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
{{- if .Values.ingress.annotations }}
annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- pathType: {{ .pathType }}
path: {{ .path }}
backend:
service:
name: {{ $serviceName }}
port:
number: {{ $servicePortNumber }}
{{- end }}
{{- end }}
{{- end -}}
{{- end -}}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "kubectl-mcp-server.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
selector:
app: {{ include "kubectl-mcp-server.name" . }}
@@ -0,0 +1,17 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kubectl-mcp-server.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
# Unlike elasticsearch-mcp (which authenticates to ES via ExternalSecret
# creds), kubectl-mcp-server authenticates to the API server with this SA's
# token via in-cluster config — the token MUST be mounted.
automountServiceAccountToken: true
{{- end }}
@@ -0,0 +1,162 @@
fullnameOverride: "kubectl-mcp-server"
replicas: 1
image:
repository: asia-southeast1-docker.pkg.dev/meesho-devops-admin-0622/prd/devop/kubectl-mcp-server
tag: v2
pullPolicy: IfNotPresent
labels:
bu: infra
team: devops
serviceAccount:
create: true
annotations: {}
name: ""
# RBAC — ClusterRole + ClusterRoleBinding for the pod ServiceAccount.
# The role grants get/list/watch ONLY (no create/update/patch/delete) and
# deliberately excludes `secrets`. This is the enforcement layer behind the
# server's own deny-by-default allowlist (no write/secret tools registered).
rbac:
create: true
podAnnotations: {}
podSecurityContext: {}
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
priorityClassName: ""
nodeSelector: {}
tolerations: []
affinity: {}
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
# --- MCP server settings ---------------------------------------------------
# The image ENTRYPOINT is `k8s-rca-mcp`, configured ENTIRELY via env vars
# (there are NO CLI args). transport: http is streamable-http for Bifrost /
# HTTP MCP clients.
mcp:
mode: single # single | multi (federation hub)
transport: http
host: "0.0.0.0"
port: 8000
# --- inbound auth on the MCP endpoint --------------------------------------
# Bearer token. On http transport the server REFUSES TO START unless the
# MCP_AUTH_TOKEN env var is present OR allowAnonymous is true. The token is
# NOT stored in values/git — it is pulled from Vault (see externalSecrets).
# Clients send: Authorization: Bearer <token>
auth:
allowAnonymous: false
# --- secrets from Vault (external-secrets.io) ------------------------------
# One ExternalSecret extracts a Vault path into the Secret `<name>-creds`,
# which the Deployment injects with `envFrom`. Every key at the Vault path
# becomes a container env var verbatim, so store them as env var NAMES:
# MCP_AUTH_TOKEN — inbound bearer (always)
# <TOKENENV> per backend — e.g. SUPPLY_MCP_TOKEN (multi mode only),
# matching the registry's `tokenEnv:` below
externalSecrets:
enabled: true
refreshInterval: "150s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
dataFrom:
# Vault path holding the tokens — set per cluster in the override.
secretKey: ""
# Optional: pin a specific Vault secret version.
version: ""
# Override the synced Secret name (default: <name>-creds).
secretName: ""
# Extra envFrom sources (merged after the Vault creds secret), e.g. a
# manually-created Secret for local testing without Vault.
envFrom: []
# --- federation (only used when mcp.mode == multi) -------------------------
# The hub holds NO remote kubeconfigs. It forwards each tool call to the
# target cluster's own single-mode MCP using a per-backend bearer token.
# Those tokens live in Vault (keys named to match each `tokenEnv:` below).
federation:
forwardTimeout: 30
# Rendered into a ConfigMap mounted at /etc/k8s-rca-mcp/clusters.yaml.
# Exactly one entry must be `self: true`.
clusters:
- name: self
self: true
# - name: supply
# endpoint: https://kubectl-mcp-server.supply.stg.meesho.int/mcp
# tokenEnv: SUPPLY_MCP_TOKEN # <- must be a key at the Vault path
# NOTE: tcpSocket, not httpGet — under streamable-http transport FastMCP
# serves ONLY /mcp (GET /health 404s; upstream's chart gets this wrong).
# /mcp itself returns 406 to plain GETs, so TCP is the reliable signal.
livenessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
service:
port: 8000
type: ClusterIP
# Ingress — 2.0.0-chart style. nginx-* classes render a networking.k8s.io
# Ingress; contour-* classes (with createContourGateway: true) render the
# standard Meesho HTTPProxy parent/child pair plus the -intra variant.
createContourGateway: true
ingress:
enabled: true
ingressClassName: nginx-internal
servicePortNumber: 8000
hosts:
- host: ""
paths:
- path: /
pathType: Prefix
annotations:
kubernetes.io/ingress.class: nginx-internal
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-connections: "20"
slowStart:
enabled: false
window: "120s"
aggression: 1
minPercent: 10