Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion charts/kubernetes-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Kubernetes MCP Server Helm Chart

Kubernetes MCP Server exposes Kubernetes cluster inspection and automation through the Model Context Protocol.
This chart deploys the official `ghcr.io/containers/kubernetes-mcp-server:v0.0.62` image in HTTP mode with in-cluster authentication,
This chart deploys the official `ghcr.io/containers/kubernetes-mcp-server:v0.0.63` image in HTTP mode with in-cluster authentication,
read-only safety flags, and least-privilege RBAC by default.

## Install
Expand Down Expand Up @@ -35,6 +35,10 @@ rbac:
The chart blocks full write plus destructive mode unless `mcp.allowUnsafeWriteAccess=true` is set.
The chart is stateless by default.
If persistence is enabled and the Deployment is scaled above one replica, use `ReadWriteMany` storage or disable persistence to avoid sharing one `ReadWriteOnce` PVC across pods.
Ingress class rendering is optional. Set `ingress.ingressClassName: ""` to omit `spec.ingressClassName`.
When `networkPolicy.enabled=true`, ingress is restricted to the configured peers.
Setting `networkPolicy.enabled=true` enables egress isolation with built-in DNS and HTTPS allowances; `networkPolicy.extraEgress` appends API server or proxy rules.
Set `networkPolicy.dnsEgressPeers` when your cluster DNS pods do not use the default kube-system/kube-dns labels.

## Security Scan: `kubernetes-mcp-server`

Expand Down
82 changes: 69 additions & 13 deletions charts/kubernetes-mcp-server/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,39 +1,95 @@
{{/* SPDX-License-Identifier: Apache-2.0 */}}
Kubernetes MCP Server has been deployed.

Access:
1. Access

- Service: http://{{ include "kubernetes-mcp-server.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }}
- Port-forward: kubectl port-forward svc/{{ include "kubernetes-mcp-server.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }}
- Local MCP URL: http://127.0.0.1:8080
- Service DNS: http://{{ include "kubernetes-mcp-server.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }}
- Port-forward:
kubectl port-forward svc/{{ include "kubernetes-mcp-server.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }}
- Local MCP URL:
http://127.0.0.1:8080

Safety mode:
2. Runtime Mode

- readOnly: {{ .Values.mcp.readOnly }}
- disableDestructive: {{ .Values.mcp.disableDestructive }}
- allowUnsafeWriteAccess: {{ .Values.mcp.allowUnsafeWriteAccess }}
- stateless: {{ .Values.mcp.stateless }}
- clusterProvider: {{ .Values.mcp.clusterProvider }}
- disableMultiCluster: {{ .Values.mcp.disableMultiCluster }}
- listOutput: {{ .Values.mcp.listOutput }}
- logLevel: {{ .Values.mcp.logLevel }}
- toolsets: {{ join "," .Values.mcp.toolsets }}

RBAC:
3. RBAC

{{- if and .Values.serviceAccount.create .Values.rbac.create }}
- ServiceAccount: {{ include "kubernetes-mcp-server.serviceAccountName" . }}
- ClusterRoleBinding target: {{ .Values.rbac.clusterRoleName }}
- Verify read access:
kubectl auth can-i list pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }}
- Verify destructive access remains denied with default values:
kubectl auth can-i delete pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }}
{{- else }}
- Chart-managed RBAC is disabled. Confirm the ServiceAccount can access the intended resources.
- Chart-managed RBAC is disabled.
- Confirm the selected ServiceAccount can access only the intended resources.
{{- end }}

Troubleshooting:
4. Configuration File

- kubectl get pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }}
- kubectl logs deploy/{{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }}
- kubectl auth can-i list pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }}
{{- if .Values.mcp.configToml }}
- TOML config is mounted from ConfigMap {{ include "kubernetes-mcp-server.fullname" . }}.
- Inspect it with:
kubectl get configmap {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} -o yaml
{{- else }}
- No TOML config is mounted; runtime options are generated from mcp.* values.
{{- end }}

5. Exposure

{{- if .Values.ingress.enabled }}
- Ingress is enabled.
- Keep this endpoint behind trusted authentication and network controls.
{{- else }}
- Ingress is disabled.
{{- end }}
{{- if .Values.gateway.enabled }}
- Gateway API HTTPRoute is enabled.
{{- else }}
- Gateway API HTTPRoute is disabled.
{{- end }}
{{- if .Values.networkPolicy.enabled }}
- NetworkPolicy is enabled for inbound MCP traffic.
{{- else }}
- NetworkPolicy is disabled.
{{- end }}

6. Persistence And Scaling

{{- if .Values.persistence.enabled }}
- Persistence is enabled at {{ .Values.persistence.mountPath }}.
- PVC source: {{ default (include "kubernetes-mcp-server.fullname" .) .Values.persistence.existingClaim }}
- For replicaCount > 1, use ReadWriteMany storage or an externally managed RWX claim.
{{- else }}
- Persistence is disabled; the default deployment is stateless.
{{- end }}
- replicaCount: {{ .Values.replicaCount }}

7. Troubleshooting

- Pods:
kubectl get pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }}
- Logs:
kubectl logs deploy/{{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }}
- Service endpoints:
kubectl get endpoints {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }}
- Rendered arguments:
kubectl get deploy {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.template.spec.containers[0].args}'

Resources:
8. Resources

- Chart documentation: https://helmforge.dev/docs/charts/kubernetes-mcp-server
- Upstream: https://github.com/containers/kubernetes-mcp-server
- Upstream project: https://github.com/containers/kubernetes-mcp-server
- Security guidance: keep write-enabled releases isolated to trusted namespaces and clients.

Happy Helmforging :)
16 changes: 16 additions & 0 deletions charts/kubernetes-mcp-server/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,19 @@ app.kubernetes.io/part-of: helmforge
{{- end -}}
{{- define "kubernetes-mcp-server.serviceAccountName" -}}{{- if .Values.serviceAccount.create -}}{{- default (include "kubernetes-mcp-server.fullname" .) .Values.serviceAccount.name -}}{{- else -}}{{- default "default" .Values.serviceAccount.name -}}{{- end -}}{{- end -}}
{{- define "kubernetes-mcp-server.image" -}}{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}{{- end -}}

{{- define "kubernetes-mcp-server.validate" -}}
{{- if and (not .Values.mcp.readOnly) (not .Values.mcp.disableDestructive) (not .Values.mcp.allowUnsafeWriteAccess) -}}
{{- fail "write access with destructive tools requires mcp.allowUnsafeWriteAccess=true" -}}
{{- end -}}
{{- if and (gt (int .Values.replicaCount) 1) .Values.persistence.enabled (not .Values.persistence.existingClaim) (not (has "ReadWriteMany" .Values.persistence.accessModes)) -}}
{{- fail "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false" -}}
{{- end -}}
{{- $podLabels := .Values.podLabels | default dict -}}
{{- if hasKey $podLabels "app.kubernetes.io/name" -}}
{{- fail "podLabels must not override the selector label app.kubernetes.io/name" -}}
{{- end -}}
{{- if hasKey $podLabels "app.kubernetes.io/instance" -}}
{{- fail "podLabels must not override the selector label app.kubernetes.io/instance" -}}
{{- end -}}
{{- end -}}
2 changes: 1 addition & 1 deletion charts/kubernetes-mcp-server/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ spec:
template:
metadata:
labels:
{{- include "kubernetes-mcp-server.labels" . | nindent 8 }}
{{- include "kubernetes-mcp-server.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
Expand Down
4 changes: 3 additions & 1 deletion charts/kubernetes-mcp-server/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.ingressClassName | quote }}
{{- with .Values.ingress.ingressClassName }}
ingressClassName: {{ . | quote }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
Expand Down
22 changes: 22 additions & 0 deletions charts/kubernetes-mcp-server/templates/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{{/* SPDX-License-Identifier: Apache-2.0 */}}
{{- if .Values.networkPolicy.enabled }}
{{- $extraEgress := .Values.networkPolicy.extraEgress | default list }}
{{- $dnsEgressPeers := .Values.networkPolicy.dnsEgressPeers | default list }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
Expand All @@ -12,6 +14,7 @@ spec:
{{- include "kubernetes-mcp-server.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
- from:
{{- if .Values.networkPolicy.ingressFrom }}
Expand All @@ -22,4 +25,23 @@ spec:
ports:
- protocol: TCP
port: http
egress:
- to:
{{- toYaml $dnsEgressPeers | nindent 8 }}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
- ipBlock:
cidr: ::/0
ports:
- protocol: TCP
port: 443
{{- with $extraEgress }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
7 changes: 1 addition & 6 deletions charts/kubernetes-mcp-server/templates/validate.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
{{/* SPDX-License-Identifier: Apache-2.0 */}}
{{- if and (not .Values.mcp.readOnly) (not .Values.mcp.disableDestructive) (not .Values.mcp.allowUnsafeWriteAccess) -}}
{{- fail "write access with destructive tools requires mcp.allowUnsafeWriteAccess=true" -}}
{{- end -}}
{{- if and (gt (int .Values.replicaCount) 1) .Values.persistence.enabled (not (has "ReadWriteMany" .Values.persistence.accessModes)) -}}
{{- fail "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false" -}}
{{- end -}}
{{- include "kubernetes-mcp-server.validate" . -}}
40 changes: 40 additions & 0 deletions charts/kubernetes-mcp-server/tests/networkpolicy_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,43 @@ tests:
content:
protocol: TCP
port: http
- contains:
path: spec.policyTypes
content: Egress
- equal:
path: spec.egress[0].ports[0].port
value: 53
- equal:
path: spec.egress[1].ports[0].port
value: 443
- it: appends extra egress rules after default DNS and HTTPS
set:
networkPolicy:
enabled: true
extraEgress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: TCP
port: 6443
asserts:
- contains:
path: spec.policyTypes
content: Egress
- equal:
path: spec.egress[0].ports[0].port
value: 53
- equal:
path: spec.egress[0].to[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"]
value: kube-system
- equal:
path: spec.egress[0].to[0].podSelector.matchLabels["k8s-app"]
value: kube-dns
- equal:
path: spec.egress[1].ports[0].port
value: 443
- equal:
path: spec.egress[2].ports[0].port
value: 6443
16 changes: 16 additions & 0 deletions charts/kubernetes-mcp-server/tests/templates_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ tests:
- equal:
path: spec.template.spec.containers[0].image
value: ghcr.io/containers/kubernetes-mcp-server:v0.0.63
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/name"]
value: kubernetes-mcp-server
- notExists:
path: spec.template.metadata.labels["helm.sh/chart"]
- it: renders service
template: templates/service.yaml
asserts:
Expand Down Expand Up @@ -41,6 +46,17 @@ tests:
- equal:
path: metadata.annotations["cert-manager.io/cluster-issuer"]
value: letsencrypt
- it: omits empty ingress class name
template: templates/ingress.yaml
set:
ingress.enabled: true
ingress.ingressClassName: ""
ingress.hosts[0].host: kubernetes-mcp-server.example.com
ingress.hosts[0].paths[0].path: /
ingress.hosts[0].paths[0].pathType: Prefix
asserts:
- notExists:
path: spec.ingressClassName
- it: renders HTTPRoute hostnames
template: templates/httproute.yaml
set:
Expand Down
15 changes: 15 additions & 0 deletions charts/kubernetes-mcp-server/tests/validation_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ tests:
asserts:
- failedTemplate:
errorMessage: "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false"
- it: allows scaling with an existing persistence claim
set:
replicaCount: 2
persistence.enabled: true
persistence.existingClaim: kubernetes-mcp-data-rwx
persistence.accessModes[0]: ReadWriteOnce
asserts:
- notFailedTemplate: {}
- it: fails when podLabels override selector labels
set:
podLabels:
app.kubernetes.io/instance: custom
asserts:
- failedTemplate:
errorMessage: "podLabels must not override the selector label app.kubernetes.io/instance"
14 changes: 12 additions & 2 deletions charts/kubernetes-mcp-server/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"type": "object",
"properties": {
"repository": { "type": "string", "default": "ghcr.io/containers/kubernetes-mcp-server" },
"tag": { "type": "string", "default": "v0.0.62" },
"tag": { "type": "string", "default": "v0.0.63" },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"], "default": "IfNotPresent" }
}
},
Expand Down Expand Up @@ -68,7 +68,17 @@
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": false },
"ingressFrom": { "type": "array", "items": { "type": "object" } }
"ingressFrom": { "type": "array", "items": { "type": "object" } },
"dnsEgressPeers": {
"type": "array",
"description": "DNS peers allowed by the built-in egress allowance",
"items": { "type": "object" }
},
"extraEgress": {
"type": "array",
"description": "Additional egress rules appended after the built-in DNS and HTTPS allowances",
"items": { "type": "object" }
}
}
},
"probes": { "$ref": "#/definitions/probes" },
Expand Down
10 changes: 10 additions & 0 deletions charts/kubernetes-mcp-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ pdb:
networkPolicy:
enabled: false
ingressFrom: []
# -- DNS peers allowed when egress isolation is enabled.
dnsEgressPeers:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
# -- Additional egress rules appended after the built-in DNS and HTTPS allowances.
extraEgress: []
Comment thread
coderabbitai[bot] marked this conversation as resolved.
probes:
startup: { enabled: true, initialDelaySeconds: 5, periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 60 }
liveness: { enabled: true, initialDelaySeconds: 0, periodSeconds: 20, timeoutSeconds: 5, failureThreshold: 3 }
Expand Down