Skip to main content

Kubernetes Tools

Unit Tests

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.

Kubernetes Tools Server MCP server

Contents

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:

  1. k8stools is installed in a virtual environment in .venv, at the root of your workspace.
  2. The environment file .envrc sets any variables you need: for example KUBECONFIG to point to your kubeconfig file and K8STOOLS_CONTEXT to pin a context. You can pass --kubeconfig and --context in args instead.

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

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, --context or K8STOOLS_CONTEXT is 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-context does not repoint a server that is already running.
  • --kubeconfig and --context are rejected with --mock or --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 (and mock_tools.TOOLS) return raw values: ConfigMap contents, env values in pod specs, container logs and command-line args, exactly as the API returns them. Only k8s-mcp-server and k8s-capture-state apply redaction for you. If you hand TOOLS to an agent, or build your own MCP server from them, wrap each one with wrap_with_redaction as 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 ad service, mid-upgrade, whose Service has no ready backends;
  • an accounting service 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 Certificate that 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=False too. Check get_pod_container_statuses before 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 raise K8sLogReclaimed, and get_workload_report shows the log tail as available: 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.

  1. By value shape: AWS access keys, JWT / bearer tokens, PEM private-key blocks.

  2. By key / env-var name: the name contains key, secret, token, password, passwd, credential or cred as a whole word. Names are split on separators and camelCase, so AWS_SECRET_ACCESS_KEY, apiKey and x-auth-token all match while VALKEY_ADDR does not.

    A field named exactly key is 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 as node.kubernetes.io/not-ready, the filename ca.crt, and secretKeyRef.key. That last is worth stating plainly: a reference to a secret is not a secret. The value lives in the Secret object 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 named KEY plausibly does hold one. Schema fields that compound key are exempt too, in both spellings: an affinity term's topologyKey / topology_key and label-key lists, and get_configmap's binary_data_keys.

  3. 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/otel
    Sensitive flags and system properties --password=[REDACTED], --token [REDACTED], -Dapi.key=[REDACTED]
    Argv lists ["--token", "[REDACTED]"]
    Connection strings Password=[REDACTED];, password=[REDACTED] dbname=otel
    Query 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, --help output), a --flag value pair 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:

  1. kubectl-like: Pydantic models that mirror a kubectl command's output. For example, get_pod_summaries is equivalent to kubectl get pods -o wide.
  2. 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.
  3. 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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for k8stools 4.0.0
File Size Uploaded
k8stools-4.0.0.tar.gz 468.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for k8stools 4.0.0
File Interpreter ABI Platform
k8stools-4.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 595.9 kB

Release files / k8stools-4.0.0.tar.gz

Download URL k8stools-4.0.0.tar.gz
Size 468.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f1c891fb576456bed82103398c12f657272103828edc8baaf5a8c0eeae7877b5
BLAKE2b-256 checksum
How to use checksums
c9419a4df9f935d7f4f8241be858055cc46d819bfda8b41a4f53bf73699f8d13
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release files / k8stools-4.0.0-py3-none-any.whl

Download URL k8stools-4.0.0-py3-none-any.whl
Size 127.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
af090250e88c36067b451540b4d6c28c97bd117323198d4a33c6819da038aa62
BLAKE2b-256 checksum
How to use checksums
0666ac404a24ce8a739dde38c8b379a9cbc6f0e404fd3da6a90066e28fa76d2e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release history Release notifications | RSS feed

4.0.2

2 release files

4.0.1

2 release files

This release

4.0.0 This release

2 release files

3.0.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page