Kubernetes Tools
k8stools gives AI agents read-only access to a Kubernetes cluster. It provides 31 tools: from "what is wrong in this namespace?" down to a single pod's spec or logs. You can use them in two ways:
- As an MCP server (
k8s-mcp-server, included), for Claude Code, Cursor, GitHub Copilot or any other MCP client. - As Python functions, passed straight to an agent framework such as pydantic-ai, or called from ordinary scripts.
Some use cases:
- Chat with your Kubernetes cluster from your coding agent.
- Build agents that monitor your cluster or perform root cause analysis.
- Build a custom chat UI, or use the tools in non-agentic automations.
- Snapshot a live cluster to a file and replay it later as a test fixture, with no cluster needed. See Capturing and replaying a cluster.
Every tool only reads. None changes the cluster, and none reads Kubernetes
Secret objects. Secret-shaped values in other objects (ConfigMaps, env vars,
command lines, logs) are redacted by default.
Contents
- Quick start
- Toolsets: choosing which tools an agent sees
- The tools
- Running the MCP server
- Using the tools from Python
- Mock data
- Capturing and replaying a cluster
- Secret redaction
- Design approach
- Development
What changed in each release is in the changelog.
Quick start
1. Install
k8stools needs Python 3.12 or newer.
pip install k8stools # or: uv add k8stools
This installs three commands:
| Command | What it does |
|---|---|
k8s-mcp-server |
The MCP server |
k8s-capture-state |
Snapshots a cluster to a file that the server can replay |
k8s-mcp-client |
Lists the server's tools and their schemas, for manual checking |
2. Try it without a cluster
--mock serves a built-in snapshot of a small cluster that has a few things wrong
with it (see Mock data), so you can see what the tools do before
pointing them at anything real. To add it to Claude Code:
claude mcp add k8stools-mock -- k8s-mcp-server --mock
Then ask something like "What's wrong in the default namespace?"
For VS Code, Cursor or another client that reads an mcp.json, see
Connecting an MCP client.
3. Point it at your cluster
Without --mock, the server uses the same cluster as kubectl: the file in
KUBECONFIG (or ~/.kube/config) and its current context. To choose a different
one, pass --kubeconfig and/or --context:
claude mcp add k8stools -- k8s-mcp-server --context my-cluster
The account it runs as needs only get and list permissions. See
Permissions for a ClusterRole that covers every tool, and
Selecting a cluster for the details.
Toolsets: choosing which tools an agent sees
An MCP client sends every tool's description to the model on every turn, and
agents choose less reliably when they have many similar tools to pick from. So the
server doesn't have to offer all 31 tools: it serves a named toolset, chosen
with --toolset:
| Toolset | What it contains | Use it when |
|---|---|---|
triage |
5 tools: get_cluster_info, get_namespace_health, get_workload_report, get_events, get_node_summaries |
The agent's job is to find what's wrong and where. The two composite tools answer in one call what otherwise takes five or six. |
investigate (default) |
27 tools: every tool except the four that another tool already covers | The agent needs to dig into details: specs, history, logs, networking, storage. |
all |
All 31 tools | You want the full set, as k8stools 2.x served. |
The four tools that investigate leaves out, and what to use instead:
| Left out | Use instead |
|---|---|
get_pod_events |
get_events(involved_name=...) |
get_replicaset_summaries |
get_workload_history |
get_logs_for_job, get_logs_for_cronjob |
get_logs_for_pod_and_container; each pod's owner names its Job |
You can adjust any toolset with --include TOOL and --exclude TOOL. Both can be
repeated:
# The triage tools, plus pod specs and container logs
k8s-mcp-server --toolset triage --include get_pod_spec --include get_logs_for_pod_and_container
# Everything except ConfigMap contents
k8s-mcp-server --toolset all --exclude get_configmap
A misspelled toolset or tool name stops the server at startup, rather than
quietly serving a different set. The table of tools below shows which
toolsets each tool is in. In Python, the same sets are in k8s_tools.TOOLSETS
(see Choosing tools in Python).
The tools
Each tool's description, which is what the model sees, is kept to a few lines. docs/TOOL_REFERENCE.md documents every parameter and field.
Warnings that apply to a particular result come back in that result, rather than
in the description: nodes, pods, container statuses, events and DaemonSets have a
notes list (for example, that a node's Ready time is not its uptime), and a log
can begin with [k8stools] note: lines (for example, that the "previous" log is
the same as the current one). See
Notes in results.
In the tables below, T means the tool is in the triage toolset and I that
it is in investigate. Every tool is in all.
Start here: overview tools
| Tool | What it answers | T | I |
|---|---|---|---|
get_namespace_health |
What is wrong in a namespace, and where. A full entry for each unhealthy workload (last termination with the exit code's meaning, how long instances live, the restart back-off, memory limit vs request, when the template last changed), the same for each recovering one (ready, but restarted within the last 24 hours), and one line for each healthy one. Workloads failing the same way are grouped. | ✓ | ✓ |
get_workload_report |
Everything about one workload in one call: images, resources and probes; each instance's state and last termination; deduplicated events; current and previous log tails; the last template change; the ConfigMaps and Secrets it uses; its Services, Ingress rules, HPA and resource usage. | ✓ | ✓ |
get_cluster_info |
Which cluster the tools are answering from: kubeconfig context, API server URL and version, or the capture being replayed. | ✓ | ✓ |
These composite tools are built from the other tools, and report facts rather than diagnoses: they say "exit code 137, without OOMKilled", not "this was an out-of-memory kill".
Cluster and nodes
| Tool | What it answers | T | I |
|---|---|---|---|
get_namespaces |
The namespaces, like kubectl get namespace |
✓ | |
get_node_summaries |
The nodes, like kubectl get nodes -o wide, with capacity, allocatable, conditions, taints and labels |
✓ | ✓ |
get_node_metrics |
Each node's current CPU and memory use against its allocatable, like kubectl top node (needs metrics-server) |
✓ | |
get_events |
Cluster- or namespace-wide events, filtered on the server (by object, reason, type) | ✓ | ✓ |
Pods, containers and logs
| Tool | What it answers | T | I |
|---|---|---|---|
get_pod_summaries |
The pods, like kubectl get pods -o wide, with each pod's controlling owner (e.g. DaemonSet/otel-collector-agent) |
✓ | |
get_pod_container_statuses |
Each container's state in a pod: running, waiting or terminated, restarts, last termination. Init containers are listed separately ([] when there are none); native sidecars are listed with the app containers. |
✓ | |
get_pod_spec |
A pod's full spec | ✓ | |
get_pod_events |
The events for one pod | ||
get_logs_for_pod_and_container |
A container's logs, with tail, since_seconds and previous (the instance before the last restart). See Previous-instance log semantics. |
✓ | |
get_container_metrics |
Each running container's current CPU and memory use beside its requests and limits, like kubectl top pod --containers (needs metrics-server) |
✓ |
Workloads
| Tool | What it answers | T | I |
|---|---|---|---|
get_deployment_summaries |
Deployments, like kubectl get deployments |
✓ | |
get_statefulset_summaries |
StatefulSets, like kubectl get statefulsets |
✓ | |
get_daemonset_summaries |
DaemonSets, like kubectl get daemonsets -o wide, with node selector and images |
✓ | |
get_workload_history |
What changed in a Deployment, StatefulSet or DaemonSet, and when: each retained revision compared with the one before (images, resources, probes, command and args, env var names, volumes, scheduling), plus the ConfigMaps and Secrets it references. It also says what the history cannot show. | ✓ | |
get_replicaset_summaries |
A Deployment's ReplicaSets with their revision numbers and images | ||
get_hpa_summaries |
HorizontalPodAutoscalers, like kubectl get hpa plus describe: target, min/max/current/desired replicas, each metric against its target, and the scaling conditions |
✓ | |
get_cronjob_summaries |
CronJobs, like kubectl get cronjobs |
✓ | |
get_job_summaries |
Jobs, like kubectl get jobs |
✓ | |
get_logs_for_job |
Logs from a Job's most recent pod | ||
get_logs_for_cronjob |
Logs from a CronJob's most recent run |
Networking
| Tool | What it answers | T | I |
|---|---|---|---|
get_service_summaries |
Services, like kubectl get services, with selector, labels, annotations, and ready / not-ready endpoint counts |
✓ | |
get_endpoint_summaries |
Each Service's backends, from its EndpointSlices: every address's ready / serving / terminating state, pod and node | ✓ | |
get_ingress_summaries |
Ingresses, like kubectl get ingress plus describe: each host/path rule with its backend Service and port resolved (flagged when the Service or port doesn't exist), TLS hosts and Secret names, load-balancer addresses |
✓ |
Configuration and storage
| Tool | What it answers | T | I |
|---|---|---|---|
get_configmap_summaries |
ConfigMaps, like kubectl get configmaps |
✓ | |
get_configmap |
The full contents of one ConfigMap (redacted by the server) | ✓ | |
get_pvc_summaries |
PersistentVolumeClaims, like kubectl get pvc, with the pods that mount each one |
✓ |
Custom resources
| Tool | What it answers | T | I |
|---|---|---|---|
get_custom_resource_definitions |
The custom kinds the cluster serves (CRDs): group, kind, plural, scope, versions | ✓ | |
get_custom_resource_status |
Custom resources' health: each instance's conditions (Ready, Synced, ...) and whether its controller has caught up with its spec. Not the objects themselves, which can be huge and can hold credentials. | ✓ |
Running the MCP server
Connecting an MCP client
The server uses the stdio transport by default, which is what local agents such as Claude Code, Cursor and VS Code expect: the client starts the server itself.
Claude Code:
claude mcp add k8stools -- k8s-mcp-server
# with options:
claude mcp add k8stools -- k8s-mcp-server --context prod-eu --toolset triage
VS Code, Cursor and other clients with an mcp.json:
{
"servers": {
"k8stools": {
"command": "${workspaceFolder}/.venv/bin/k8s-mcp-server",
"args": ["--toolset", "investigate"],
"envFile": "${workspaceFolder}/.envrc"
}
}
}
This assumes that:
- k8stools is installed in a virtual environment in
.venv, at the root of your workspace. - The environment file
.envrcsets any variables you need: for exampleKUBECONFIGto point to your kubeconfig file andK8STOOLS_CONTEXTto pin a context. You can pass--kubeconfigand--contextinargsinstead.
Command-line options
The options, grouped by what they're for:
| Purpose | Options |
|---|---|
| Which tools to serve | --toolset {triage,investigate,all} (default investigate), --include TOOL, --exclude TOOL. See Toolsets. |
| Which cluster to read | --kubeconfig PATH, --context NAME. See Selecting a cluster. |
| Serve a snapshot instead of a cluster | --mock (the built-in snapshot), --state-file FILE (one of your own), --state-time {advancing,frozen}. See Mock data and Capturing and replaying a cluster. |
| Secret redaction | --no-redact turns it off (it is on by default). See Secret redaction. |
| Transport | --transport {stdio,streamable-http} (default stdio), --host (default 127.0.0.1), --port (default 8000) |
| Logging | --log-level, --debug |
Some common combinations:
k8s-mcp-server # stdio, current kubeconfig context, investigate toolset
k8s-mcp-server --toolset triage # just the five triage tools
k8s-mcp-server --context prod-eu # a specific cluster
k8s-mcp-server --mock # the built-in snapshot, no cluster needed
k8s-mcp-server --state-file incident-1234.json # replay your own capture
k8s-mcp-server --transport=streamable-http --port=8000 # HTTP, for remote access
Full k8s-mcp-server --help
usage: k8s-mcp-server [-h] [--transport {streamable-http,stdio}] [--host HOST] [--port PORT]
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
[--kubeconfig PATH] [--context NAME] [--mock] [--state-file FILE]
[--state-time {advancing,frozen}] [--no-redact]
[--toolset {triage,investigate,all}] [--include TOOL] [--exclude TOOL]
Run the MCP server.
options:
-h, --help show this help message and exit
--transport {streamable-http,stdio}
Transport to use for MCP server [default: stdio]
--host HOST Hostname for HTTP service [default: 127.0.0.1]
--port PORT Port for HTTP service [default: 8000]
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
Log level [default: INFO]
--debug Enable debug mode [default: False]
--kubeconfig PATH Kubeconfig file to use [default: KUBECONFIG, then ~/.kube/config]
--context NAME Kubeconfig context to use [default: K8STOOLS_CONTEXT, then the
kubeconfig's current-context]. A kubeconfig or context that is given but
cannot be loaded stops the server rather than falling back to another
cluster.
--mock If specified, just run mock versions of the tools that don't need a
cluster
--state-file FILE Serve a captured cluster snapshot from FILE (implies --mock). Captures are
written by k8s-capture-state.
--state-time {advancing,frozen}
Replay clock for a captured snapshot [default: advancing]. 'frozen' pins
every age at its captured value, so repeated runs are identical - what an
automated suite wants.
--no-redact Disable secret redaction of tool output (redaction is on by default; can
also be disabled with K8STOOLS_REDACT=0)
--toolset {triage,investigate,all}
Which tools to serve [default: investigate]. 'triage' is the few needed to find
where to look, 'investigate' is every tool but those another tool covers,
'all' is everything.
--include TOOL Also serve TOOL (repeatable)
--exclude TOOL Don't serve TOOL (repeatable)
Environment variables:
| Variable | Effect |
|---|---|
KUBECONFIG |
Kubeconfig file, when --kubeconfig isn't given |
K8STOOLS_CONTEXT |
Kubeconfig context, when --context isn't given |
K8STOOLS_REDACT=0 |
Turns redaction off, like --no-redact |
When a tool call fails
A failed call comes back to the client as an error result with the reason, for
example 404 Not Found: pods "x" not found, a permission error naming what the
account can't list, can't reach the Kubernetes API server at ..., or why a
log can't be read, so an agent can correct itself instead of retrying blindly.
The reason is redacted like any result. An unexpected failure (a bug) shows
only Error executing tool <name>, and its details go to the server log.
Selecting a cluster
Each server answers from exactly one cluster, chosen at startup:
| Kubeconfig file | Context | |
|---|---|---|
| 1st | --kubeconfig PATH |
--context NAME |
| 2nd | KUBECONFIG |
K8STOOLS_CONTEXT |
| 3rd | ~/.kube/config |
the file's current-context |
With none of these given and no usable kubeconfig, the server falls back to the in-cluster service account, as when it runs in a pod.
The server logs the cluster it chose when it starts:
WARNING:root:Bound to cluster: context 'prod-eu' at https://prod-eu.example:6443 (kubeconfig: /home/me/.kube/prod.yaml)
and the get_cluster_info tool reports it, so an agent can check which cluster it
is talking to.
A few rules keep a server from quietly answering from the wrong cluster:
- An explicit selection never falls back. If a
--kubeconfig,--contextorK8STOOLS_CONTEXTis given but cannot be loaded (a missing file, a misspelled context), the server exits with an error. It does not fall back to in-cluster config, which in a pod would silently mean that pod's cluster. - The choice is made once. Every tool shares it, and it does not follow later
changes to the kubeconfig: running
kubectl config use-contextdoes not repoint a server that is already running. --kubeconfigand--contextare rejected with--mockor--state-file, which serve a snapshot rather than a cluster.
To serve two clusters, run two servers:
k8s-mcp-server --transport=streamable-http --port=8001 --context prod-eu
k8s-mcp-server --transport=streamable-http --port=8002 \
--kubeconfig ~/.kube/eks.yaml --context staging
Permissions
The tools only read, with the get and list verbs. A read-only ClusterRole
that covers every tool:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k8stools-reader
rules:
- apiGroups: [""]
resources: [namespaces, nodes, pods, pods/log, events, services, configmaps,
persistentvolumeclaims]
verbs: [get, list]
- apiGroups: [apps]
resources: [deployments, replicasets, statefulsets, daemonsets, controllerrevisions]
verbs: [get, list]
- apiGroups: [batch]
resources: [jobs, cronjobs]
verbs: [get, list]
- apiGroups: [autoscaling]
resources: [horizontalpodautoscalers]
verbs: [get, list]
- apiGroups: [discovery.k8s.io]
resources: [endpointslices]
verbs: [get, list]
- apiGroups: [metrics.k8s.io]
resources: [pods, nodes]
verbs: [get, list]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list]
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
verbs: [get, list]
get_custom_resource_status also needs read access to each custom resource you
want it to see, granted per group. Add a rule like this for each operator you
use, rather than a wildcard:
- apiGroups: [cert-manager.io]
resources: [certificates, issuers, clusterissuers]
verbs: [get, list]
get_cluster_info also reads the server version (/version), which every
authenticated user can read by default. No tool reads Secrets: get_workload_history
names the Secrets a workload uses, from its pod template, without reading them.
The HTTP transport
--transport=streamable-http starts an HTTP server that listens on --host and
--port (by default 127.0.0.1 and 8000). Use it when the agent is not on the
same machine as the server.
Here's how to start the server and check it with curl:
# start the server
$ k8s-mcp-server --transport=streamable-http
INFO: Started server process [6649]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# in another terminal, list the tools
$ curl -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}' \
http://127.0.0.1:8000/mcp
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[.... long text elided ...]}}
Checking the tools with k8s-mcp-client
k8s-mcp-client starts the server over stdio and prints each tool's description
and input schema as Markdown. --short prints only the descriptions, and
--tools NAME ... limits it to the tools named. It passes KUBECONFIG and
K8STOOLS_CONTEXT through to the server.
Using the tools from Python
In an agent
The tools are in k8stools.k8s_tools. Here's an example with pydantic-ai:
from pydantic_ai.agent import Agent
from k8stools.k8s_tools import TOOLS
from k8stools.redaction import wrap_with_redaction
agent = Agent(
model="openai:gpt-4.1",
system_prompt=SYSTEM_PROMPT,
tools=[wrap_with_redaction(fn) for fn in TOOLS],
)
result = agent.run_sync("What is the status of the pods in my cluster?")
print(result.output)
⚠️ Redaction is not automatic outside the k8stools MCP server. The functions in
TOOLS(andmock_tools.TOOLS) return raw values: ConfigMap contents, env values in pod specs, container logs and command-line args, exactly as the API returns them. Onlyk8s-mcp-serverandk8s-capture-stateapply redaction for you. If you handTOOLSto an agent, or build your own MCP server from them, wrap each one withwrap_with_redactionas above, or anything secret-shaped in your cluster goes straight into the model's context. See Where redaction applies.
Choosing a cluster
The tools use KUBECONFIG (or ~/.kube/config) and its current context. To pin a
cluster instead, call configure before the first tool call:
from k8stools import k8s_tools
k8s_tools.configure(kubeconfig="~/.kube/prod.yaml", context="prod-eu")
The selection is resolved as described in Selecting a cluster.
Choosing tools in Python
TOOLS is every tool. The toolsets are in TOOLSETS, and select_tools builds an
adjusted one, just like --toolset, --include and --exclude:
from k8stools.k8s_tools import TOOLS, TOOLSETS, select_tools
triage = TOOLSETS["triage"]
mine = select_tools(TOOLS, "triage", include=("get_pod_spec",), exclude=("get_events",))
tools = [wrap_with_redaction(fn) for fn in mine] # wrap them, as above
mock_tools.TOOLSETS holds the same sets of mock tools.
Printing results
Most get_* tools have a print_* companion that prints the result as a table,
which is handy when debugging (for example print_pod_summaries,
print_workload_history, print_hpa_summaries). The log readers have none, since
they already return text.
from k8stools.k8s_tools import print_pod_summaries
print_pod_summaries("default")
Mock data
When building agents, it can be helpful to test them against mock versions of the
tools that don't need a real cluster but return realistic values. The module
k8stools.mock_tools does just that, and so does the server's --mock option.
The data is a small, hand-maintained cluster modeled on a Minikube instance running the OpenTelemetry Demo application, with problems for an agent to find:
- a crash-looping
adservice, mid-upgrade, whose Service has no ready backends; - an
accountingservice that is ready but was OOM-killed 40 minutes ago, after 578 restarts, and whose previous log file is gone; - a Deployment whose HPA is at its maximum;
- an Ingress with one rule to a missing Service;
- a cert-manager
Certificatethat isn't Ready; - a DaemonSet, a CronJob and its Job, a StatefulSet with PVCs and a completed init container, and events.
The mock serves a capture (a JSON snapshot of one cluster) rather than a set of canned answers per tool, so the tools agree with each other: a pod that is not in the capture does not exist for any tool, and one that is has consistent statuses, events, spec and logs. You can snapshot your own cluster the same way.
Capturing and replaying a cluster
k8s-capture-state snapshots a live cluster into a single JSON file, and the MCP
server replays that file as if it were the cluster. A whole scenario, such as a
crash loop, a full volume or a bad rollout, becomes a fixture you can re-run,
commit, or attach to an issue, with no cluster needed to reproduce it.
# Snapshot the current cluster (respects KUBECONFIG)
k8s-capture-state -o incident-1234.json
# Replay it
k8s-mcp-server --state-file incident-1234.json
It finishes with a summary of what it got:
$ k8s-capture-state --namespace default -o incident-1234.json
Captured 27 pod(s), 31 container(s).
logs captured: 31
previous logs captured: 31
values redacted: 4
Wrote incident-1234.json (1,435,335 bytes, redacted).
k8s-capture-state options
usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
[--kubeconfig PATH] [--context NAME] [--no-logs]
[--max-log-lines MAX_LOG_LINES] [--no-previous-logs]
[--no-redact] [--redact-file FILE]
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
Snapshot a Kubernetes cluster to a JSON file replayable by the k8stools MCP
server (--state-file) or MockState.
options:
-h, --help show this help message and exit
--namespace NS [NS ...]
Namespace(s) to capture namespaced resources from
[default: all]. Namespaces and nodes are always
captured in full.
-o FILE, --output FILE
Output file [default: k8s-state-<timestamp>.json]. A
name ending in .gz is written gzipped; captures are
read back compressed or not either way.
--kubeconfig PATH Kubeconfig file to capture from [default: KUBECONFIG,
then ~/.kube/config]
--context NAME Kubeconfig context to capture from [default:
K8STOOLS_CONTEXT, then the kubeconfig's current-
context]
--no-logs Skip container logs entirely, including previous-
instance logs, for a structure-only snapshot
--max-log-lines MAX_LOG_LINES
Log lines to capture per container [default: 1000]
--no-previous-logs Skip the previous-instance logs of restarted
containers. These usually carry the diagnosis for a
crash loop, so dropping them is rarely what you want.
--no-redact Disable secret redaction of captured values (redaction
is on by default; can also be disabled with
K8STOOLS_REDACT=0)
--redact-file FILE Instead of capturing, re-run redaction over an
existing capture FILE and rewrite it in place (keeping
its compression). Use on captures taken with --no-
redact, or before a newer k8stools added redaction
rules.
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
Log level [default: INFO]
What gets captured
Everything the tools can read, so that every tool answers on replay:
| Cluster | context name, API server URL and version, so get_cluster_info answers on replay (not the kubeconfig path, which names a file on the capturing machine) |
| Cluster-wide | namespaces, nodes, custom resource definitions and their instances' conditions (up to 500 per type), and resource usage (one metrics-server sample of containers and nodes, or why metrics weren't available) |
| Per namespace | deployments, replica sets, services and their endpoints, ingresses, statefulsets, daemonsets, HPAs, cronjobs, jobs, PVCs, events, and each workload's change history (get_workload_history's result, so no env values are written) |
| ConfigMaps | summary and full contents, so get_configmap works too |
| Per pod | summary, labels, container and init container statuses, spec, and per-container logs (init containers included) |
A cluster is selected exactly as for the MCP server (see Selecting a cluster), and a selection that cannot be loaded fails the capture. The capture needs the permissions in Permissions; a custom-resource type it can't read is recorded as such rather than failing the capture, and so is a cluster without metrics-server.
--namespace restricts only the namespaced resources; namespaces and nodes are
always captured in full so the cluster still makes sense.
Logs dominate the file size: expect a few MB for a few dozen pods (the 27-pod
capture above was 1.4 MB with only 50 lines per container; a 38-pod one at 200
lines was 3.3 MB). Use --max-log-lines to trade size against history, or
--no-logs for a structure-only snapshot. Captures compress about 10x, and can be
written and read gzipped; see Compression, and what to check
in.
Captures made by older versions of k8stools still load. Data they don't have
replays as empty, as unknown (None, never 0), or as an error saying the capture
predates it.
Compression, and what to check in
A name ending in .gz is written gzipped, and captures are read back compressed
or not either way. Detection is by content, so a capture still loads after being
renamed:
k8s-capture-state -o incident-1234.json.gz # 3.3 MB -> 364 KB
k8s-mcp-server --state-file incident-1234.json.gz
For a fixture you check into git, use plain .json anyway. Git already stores
blobs compressed, so you do not pay the uncompressed size, and it deltas
successive versions of a text file against each other, which it cannot do for a
gzip blob. Measured on two captures of the same 38-pod cluster:
| first commit | updating the fixture later | |
|---|---|---|
.json (3.3 MB) |
404 KB | +61 KB |
.json.gz (364 KB) |
388 KB | +358 KB |
The two cost about the same once, but every re-capture of a .gz adds a whole
fresh blob, so the repo grows roughly 6x faster. Captures are also indented rather
than compact for the same reason: it gives git something to delta. This is also
why you probably do not need git-lfs here: LFS stores every version whole, which
gives up exactly the delta compression that makes these files cheap.
Reach for .gz when the file travels on its own: attached to an issue, dropped
in object storage, or simply too large to keep expanded in a working tree.
Previous-instance logs
The capture includes the previous-instance logs of every restarted container,
which is what get_logs_for_pod_and_container(..., previous=True) returns. For a
crash-looping container the current instance's logs are usually empty or
post-restart, and the stack trace or OOM message that explains the crash lives in
the previous instance, so a capture without them would be missing the evidence
that matters most.
They roughly double the log payload for a cluster where a lot is restarting,
which is exactly the cluster worth capturing. --no-previous-logs skips them. The
summary reports how many could not be read (the previous instance may have been
garbage-collected), because a crash-loop capture that lost them looks identical to
one that never had any.
Previous-instance log semantics
previous=True asks the kubelet for the container's last terminated instance.
Three of its behaviors read as bugs and are not, and all three show up in captures
as well as in live queries:
- In CrashLoopBackOff, both calls return the same text. There is no running
instance during the backoff, so the kubelet serves the most recently terminated
one for
previous=Falsetoo. Checkget_pod_container_statusesbefore concluding the flag was ignored. - A restart between two calls shifts the window. The instance that was current becomes the previous one, so a container crash-looping every few seconds can return byte-identical output for both. Compare the log timestamps, not the flag.
- A reclaimed log file returns 200, not an error, with the body
unable to retrieve container logs for <container-id>. It is a successful call whose content is an error message. The log tools don't return that text, which agents kept reading as the program's output: they raiseK8sLogReclaimed, andget_workload_reportshows the log tail asavailable: false, with no lines.
Only the single most recent terminated instance is retained; there is no way to reach further back than one.
The first is detected for you: the log then starts with a [k8stools] note:
line saying so. The second can't be seen from a single call.
Replay clock
Every age in a capture is stored relative to the moment of capture, so the intervals between resources survive replay exactly: a deployment aged 8 days whose newest replica set is aged 7h34m still says "upgraded 7h34m ago" whenever it is replayed.
By default those ages advance in real time from when the server starts, which is
what makes an interactive session feel like a live cluster. For an automated test
suite pass --state-time frozen:
k8s-mcp-server --state-file incident-1234.json --state-time frozen
which pins every age at its captured value, so repeated runs are identical and a
long session cannot see ages drift between its first tool call and its last.
(--state-time requires --state-file; it is rejected rather than ignored, since
a suite that believed it had frozen the clock and had not would be flaky with no
visible cause.)
Absolute times are replayed the same way: every timestamp a tool returns is moved
so the moment of capture becomes the moment the server started. A container killed
5 minutes before the capture reads as killed 5 minutes before server start, and
get_cluster_info's captured_at is that start time, not the date in the file,
so the agent sees one consistent clock. Log line timestamps (the kubelet's
prefix) are moved too, so since_seconds works on a capture of any age;
timestamps an application writes inside its own log messages are left as
recorded. The file's own capture date is in the server's startup log.
Replaying from Python
For a test suite, skip the server and load the capture directly. MockState has
the same query methods as the tools:
from pathlib import Path
from k8stools.mock_state import MockState
state = MockState.from_file(Path("incident-1234.json"), frozen=True)
pods = state.get_pod_summaries("default")
logs = state.get_logs_for_pod_and_container("ad-647b4947cc-s5mpm", "default",
previous=True)
Or point the mock_tools module at it, so anything already calling
k8stools.mock_tools serves your capture instead of the built-in one:
from k8stools import mock_tools
mock_tools.load_mock_state(Path("incident-1234.json"), frozen=True)
agent = Agent(model="openai:gpt-4.1", tools=mock_tools.TOOLS)
Captures and secrets
k8s-capture-state applies the same redaction pass as the MCP server, on by
default, opt out with --no-redact or K8STOOLS_REDACT=0. This matters because
a capture file is far more portable than cluster access: it gets committed to
git, attached to issues, and passed around long after anyone remembers which
cluster it came from. Redaction is one-way: replaying a redacted capture with
--no-redact restores nothing, so a scenario that genuinely needs real values has
to be re-captured.
A capture taken with --no-redact, or by an older k8stools before a redaction
rule existed, can be redacted afterwards. This rewrites the file in place, keeping
its compression; running it again finds nothing more:
k8s-capture-state --redact-file incident-1234.json
Captures taken before 3.0.0 may hold credentials embedded in args, URLs and connection strings (see below). Re-redact any you've committed or shared.
Secret redaction
Some read-only resources can carry secret-shaped values even though they are not
Kubernetes Secret objects: ConfigMap data and the env blocks in a pod spec
are the common cases. When you run the k8stools MCP server directly against an
agent (with no wrapping service to scrub output), those values would otherwise flow
straight into the model's context.
To prevent that, the MCP server applies a redaction pass to every tool's output.
It is on by default and can be disabled with --no-redact or by setting
K8STOOLS_REDACT=0. Redaction matches three ways, replacing each match with a
visible [REDACTED] marker so the agent can tell "hidden" from "absent". We never
provide a reader for Kubernetes Secret objects.
-
By value shape: AWS access keys, JWT / bearer tokens, PEM private-key blocks.
-
By key / env-var name: the name contains
key,secret,token,password,passwd,credentialorcredas a whole word. Names are split on separators and camelCase, soAWS_SECRET_ACCESS_KEY,apiKeyandx-auth-tokenall match whileVALKEY_ADDRdoes not.A field named exactly
keyis exempt: in the Kubernetes API that is always a map entry, a taint, a label selector or a projected-volume item, never a credential. This matters more than it sounds: on a real 38-pod cluster it was 130 of 139 redactions, none of them secrets: toleration keys such asnode.kubernetes.io/not-ready, the filenameca.crt, andsecretKeyRef.key. That last is worth stating plainly: a reference to a secret is not a secret. The value lives in theSecretobject and is resolved by the kubelet, never appearing in tool output, so redacting the pointer hides which key feeds an env var and protects nothing. The exemption does not apply to env-var names, where a variable someone namedKEYplausibly does hold one. Schema fields that compoundkeyare exempt too, in both spellings: an affinity term'stopologyKey/topology_keyand label-key lists, andget_configmap'sbinary_data_keys. -
By context: a credential inside a longer string, found by what surrounds it, with only the credential replaced:
Where Example URL userinfo postgres://otelu:[REDACTED]@postgresql/otelSensitive flags and system properties --password=[REDACTED],--token [REDACTED],-Dapi.key=[REDACTED]Argv lists ["--token", "[REDACTED]"]Connection strings Password=[REDACTED];,password=[REDACTED] dbname=otelQuery strings and presigned URLs ?access_token=[REDACTED]&,X-Amz-Signature=[REDACTED],sig=[REDACTED]Properties files spring.datasource.password=[REDACTED]SQL password literals CREATE USER otelu WITH PASSWORD '[REDACTED]',IDENTIFIED BY '[REDACTED]'In running text (
sh -c "...", logs,--helpoutput), a--flag valuepair counts only when the value looks like a credential (a digit or symbol, or 16+ characters), because prose and help text put words and type names after flags.
Nothing that only locates a secret is redacted. Like secretKeyRef.key, these
are references, and hiding them hides where a secret comes from and protects
nothing: $(DB_PASSWORD), ${token}, $API_KEY, templates, file paths
(--tls-key /etc/certs/tls.key), and flags named for a location
(--password-file, --secret-name, --token-url). Booleans and masks
(skip-secret: "true", password=****) are left alone too.
On a real 38-pod cluster, these rules redact 7 values, all real credentials (the seventh is the SQL password literal), where the 2.x rules redacted 6 of which 3 were not secrets.
Where redaction applies
| How the tools are used | Redacted? |
|---|---|
k8s-mcp-server |
Yes, on by default; --no-redact or K8STOOLS_REDACT=0 turns it off |
k8s-capture-state |
Yes: the capture applies the pass itself before writing the file |
k8s_tools.TOOLS / mock_tools.TOOLS given to an agent framework, or composed into your own MCP server |
No. Wrap each function: [wrap_with_redaction(fn) for fn in TOOLS] |
| Calling a tool function directly in Python | No. Apply the pass to the result: redact_object(result) returns (redacted, count) |
wrap_with_redaction keeps each function's name, signature and docstring, so a
framework that builds tool schemas from them (pydantic-ai, MCP) sees the same tool.
Redaction cannot catch everything. A secret with nothing naming it, such as a bare
positional argument (app hunter2), can't be told apart from any other value; nor
can an unusual format none of the rules above recognise.
Design approach
Our goal is quality over quantity: well-documented, strongly typed tools. We believe this is critical for agents to use tools effectively, beyond simple demos.
The tools are built on the Kubernetes Python client, and return one of three kinds of result:
- kubectl-like: Pydantic models that mirror a
kubectlcommand's output. For example,get_pod_summariesis equivalent tokubectl get pods -o wide. - API-typed: Pydantic models that match the Kubernetes client's types
(see https://github.com/kubernetes-client/python/tree/master/kubernetes/docs),
with lesser-used fields omitted. For example,
get_pod_container_statuses. - Raw dict: the result of
to_dict()on the API's object (defined in https://github.com/kubernetes-client/python/tree/master/kubernetes/client/models), with its fields documented in the tool's docstring. For example,get_pod_spec.
Read-only tools come first, because we are focusing on monitoring and root cause analysis. If we add tools that change the cluster, they will be kept separate from the read-only ones, so you can still build "safe" agents.
Development
git clone https://github.com/BenedatLLC/k8stools.git
cd k8stools
uv sync
# Unit tests (no cluster needed)
uv run pytest -v
# Integration tests against a real cluster (skipped automatically if unreachable)
KUBECONFIG=~/.kube/your-config.yaml uv run pytest tests/test_k8s_tools_realk8s.py -v
Planned and deferred work is in docs/ROADMAP.md.
Metadata
Release files for k8stools 4.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| k8stools-4.0.1.tar.gz | 473.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| k8stools-4.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 602.1 kB
Release files / k8stools-4.0.1.tar.gz
| Download URL | k8stools-4.0.1.tar.gz |
|---|---|
| Size | 473.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a3dd12f2ff281c8b71e57878271c8fce0395b245dd00329adc24a28650b0275a
|
|
BLAKE2b-256 checksum How to use checksums |
ebbb368ab82ac59f60c28f74f6196adadb3dfeeabcf010072bb872157c74bc8a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / k8stools-4.0.1-py3-none-any.whl
| Download URL | k8stools-4.0.1-py3-none-any.whl |
|---|---|
| Size | 128.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9d0d320a55ed55591930e3938de4f9523972d8d5b7c883dbb2a088c078e74880
|
|
BLAKE2b-256 checksum How to use checksums |
1dede500bb336d082a45e8b479026181cf15467ccfb996df9bab75b53c15f022
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|