MCP server¶
kubeagent mcp serves the exact same deterministic diagnosis kubeagent scan
runs, over the Model Context Protocol on
stdio, so another AI agent can call kubeagent as a tool instead of shelling
out to the CLI and parsing text.
Note
Two separate claims, stated precisely rather than blurred together. The
server is read-only toward the cluster: every tool issues only
get/list/watch calls — the same calls scan makes. No tool can
reach the guard-railed --fix writer, and no tool name contains a write
verb; there is no code path from this package into internal/remediate.
Separately, and more strongly, the server makes no LLM call of its
own — nothing it returns is generated text. Every field in every
result is computed by kubeagent's detectors, exactly as scan computes
them; the calling agent may itself be a model, but kubeagent's half of
the conversation never is.
The four tools¶
| Tool | Arguments | What it does |
|---|---|---|
kubeagent_triage |
namespace (optional), context (optional) |
Runs the same scan kubeagent scan runs and returns a healthy/degraded verdict, the findings that support it, and a coverage block. |
kubeagent_inspect |
kind (required — pod, deployment, statefulset, daemonset, replicaset, job, or cronjob), namespace (required), name (required), context (optional) |
Drills into one workload or pod: its status, its pods, kubeagent's findings for it, and its recent Kubernetes events. A pod answer names the controller that owns it in owner. |
kubeagent_advisory |
sections (required — any of operators, drift, capacity, security, certificates), namespace (optional), context (optional) |
Runs kubeagent's opt-in advisory sections. Each section costs extra API reads, so it only runs what's requested. |
list_contexts |
none | Lists the kubeconfig contexts the server may switch between. Only registered when the server was started with --allow-context-switch. |
context on kubeagent_triage, kubeagent_inspect, and kubeagent_advisory
is accepted only when the server was started with --allow-context-switch;
otherwise a call naming one is rejected without ever reaching the cluster.
What kubeagent_inspect resolves¶
found: false means one thing: no object of that kind, with that name, exists
in that namespace in the snapshot the call collected. It is not a proxy for
"healthy", and it is not a truncation artefact. Existence is answered against
the raw objects the scan collected, not against the workload list the text
report renders — that list is filtered for display, so it drops the healthy
majority, which is right for a report an operator reads and wrong for a
lookup.
One display rule still shapes what a workload answer carries: a job or
cronjob answer lists at most three pod rows, the same cap the text report
uses, and the result carries no signal that rows were left out. The cap bounds
the pods array, never the found verdict — and a pod it leaves out is still
resolvable on its own, as kind: pod under its own name.
A found: false result still carries the object's recent events. That is
deliberate: "the object is gone but its events explain why" is exactly what a
drill-down has to answer, and a deleted pod's events are often the whole story.
An event's type, reason and message are free text the API server does not validate, so all three are sanitized before they reach a tool result: valid UTF-8, one line, no control characters and no Unicode formatting characters, bounded in length. This is the same treatment every other kubeagent surface gives an unvalidated API value, and it matters more here than on a terminal — a tool result is forwarded verbatim to a client that renders it however it likes, and JSON encoding is not sanitizing. It escapes an ESC byte; it passes U+202E RIGHT-TO-LEFT OVERRIDE through unchanged.
A pod answer describes the pod, not its controller. kind is Pod, status is
the pod's own phase, pods carries that one row, and findings are the pod's
own. desired and ready are absent rather than 0 — a pod has no replica
count, and absence must never read as zero. The controller that owns it is named
in owner:
{
"found": true,
"kind": "Pod",
"namespace": "payments",
"name": "worker-7d9c6f6b8-x2z4q",
"status": "Running",
"owner": "Deployment/worker",
"image": "registry.example.com/worker:1.4.0",
"pods": [ "…one row for this pod…" ],
"findings": [ "…this pod's own findings…" ],
"events": [ "…" ],
"coverage": { "…" }
}
owner is the escalation pointer: kubeagent_triage reports a critical finding
against a pod, and this is how a caller reaches the workload behind it without
guessing its name. The key is absent for a bare pod — one with no controller,
which must not claim an owner it does not have — and for every kind other than
pod.
The coverage block¶
Every result from the three diagnosis tools — kubeagent_triage,
kubeagent_inspect and kubeagent_advisory — carries a coverage object, so
a model can tell "nothing is wrong" from "nothing was checked": the same
failure mode a JSON reader hits when it treats an absent key as zero.
(list_contexts has no coverage block; it reads the kubeconfig, not the
cluster, so there is nothing for it to have looked at or missed.) A
kubeagent_triage call against a cluster with one crash-looping pod, started
without --logs, returns:
{
"verdict": "degraded",
"cluster": {
"context": "my-cluster",
"version": "v1.31.2",
"nodes": 3
},
"findings": [
{
"severity": "critical",
"kind": "Pod",
"namespace": "payments",
"name": "worker-7d9c6f6b8-x2z4q",
"reason": "Container repeatedly crashes after starting",
"detail": "Container repeatedly crashes after starting (container \"worker\", restartCount=5)",
"confidence": "high",
"remediationHint": "starts then crashes — inspect the crash output"
}
],
"coverage": {
"context": "my-cluster",
"namespaceScope": "all namespaces",
"collectedAt": "2026-07-28T09:14:02Z",
"checksRun": [
"workloads", "pod-diagnosis", "services", "ingresses",
"persistentvolumeclaims", "terminating", "poddisruptionbudgets",
"horizontalpodautoscalers", "webhooks", "resourcequotas"
],
"checksSkipped": [
{ "check": "credential-lint", "why": "not run by triage; use the kubeagent CLI" },
{ "check": "disk-usage", "why": "not run by triage; it needs node stats the server does not request" },
{ "check": "security", "why": "not run by triage; call kubeagent_advisory with section \"security\"" },
{ "check": "certificates", "why": "not run by triage; call kubeagent_advisory with section \"certificates\"" },
{ "check": "kubelet-health", "why": "not run by triage; it is opt-in and not reachable through kubeagent_advisory either — use the kubeagent CLI's --kubelet-health flag" },
{ "check": "control-plane-health", "why": "not run by triage; it is opt-in and not reachable through kubeagent_advisory either — use the kubeagent CLI's --control-plane-health flag" },
{ "check": "dns-health", "why": "not run by triage; it is opt-in and not reachable through kubeagent_advisory either — use the kubeagent CLI's --dns-health flag" },
{ "check": "log-tails", "why": "the server was started without --logs" }
],
"partial": [],
"metricsServer": "not-checked"
}
}
checksRun names every check that actually executed; checksSkipped names
every check that did not, each with a reason — kubeagent_triage
deliberately does not run credential linting, disk-usage, the security and
certificate sections (call kubeagent_advisory for those), or the three
opt-in health probes (kubelet, control-plane, DNS), which are not reachable
through kubeagent_advisory either — the CLI's --kubelet-health,
--control-plane-health, and --dns-health flags are the only way to run
them. Those seven are skipped on every kubeagent_triage call. The eighth
entry above, log-tails, is the one that varies: it is skipped only because
this server was started without --logs, and it moves to checksRun on a
server started with it. partial names a resource kubeagent tried to list and
couldn't, so an empty result is distinguishable from a denied one.
metricsServer is the literal string "not-checked" until a call actually
requests capacity data (kubeagent_advisory with section "capacity"); only
then does it become "available" or "absent". Read "not-checked" as "this
call never looked," not as "no metrics problem was found" — a model that
reads it as the latter will silently miss a missing metrics-server.
Configuration¶
Point an MCP host at the binary:
{
"mcpServers": {
"kubeagent": {
"command": "kubeagent",
"args": ["mcp", "--context", "my-cluster"]
}
}
}
Flags: --kubeconfig (default: the usual $KUBECONFIG/~/.kube/config
resolution), --context (default: the kubeconfig's current context),
--allow-context-switch (off by default), --logs (off by default — enables
the log-tail enrichment scan --logs performs).
Context switching¶
Off by default. A server started against one cluster only ever answers for
that cluster — a call naming a different context is rejected before it
reaches the cluster. Starting with --allow-context-switch also registers
list_contexts, and lets kubeagent_triage, kubeagent_inspect, and
kubeagent_advisory accept a context argument naming any context in the
same kubeconfig.
--allow-context-switch also changes what happens at startup when your
kubeconfig marks no context as current — the usual posture when you hold
several production kubeconfigs and do not want a stray kubectl to reach one.
Without the flag, the server exits: there is no default cluster and no way to
name one. With it, the server starts anyway, with no default cluster. All four
tools stay registered, list_contexts answers as usual with an empty
current, and a call naming a context works normally. A call naming none is
refused with a message telling the caller to list the contexts and pick one.
Nothing else degrades. A kubeconfig that cannot be read, one naming no
contexts at all, an API server that cannot be reached, and a --context that
does not resolve all still exit at startup — a server that starts and then
fails every call is worse than one that refuses to start. Those four are not
one error, though. Three of them fail before a connection is attempted and
exit with connecting to the cluster:, which names the kubeconfig file and
context it tried; that is the unredacted startup error the note below is
about. An API server that cannot be reached exits with reaching the API
server: instead, and that one is already redacted to scheme://host — no
kubeconfig path, no context name.
Freshness¶
There is no cache. Every call runs a fresh scan against the live cluster, so
an agent making several calls in a session never reasons about a stale
snapshot — the tradeoff is that each call costs the same API reads scan
would.
What it does not do¶
- No remediation. No tool can reach
--fix; the server has no write path into the cluster at all. - No
--explain. Nothing here calls an LLM — see the note at the top of this page. A calling agent is expected to reason over the structured result itself. - No
watch-style streaming. Each tool call is one point-in-time scan; there is no informer, no push notification, and no persistent session state between calls.
The startup error on stderr is not redacted
Every result on the protocol stream is free of kubeconfig paths, and the
address of the API server kubeagent connects to is reduced to
scheme://host wherever kubeagent reports it — that redaction is what
keeps list_contexts and every tool's error path safe to hand to a
remote model. (It governs the URLs kubeagent builds. A URL the cluster
wrote, inside an event or condition message, is quoted as it was written;
see the note below.) Startup is different. kubeagent mcp validates the
cluster connection before it starts serving — a server that starts
happily and then fails every call teaches the calling agent that
kubeagent is unreliable — and if the kubeconfig cannot be loaded, the
process exits with connecting to the cluster: naming the kubeconfig
file and context it tried, printed to stderr, because that is what
an operator needs to fix it. (The other startup failure, reaching the
API server:, is redacted to scheme://host like every other result;
only this one names a path.)
MCP hosts commonly capture a server subprocess's stderr into their own
logs. If your host treats its logs as shareable, know that this one
startup error is the exception to "no kubeconfig paths cross the MCP
boundary" — it is deliberate, not a defect, but it is not redacted.
A tool result carries API text, and API text can name a path or a URL
"Free of kubeconfig paths" is the whole of that promise, and it is worth
stating what sits outside it. A kubeagent_triage or kubeagent_inspect
result quotes events and condition messages — text the cluster itself
wrote — as the finding's detail. kubeagent passes them through
internal/safetext.Line, which normalises control characters and bounds
length; it does not filter them. Two shapes turn up in practice. The
first is a filesystem path: a kubelet FailedMount message routinely
names one under /var/lib/kubelet/, and that path is usually the
diagnostic an operator needs. The second is a full URL: when an
admission webhook is unreachable, the API server's FailedCreate message
quotes the endpoint it called — scheme://host/path?query, in-cluster
Service DNS with the webhook's path and its query parameters. That is
more than the scheme://host kubeagent reduces its own API server
address to, because it is not kubeagent's address to reduce: it is a
verbatim quote of what the cluster reported.
Both describe the cluster's own layout — not the operator's workstation,
and not which kubeconfig they hold, which is what makes the
kubeconfig-path rule a credential rule in the first place. If your
cluster's webhook endpoints are themselves sensitive, that is the one
thing here to weigh before handing a result to a remote model.