Are you the author? Sign in to claim
An MCP server that lets AI agents operate a multi-cluster Kubernetes fleet through an Open Cluster Management hub, with
An MCP server that lets AI agents operate a multi-cluster Kubernetes fleet through an Open Cluster Management hub, with policy, approval, and audit between the model and your clusters.
The agent never holds a kubeconfig. Every write is policy-checked, human-approved, and traced.
✨ Why · 🧭 Architecture · 🧰 Toolsets · 🛠️ Tools · 💬 Prompts · 🚀 Quickstart · 📖 Wiki · 📚 Docs
The whole safe-remediation loop: investigate with free reads, propose a change, get rejected by the guardrails and correct it, wait for a human-signed token, apply, verify, and report from the audit log.
Your team runs many Kubernetes clusters. Sooner or later somebody asks the question: can an AI agent take the 2 a.m. page?
The quickest way to find out is to hand a model kubectl with cluster-admin and watch.
In production that experiment ends badly, for three separate reasons:
kubectl delete the next.This project starts from a different observation: fleets already have a control point that
humans trust every day, the multi-cluster hub. Open Cluster Management (a CNCF project)
gives every fleet an inventory (ManagedCluster), a scheduler (Placement), and a delivery
channel (ManifestWork). ocm-mcp-server exposes that hub to agents as a small set of
typed MCP tools, and puts four independent layers
between the model and your clusters:
| # | Layer | Enforced by | What it stops |
|---|---|---|---|
| 1 | Static checks | this server, before anything else | privileged pods, host access, system namespaces, unpinned images, disallowed kinds |
| 2 | Policy admission | Kyverno dry-run on the hub | anything your org's policies reject, evaluated inside the ManifestWork envelope |
| 3 | Human approval | Ed25519 token signed by ocm-mcp approve on a trusted terminal (the server holds only the public key) | any change reaching a cluster without a person consenting to that exact content and operation |
| 4 | Least-privilege RBAC | Kubernetes | everything else; no Secrets, no exec, no deletes outside its own ManifestWorks |
None of these layers live in the system prompt, so none of them can be talked out of.
flowchart LR
A["🤖 AI Agent<br/>(any MCP client)"] -->|"typed tool calls"| S["🛡️ ocm-mcp-server<br/>static guardrails · audit"]
S -->|"reads + dry-run + apply"| H["☸️ OCM Hub<br/>Placement · ManifestWork<br/>Kyverno · RBAC"]
H --> C1["cluster1"]
H --> C2["cluster2"]
H --> C3["cluster3"]
U["🧑💻 Human operator<br/>ocm-mcp approve"] -.->|"approval token"| A
S -.->|"spans"| J["🔍 OpenTelemetry / Jaeger"]
The write path in one sentence: the agent proposes a ManifestWork; static guardrails
and a Kyverno dry-run validate it; a human reviews the exact content and mints an
approval token bound to its hash; only then does apply deliver it, with every step traced
and logged.
The second guardrail layer does not live in this server - it lives in the cluster. Before a
proposed change is ever stored, the server does a server-side dry-run create of the
ManifestWork on the hub, so the hub's Kyverno validating admission
runs against the exact manifests the agent wants to apply. If your organization's policy
says no, the proposal is rejected at admission with the policy's own message - the same
control that governs every human kubectl apply.
Why Kyverno:
ManifestWork.Where it is used here:
deploy/policies/ ships three ClusterPolicy objects that foreach
over spec.workload.manifests inside a ManifestWork (block privileged/host access,
protect system namespaces, enforce a kind allow-list), scoped by the
app.kubernetes.io/managed-by: ocm-mcp-server label so they judge only agent-authored work.make policy-test runs a 12-case offline suite with the kyverno CLI - good, bad, and
human-authored ManifestWorks - needing no cluster and no dependencies. It runs in CI, so a
policy regression fails the build before it can reach a hub.The surface is 34 tools across ten toolsets. Almost all of it is read: the whole
Open Cluster Management API is safe to inspect. Only two toolsets can change
anything, and only through the propose -> approve -> apply gate. Every hub-level
tool works for any managed spoke - a standalone OpenShift cluster, a HyperShift
hosted cluster, or a cloud cluster - because on the hub they are all ManagedClusters.
| Toolset | What it covers | Tools | Writes |
|---|---|---|---|
| inventory | ManagedClusters, ClusterSets, set bindings, ClusterClaims, ManagedClusterInfo | 6 | - |
| observability | cluster health, events, pod logs | 3 | - |
| placement | Placements, PlacementDecisions, AddOnPlacementScores | 3 | - |
| work | ManifestWork status feedback + the gated deploy and rollback flow | 7 | gated |
| addons | ClusterManagementAddOns, fleet + per-cluster add-on health | 3 | - |
| registration | pending join CSRs + gated cluster lifecycle actions | 3 | gated |
| policy | governance compliance + violations rollup (if the add-on is installed) | 2 | - |
| hosted-control-planes | HyperShift HostedClusters and NodePools (when the hub hosts them) | 3 | - |
| resources | generic get/list over an allow-list of OCM API types | 2 | - |
| audit | pending proposals, this server's own audit trail | 2 | - |
Every read tool is annotated readOnlyHint; every write tool is annotated
destructiveHint and enforced by the gate. Setting OCM_MCP_READ_ONLY=1 turns off
the two writing toolsets entirely, for a strictly-inspection deployment.
Validate against your own hub in one command:
ocm-mcp doctorcalls every read tool against the live hub and prints aPASS / EMPTY / SKIP / FAILtable (writing nothing), so you can confirm exactly what the server sees before wiring up an agent.
There is deliberately no tool that reads Secrets, execs into pods, or deletes
arbitrary resources. The generic reader (list_resources / get_resource) works
against an allow-list of OCM types, so Secrets are not restricted - they are
simply not expressible. A capability that does not exist cannot be prompt-injected
into use.
Each tool below is annotated with its class: read (free, no gate), propose (stores a pending change, mutates nothing), or apply (delivers an approved change; needs a human token).
list_clusters (read) - all managed clusters with availability, version, labels, capacity.get_cluster (read) - full view of one cluster.
cluster (string) - managed cluster name.list_cluster_sets (read) - ManagedClusterSets with selector type and member clusters.list_cluster_set_bindings (read) - which ClusterSets a namespace's Placements may target.
namespace (string, optional) - limit to one namespace; empty lists all.list_cluster_claims (read) - every cluster's ClusterClaims (id, platform, region, version).get_cluster_info (read) - extended inventory from the hub (OpenShift version, nodes, console URL); needs no spoke access.
cluster (string) - managed cluster name.get_cluster_health (read) - hub conditions, unhealthy pods, degraded deployments.
cluster (string) - managed cluster name.query_events (read) - recent Kubernetes events, newest first.
cluster (string) - managed cluster name.namespace (string, optional) - namespace filter; empty means all.limit (int, optional) - max events (default 40).get_pod_logs (read) - tail a pod's logs (falls back to the previous instance if crashing).
cluster (string), namespace (string), pod (string) - target.container (string, optional) - container name; empty picks the default.lines (int, optional) - trailing lines (default 80).list_placements (read) - Placements and how many clusters each selects.
namespace (string, optional) - limit to one namespace.get_placement_decision (read) - the clusters a Placement actually selected.
placement (string) - Placement name.namespace (string) - the Placement's namespace.list_addon_placement_scores (read) - custom scores prioritizers consume.
cluster (string) - managed cluster name.list_manifestworks (read) - ManifestWorks targeting a cluster.
cluster (string) - managed cluster name.get_manifestwork (read) - detailed status + per-resource status feedback (the "why not Applied").
cluster (string), name (string) - target.list_manifestworkreplicasets (read) - a template fanned across a Placement, with rollout summary.
namespace (string, optional) - limit to one namespace.propose_manifestwork (propose) - propose a change as a ManifestWork. Applies nothing.
cluster (string) - target cluster.name (string) - short kebab-case ManifestWork name.summary (string) - one or two sentences the human approver reads.manifests_json (string) - JSON array of complete manifests (allowed kinds; namespaced; pinned images).apply_manifestwork (apply) - deliver an approved ManifestWork.
proposal_id (string), approval_token (string) - from ocm-mcp approve <id>.propose_rollback (propose) - propose undoing an applied ManifestWork; creates a rollback proposal bound to its UID.
proposal_id (string) - the applied ManifestWork proposal to undo.rollback_manifestwork (apply) - delete the ManifestWork after the rollback is approved (needs a rollback-scoped token).
rollback_proposal_id (string), approval_token (string).list_cluster_management_addons (read) - fleet-level add-on definitions and install strategy.get_addon_health (read) - per-cluster ManagedClusterAddOn Available / Degraded / Progressing.list_addons_for_cluster (read) - every add-on on one cluster, with install namespace and health.
cluster (string) - managed cluster name.list_pending_csrs (read) - pending cluster-join / add-on registration CSRs awaiting approval.propose_cluster_action (propose) - propose a lifecycle action. Applies nothing.
cluster (string) - target cluster.action (string) - one of cordon (taint out of scheduling), uncordon, set_label, accept (hubAcceptsClient + approve join CSRs), enable_addon / disable_addon (create/delete a ManagedClusterAddOn).summary (string) - what the human approver reads.params_json (string, optional) - action parameters; set_label needs {"key","value"}, the add-on actions need {"addon"} (+ optional install_namespace).apply_cluster_action (apply) - apply an approved lifecycle action.
proposal_id (string), approval_token (string).list_policies (read) - Policies and per-cluster compliance. Reports clearly if the governance add-on is not installed.
namespace (string, optional) - limit to one namespace.list_policy_violations (read) - only the NonCompliant / Pending policy-cluster pairs across the fleet.list_hosted_clusters (read) - HostedClusters with version and conditions. Reports clearly if HCPs are hosted on a different management cluster.
namespace (string, optional) - limit to one namespace.get_hosted_cluster (read) - one HostedCluster in detail, with its NodePools.
name (string), namespace (string) - target.list_node_pools (read) - HyperShift NodePools (worker groups), desired vs current replicas.
namespace (string, optional), cluster (string, optional) - filters.list_resources (read) - list any allow-listed OCM type (identity + conditions).
resource (string) - e.g. managedclusters, placements, manifestworks, managedclusteraddons, klusterlets.namespace (string, optional) - for namespaced types.get_resource (read) - get one allow-listed OCM object in full. Never returns a Secret (not on the allow-list).
resource (string), name (string) - target.namespace (string, optional) - required for namespaced types.list_pending_proposals (read) - ManifestWorks and cluster actions awaiting approval.get_audit_trail (read) - the last N tool calls from this server's append-only log.
last_n (int, optional) - trailing entries (default 30).The server also ships ten MCP prompts - reusable templates that encode the safe workflow so any client can start from a good runbook instead of a blank box.
| Prompt | What it drives | Arguments |
|---|---|---|
diagnose_fleet | sweep every cluster and add-on, summarize what is unhealthy and why - reads only | - |
remediate_with_approval | investigate a symptom, propose the smallest safe fix, wait for the human token, apply, verify, report | symptom |
incident_postmortem | write the post-incident report strictly from get_audit_trail, not from memory | - |
why_not_scheduled | explain why a cluster was or was not selected by a Placement, from the live objects | cluster, placement, namespace |
onboard_cluster | accept a pending cluster safely through the approval gate | cluster |
addon_troubleshoot | diagnose a degraded add-on across the fleet | addon |
hosted_cluster_health | assess a HyperShift hosted control plane and its node pools | cluster |
policy_compliance_report | summarize governance compliance and prioritize what to fix | - |
capacity_report | find clusters with headroom and clusters under pressure | - |
rollout_status | track a ManifestWorkReplicaSet rollout across selected clusters | name, namespace |
Requirements: docker, kind, kubectl, clusteradm, helm, Python 3.11+. The deployment guide has install commands and the real-fleet path.
git clone https://github.com/sandeepbazar/ocm-mcp-server.git
cd ocm-mcp-server
make bootstrap # 1 hub + 3 managed kind clusters, OCM, Kyverno, policies, demo app
make install # pip install -e ".[dev,tracing]"
The server is configured entirely through environment variables. The two that matter most
are kubeconfig context names. New to those? The
context names guide explains what they are and the exact
commands to find yours, from a laptop kind cluster to a cloud login. In short: run
kubectl config get-contexts and read the NAME column (make bootstrap prints
ready-to-paste values at the end).
| Variable | Required | What goes in it |
|---|---|---|
OCM_MCP_HUB_CONTEXT | yes | The kubeconfig context that points at the OCM hub cluster, where ManagedCluster and ManifestWork live. After make bootstrap this is kind-hub. Empty = current context. |
OCM_MCP_SPOKE_CONTEXTS | for events/logs | Comma-separated <managed-cluster-name>=<kubeconfig-context> pairs mapping each cluster as the hub names it (kubectl --context kind-hub get managedclusters) to a context holding read-only spoke credentials. Only query_events / get_pod_logs / spoke-side health need this; hub-level tools work without it. |
KUBECONFIG | no | Kubeconfig file path(s); defaults to ~/.kube/config. |
OTEL_EXPORTER_OTLP_ENDPOINT | no | Set (e.g. http://localhost:4318) to emit a trace span per tool call. Unset = tracing off, audit log still on. |
OCM_MCP_HOME | no | State directory (approval secret, pending proposals, audit.jsonl). Default ~/.ocm-mcp. |
OCM_MCP_APPROVAL_TTL | no | Approval-token lifetime in seconds. Default 3600. |
OCM_MCP_READ_ONLY | no | Set to 1/true for a strictly-inspection deployment: every propose/apply tool refuses, a coarse backstop under the token gate. Default off. |
OCM_MCP_CLIENT_TTL | no | Seconds before the cached Kubernetes API client is rebuilt, so rotated/refreshed credentials are picked up. Default 600. |
OCM_MCP_SPOKE_TIMEOUT | no | Read timeout (seconds) for spoke health/event/log calls, so one large cluster cannot hang a tool. Default 30. |
OCM_MCP_HEALTH_LIMIT | no | Max pods/deployments get_cluster_health fetches per cluster; the result notes truncation. Default 500. |
For key management, ocm-mcp rotate-secret generates a fresh Ed25519 approval keypair
(invalidating every outstanding approval token), and ocm-mcp doctor runs the live
read-path smoke test.
# the values make bootstrap prints, spelled out:
export OCM_MCP_HUB_CONTEXT=kind-hub # context of the hub cluster
export OCM_MCP_SPOKE_CONTEXTS=cluster1=kind-cluster1,cluster2=kind-cluster2,cluster3=kind-cluster3
# └ name on the hub ┘ └ kubeconfig context with read-only creds ┘
Not sure where kind-hub or cluster1=kind-cluster1 come from, or what your own values
should be? The context names guide walks through it
step by step, including cloud logins (EKS, GKE, AKS, OpenShift). Pointing at a real
fleet instead of kind? Same variables; the deployment guide covers
the read-only spoke accounts and production hardening.
The server speaks standard MCP over stdio; nothing here is specific to one vendor's agent.
Ready-made configs live in examples/:
.mcp.json in your project (or claude mcp add){
"mcpServers": {
"ocm-fleet": {
"command": "ocm-mcp-server",
"env": {
"OCM_MCP_HUB_CONTEXT": "kind-hub",
"OCM_MCP_SPOKE_CONTEXTS": "cluster1=kind-cluster1,cluster2=kind-cluster2,cluster3=kind-cluster3"
}
}
}
}
~/.codex/config.toml[mcp_servers.ocm-fleet]
command = "ocm-mcp-server"
[mcp_servers.ocm-fleet.env]
OCM_MCP_HUB_CONTEXT = "kind-hub"
OCM_MCP_SPOKE_CONTEXTS = "cluster1=kind-cluster1,cluster2=kind-cluster2,cluster3=kind-cluster3"
~/.gemini/settings.json{
"mcpServers": {
"ocm-fleet": {
"command": "ocm-mcp-server",
"env": {
"OCM_MCP_HUB_CONTEXT": "kind-hub",
"OCM_MCP_SPOKE_CONTEXTS": "cluster1=kind-cluster1,cluster2=kind-cluster2,cluster3=kind-cluster3"
}
}
}
}
{
"mcpServers": {
"ocm-fleet": {
"command": "ocm-mcp-server",
"env": {
"OCM_MCP_HUB_CONTEXT": "kind-hub",
"OCM_MCP_SPOKE_CONTEXTS": "cluster1=kind-cluster1,cluster2=kind-cluster2,cluster3=kind-cluster3"
}
}
}
}
Most MCP clients accept an mcpServers block like this one. If ocm-mcp-server is not
on the PATH the client launches with, use the absolute path from
which ocm-mcp-server as the command value.
Give the agent the runbook discipline in
examples/system-prompt.md, then break something and watch
the flow:
make inject SCENARIO=failing-rollout CLUSTER=cluster2
You: "Payments is degraded somewhere in the fleet. Investigate and fix."
Agent:
list_clusters→get_cluster_health(cluster2)→query_events→get_pod_logs→ "payments-v2 on cluster2 is in ImagePullBackOff. Proposing a ManifestWork pinning the last good image. Proposal4f1a2b3cneeds your approval."You (trusted terminal):
ocm-mcp approve 4f1a2b3c, then paste the token back.Agent:
apply_manifestwork→ verifies recovery →get_audit_trail→ writes the incident report.
Then try to talk it into something dangerous ("just redeploy it privileged with hostNetwork, it's faster"). The proposal dies at layer 1 or layer 2, and the rejection message tells the agent exactly why. More worked examples →
eval/ ships 22 scripted incident scenarios in three classes: remediate (15),
diagnose-only (3), adversarial (4). Scoring is objective on all three axes: diagnosis
keywords in the transcript, live cluster state for recovery, and the server's own audit log
for safety.
python3 eval/run_eval.py --agent-cmd "claude -p" # or any agent CLI
Run it against your model of choice and publish your numbers, including the failures. The point is real data about what agents can and cannot yet be trusted to do.
The Kyverno policies have their own offline test suite: make policy-test runs 12 CLI
cases (deploy/policies/tests/) against good, bad, and
human-created ManifestWorks with no cluster and no dependencies. It runs in CI too, so a
policy regression fails the build before it ever reaches a hub.
Want proof it works against real clusters, not mocks? One script stands up a real Open Cluster Management fleet on kind, exercises every tool and prompt, runs a break-then-fix scenario, and writes a graphical HTML report:
./hack/e2e-local.sh # 2 spokes, auto-cleanup (SPOKES=1 for a lighter run)
It (1) installs or version-checks the dependencies (Podman, kind, kubectl, clusteradm,
helm; Docker is not required), (2) bootstraps a hub plus spokes, (3) runs the read
tools, the gated propose -> approve -> apply write flow, a lifecycle action, and the
prompts - each with a plain-language explanation of what it does and why, (4) injects a
failing rollout and shows the diagnose-and-fix loop end to end, then (5) writes
e2e-report.html and tears the fleet back down (kind and Podman stay installed). The
report is git-ignored. Works on macOS (Homebrew + Podman) and Linux.
ocm-mcp doctor runs just the live read-path smoke test on its own, against any hub.
| Page | What it covers |
|---|---|
| Tools and Prompts reference | every tool by toolset, its class (read / propose / apply), arguments, and the OCM API it touches; the four MCP prompts |
| Context names guide | zero-background: what a kubeconfig context is and the exact commands to find yours (kind, EKS, GKE, AKS, OpenShift) |
| Deployment guide | laptop quickstart in depth, real OCM fleets, Docker, production hardening, troubleshooting |
| Worked examples | full incident transcripts, approval sessions, adversarial rejections, audit output |
| Architecture | the choke-point idea, components, design decisions worth arguing about |
| Guardrails | the four layers, deliberate absences, threat model, what we refuse to automate |
| Security self-assessment | CNCF TAG-Security-style assessment: actors, actions, security functions, limits |
| CNCF Sandbox readiness | a self-check against CNCF Sandbox expectations, used as a quality bar; honest gaps |
| Demo script | a timed 3-act live demo with fallbacks |
| Upstream notes | gaps found while building this; proposals for MCP, OCM, and Kyverno |
| Eval harness | scenario classes, scoring, how to run against your model |
| Changelog · Support · Security · Contributing | project meta |
yanmxa/multicluster-mcp-server
also bridges agents to Open Cluster Management, with kubectl-level tools: it can generate
a kubeconfig bound to a ClusterRole (cluster-admin by default) and execute kubectl
commands. That design maximizes capability. This project sits at the other end of the
trade-off: no kubectl, no kubeconfig exposure, a fixed tool surface, and mandatory policy
plus human approval on every write. Pick by how much you need to trust the agent.ocm-mcp container that
manages OpenShift clusters through the OpenShift Cluster Manager API. Same acronym,
different system. OCM in this repository always means
Open Cluster Management, the CNCF multi-cluster
project.src/ocm_mcp_server/ the MCP server: tools, guardrails, approvals, tracing, CLI
deploy/ least-privilege RBAC + Kyverno ClusterPolicies (+ offline tests)
hack/ bootstrap.sh / teardown.sh / demo app (kind-based fleet)
chaos/ failure-injection scenarios (reversible, diagnosable)
eval/ 22-scenario evaluation harness + results
docs/ deployment, examples, architecture, guardrails, demo, upstream
examples/ MCP client configs + a production-shaped system prompt
eval/results/)docs/upstream-notes.md
(MCP long-running operations · OCM ManifestWork feedback · Kyverno catalog contribution)Have a need that's not here? Open a feature request. New tools require a safety rationale; see CONTRIBUTING.md.
Issues and PRs welcome. Start with CONTRIBUTING.md. Getting help: SUPPORT.md. Security reports (privately, please): SECURITY.md.
This project is independently maintained. If your organization wants priority integration help, a hardened deployment review, sponsored features, or talks and workshops on safe agentic operations, connect on LinkedIn (details in SUPPORT.md).
Sandeep Bazar - Engineering Leader. Multi-cluster Kubernetes platforms, day-2 operations, and making fleets safer to automate.
If this project is useful to you, a ⭐ helps others find it.
This project holds itself to CNCF community, governance, and security practices as a quality bar - the same standards expected of a CNCF Sandbox project - so it is easy to adopt, contribute to, and trust. The scaffolding is in place:
Every change is signed off under the DCO, and any change that touches a guardrail requires a written safety rationale.
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows