Kubernetes Tools MCP Server
A read-only Kubernetes MCP server for inspecting cluster state, workloads, configuration, logs, and events.
Identify the bound cluster and list namespaces and nodes.
Summarize pods, containers, deployments, ReplicaSets, StatefulSets, DaemonSets, Jobs, CronJobs, Services, ConfigMaps, and PVCs.
Inspect pod specs, container statuses, pod events, and filtered cluster/namespace events.
Fetch logs from pods/containers, Jobs, and CronJobs, including previous container instances.
Read ConfigMap contents and PVC status/mount details, with no Secret access and no write operations.
Enables chat interactions with Kubernetes clusters via GitHub Copilot, allowing users to query cluster status, inspect resources, and retrieve logs through natural language conversation supported by custom instruction files.
Provides a collection of Kubernetes tools for querying and monitoring cluster resources, including namespaces, nodes, pods, deployments, and services. Enables agents to retrieve logs, inspect pod specifications, and check container statuses to support monitoring and root cause analysis use cases.
Provides mock tools that can simulate interactions with a Kubernetes cluster running OpenTelemetry Demo, useful for testing agents against realistic but static data without requiring a live cluster.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Kubernetes Tools MCP Server show me the status of pods in the default namespace"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Kubernetes Tools
k8stools gives AI agents read-only access to a Kubernetes cluster. It provides 33 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
What changed in each release is in the changelog.
Related MCP server: kubeview-mcp
Quick start
1. Install
k8stools needs Python 3.12 or newer.
pip install k8stools # or: uv add k8stoolsThis installs three commands:
Command | What it does |
| The MCP server |
| Snapshots a cluster to a file that the server can replay |
| 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 --mockThen 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-clusterThe 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 33 tools: it serves a named toolset, chosen
with --toolset:
Toolset | What it contains | Use it when |
| 5 tools: | 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. |
| 29 tools: every tool except the four that another tool already covers | The agent needs to dig into details: specs, history, logs, networking, storage. |
| All 33 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 |
|
|
|
|
|
|
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_configmapA 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 |
| 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), pods that can't be scheduled and a Deployment's failing rollout conditions, 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. | ✓ | ✓ |
| 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 of its most troubled pod, short ones of the others ( | ✓ | ✓ |
| 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 |
| The namespaces, like | ✓ | |
| The nodes, like | ✓ | ✓ |
| Each node's current CPU and memory use against its allocatable, like | ✓ | |
| Cluster- or namespace-wide events, filtered on the server (by object, reason, type) | ✓ | ✓ |
Pods, containers and logs
Tool | What it answers | T | I |
| The pods, like | ✓ | |
| Each container's state in a pod: running, waiting or terminated, restarts, last termination. Init containers are listed separately ( | ✓ | |
| A pod's full spec | ✓ | |
| The events for one pod | ||
| A container's logs, with | ✓ | |
| When a pattern occurs across a container's whole retained log, not just a tail: the count, first and last match, matches per time bucket (so an onset or a gap shows), a few matching lines, and how far back the log goes. A small result whatever the log's size. | ✓ | |
| Each running container's current CPU and memory use beside its requests and limits, like | ✓ |
Workloads
Tool | What it answers | T | I |
| Deployments, like | ✓ | |
| StatefulSets, like | ✓ | |
| DaemonSets, like | ✓ | |
| 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. | ✓ | |
| A Deployment's ReplicaSets with their revision numbers and images | ||
| HorizontalPodAutoscalers, like | ✓ | |
| CronJobs, like | ✓ | |
| Jobs, like | ✓ | |
| Logs from a Job's most recent pod | ||
| Logs from a CronJob's most recent run |
Networking
Tool | What it answers | T | I |
| Services, like | ✓ | |
| Each Service's backends, from its EndpointSlices: every address's ready / serving / terminating state, pod and node | ✓ | |
| Ingresses, like | ✓ | |
| NetworkPolicies: the pods each selects, its rules, and what it allows per direction in plain words ("allows all ingress", "denies all egress", "allows ingress only from ..."), by the API's rules rather than the policy's name | ✓ |
Configuration and storage
Tool | What it answers | T | I |
| ConfigMaps, like | ✓ | |
| The full contents of one ConfigMap (redacted by the server) | ✓ | |
| PersistentVolumeClaims, like | ✓ |
Custom resources
Tool | What it answers | T | I |
| The custom kinds the cluster serves (CRDs): group, kind, plural, scope, versions | ✓ | |
| 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 triageVS 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 |
|
Which cluster to read |
|
Serve a snapshot instead of a cluster |
|
Secret redaction |
|
Transport |
|
Logging |
|
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 accessusage: 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 file, when |
| Kubeconfig context, when |
| Turns redaction off, like |
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 |
|
|
2nd |
|
|
3rd |
| the file's |
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 stagingPermissions
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, networkpolicies]
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.
To answer from something that isn't a cluster (a converter serving a recorded
snapshot, say), bind an ApiClient whose rest_client answers the requests, and
pin the clock to the snapshot's time. Both apply to every tool and to
capture_state, and are restored at the end of the block:
with k8s_tools.use_api_client(api, context="snapshot-33"), \
k8s_tools.pinned_clock(snapshot_time):
state = capture_state()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 abovemock_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
checkoutrollout that can't finish: its new pod can't be scheduled (a node selector no node matches), while the old pod still serves;a Deployment whose HPA is at its maximum;
a new
quoteDeployment whose pod can't be scheduled, because the PersistentVolumeClaim it mounts can't bind: its storage class doesn't exist;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.jsonIt 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]
[--from-itbench DIR] [--at PULL] [--list-pulls]
[--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, up to 16 MiB of
each [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.
--from-itbench DIR Instead of capturing, import an ITBench-Lite snapshot
(a scenario directory with k8s_objects_raw.tsv) as of
one of its pulls. What it doesn't record replays as
"not recorded".
--at PULL With --from-itbench: the pull to import, numbered from
1 [default: the last]. --list-pulls shows them.
--list-pulls With --from-itbench: list the snapshot's pulls and
exit
--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 |
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 ( |
ConfigMaps | summary and full contents, so |
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, but
a kind its role may not read (a 403) doesn't fail it: the kind is recorded as not
readable, per namespace, and the capture carries on, so a role that predates a
kind added in a newer k8stools still captures everything else. The summary at
the end lists what wasn't readable and what to grant. Pods are required, and so
is the namespace list unless --namespace is given. A custom-resource type it
can't read, metrics it may not read, and a cluster without metrics-server are
recorded the same way.
--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. A log is kept to its newest 16 MiB,
which can leave fewer lines than --max-log-lines (the end-of-capture report
lists those logs); the tools' 1 MiB limit on a result doesn't apply. The capture
records where each log began and which limit cut it, so on replay
get_log_pattern_summary and the log tool say when the capture kept only the
newest lines, and why. 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. What a capture didn't record replays as not recorded, never as none, whether the capture predates it or didn't collect it:
A kind the capture has no list of (an older capture has no
endpoints, say) makes its tool raiseK8sNotRecorded, an error saying the kind is unknown rather than absent.get_namespace_healthandget_workload_reportname such kinds in one note instead of failing.A container log that wasn't recorded (unreadable when captured, or
--no-logs) raisesK8sNotRecordedtoo, and so does a previous-instance log of a container that restarted, when its previous log wasn't captured. A container that never restarted has no previous instance, and says so, as live. A log recorded as empty is replayed as empty.A kind the capture's role couldn't read raises
K8sNotRecordedwith the reason, in the namespaces it couldn't read and across all of them; the other namespaces answer as recorded. Such a capture is format version "2", which k8stools before 4.5.0 refuses rather than read those kinds as empty; a complete capture stays version "1".A field the capture predates is unknown (
None, never 0).
Importing an ITBench-Lite snapshot
An ITBench-Lite scenario
directory holds what an OpenTelemetry collector exported during the incident:
Kubernetes objects pulled every five minutes (k8s_objects_raw.tsv), the event
stream (k8s_events_raw.tsv) and the services' OTel log records
(otel_logs_raw.tsv). --from-itbench turns one pull of it into a capture:
k8s-capture-state --from-itbench Scenario-33 --list-pulls # numbered, with their kinds
k8s-capture-state --from-itbench Scenario-33 --at 7 -o s33.json # default: the last pull
k8s-mcp-server --state-file s33.json --state-time frozenThe importer serves the pull to the real tools, with the clock set to the pull's time, and runs the same capture code against it, so an imported capture replays under the same rules as any other. What the snapshot doesn't hold replays as not recorded:
Kinds the pull has no batch of. A pull writes nothing for a kind with no objects, so a missing kind may be none or a lost batch;
--list-pullsand the import's report name them. HPAs, CronJobs and ControllerRevisions are never pulled, so StatefulSet and DaemonSet revisions are unknown (Deployment history comes from the ReplicaSets).Logs are OpenTelemetry records, not container output. Every replayed log says so in a note: a stack trace a process printed to stderr isn't in them. Records carry no container name: a record goes to its pod's only container, or the container named like its service, or none (counted). Only the current instance's records are kept (those since it started). A container with no records at all is not recorded, and previous-instance logs never are.
Metrics and custom resource instances were never collected, and say so.
Events are those that existed at the pull (each recorded twice in the snapshot, kept once). Endpoints are the core
Endpointsobjects, served as EndpointSlices.
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.gzFor 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 | |
| 404 KB | +61 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 frozenwhich 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.jsonCaptures 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? |
| Yes, on by default; |
| Yes: the capture applies the pass itself before writing the file |
| No. Wrap each function: |
Calling a tool function directly in Python | No. Apply the pass to the result: |
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
# Integration tests that write to the cluster (skipped unless asked)
K8STOOLS_LIVE_WRITE=1 KUBECONFIG=~/.kube/your-config.yaml uv run pytest tests/test_live_writes.py -vThe integration tests above only read what the cluster has. The ones in
tests/test_live_writes.py create the failures they check, such as a claim on a
storage class that doesn't exist or a rollout whose new pod can't be scheduled,
and compare the tools' output with the cluster's own messages. Each test creates
a k8stools-test-<random> namespace and deletes it afterwards, so it needs
permission to create and delete namespaces, Deployments and
PersistentVolumeClaims. The tests pull registry.k8s.io/pause. Use a test
cluster such as Minikube or kind. They take about 30 seconds. If a run is
interrupted, remove what it left with
kubectl get ns -o name | grep k8stools-test- | xargs -r kubectl delete.
Planned and deferred work is in docs/ROADMAP.md.
Available Tools
21 toolsget_cluster_infoA
Report which Kubernetes cluster these tools are bound to. Call this first when more than one cluster could be involved, and before drawing conclusions that depend on which cluster the data came from.
The binding is made once (at server startup, or on the first tool call) and
shared by every tool, so all tools answer from this cluster.
Parameters
----------
None
This function does not take any parameters.
Returns
-------
ClusterInfo
An object with the following fields:
source : str
"kubeconfig" (bound through a kubeconfig context), "in-cluster" (the
service account of the pod the server runs in), or "capture" (a
recorded snapshot of a cluster being replayed, not a live cluster).
context : Optional[str]
The kubeconfig context name. None for in-cluster config, and for a
capture that did not record one.
server : Optional[str]
URL of the cluster's API server.
kubeconfig : Optional[str]
Path of the kubeconfig file (or ``:``-separated list of files) the
binding was read from. None unless source is "kubeconfig".
server_version : Optional[str]
Kubernetes version reported by the API server, e.g. "v1.31.2". None if
the server could not be reached, or the capture did not record it.
captured_at : Optional[datetime.datetime]
For a capture, when it was taken; every age and timestamp the tools
return is relative to that moment. None for a live cluster.
Raises
------
K8sConfigError
If no cluster configuration can be loaded.
Inputs: no named parameters. Returns: Which cluster these tools are answering from.
get_configmapA
Retrieves the full contents of a single ConfigMap.
WARNING: ConfigMaps can contain secret-shaped values (e.g. credentials stored
as plain config). When this tool is served through the k8stools MCP server, the
output passes through a redaction step by default (see the `redaction` module
and the server's `--no-redact` flag). Callers using this function directly get
the raw values and are responsible for their own redaction.
Parameters
----------
name : str
Name of the ConfigMap.
namespace : str, optional
Namespace of the ConfigMap (default is "default").
Returns
-------
dict[str, Any]
A dictionary describing the ConfigMap with the following keys:
name : str
Name of the ConfigMap.
namespace : str
Namespace of the ConfigMap.
data : dict[str, str]
The string key/value data map (empty dict if none).
binary_data_keys : list[str]
Names of any binary keys. The binary values themselves are not
returned.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the ConfigMap is not found or the API call fails.
Inputs: name (required), namespace. Returns: object
get_configmap_summariesA
Retrieves a list of ConfigMapSummary objects for ConfigMaps in a given namespace
or all namespaces, similar to kubectl get configmaps.
Note that this returns only summary metadata (not the ConfigMap contents); use
`get_configmap` to read the actual data map.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list ConfigMaps from. If None, lists ConfigMaps from
all namespaces.
Returns
-------
list of ConfigMapSummary
A list of ConfigMapSummary objects, each with the following fields:
name : str
Name of the ConfigMap.
namespace : str
Namespace in which the ConfigMap lives.
key_count : int
Number of keys across `data` and `binary_data`.
data_size : int
Approximate total size in bytes of all values.
age : datetime.timedelta
Age of the ConfigMap (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list ConfigMaps fails.
Inputs: namespace. Returns: result
get_cronjob_summariesA
Retrieves a list of CronJobSummary objects for CronJobs in a given namespace
or all namespaces, similar to kubectl get cronjobs.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list CronJobs from. If None, lists from all
namespaces.
Returns
-------
list of CronJobSummary
A list of CronJobSummary objects, each with the following fields:
name : str
Name of the CronJob.
namespace : str
Namespace of the CronJob.
schedule : str
Cron schedule expression.
suspend : bool
Whether the CronJob is suspended.
active : int
Number of currently active (running) jobs.
last_schedule_time : Optional[datetime.timedelta]
Time since the CronJob was last scheduled (None if never).
last_successful_time : Optional[datetime.timedelta]
Time since the CronJob last completed successfully (None if never).
age : datetime.timedelta
Age of the CronJob (current time minus creation timestamp).
containers : list[ContainerTemplateSummary]
The job pod template's containers, each with name, image, and literal
environment values.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list CronJobs fails.
Inputs: namespace. Returns: result
get_daemonset_summariesA
Retrieves a list of DaemonSetSummary objects for DaemonSets in a given
namespace or all namespaces, similar to kubectl get daemonsets -o wide.
A DaemonSet runs one pod on each node it targets; its pods are named
"<daemonset>-<5 characters>" and have `owner` "DaemonSet/<daemonset>" in
`get_pod_summaries`. A shortfall between the counts below ("desired 5,
ready 4") is often the first sign of a problem with a particular node.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list DaemonSets from. If None, lists from all
namespaces.
Returns
-------
list of DaemonSetSummary
A list of DaemonSetSummary objects, each with the following fields:
name : str
Name of the DaemonSet.
namespace : str
Namespace in which the DaemonSet runs.
desired_number_scheduled : int
Number of nodes that should be running the DaemonSet's pod - the
actual number of nodes it targets.
current_number_scheduled : int
Number of nodes running at least one of its pods that should.
number_ready : int
Number of nodes whose pod is ready.
updated_number_scheduled : int
Number of nodes running a pod from the current pod template. Less
than desired means a rollout is in progress or stuck.
number_available : int
Number of nodes whose pod has been ready for at least minReadySeconds.
number_misscheduled : int
Number of nodes running its pod that should not be.
node_selector : dict[str, str]
The pod template's nodeSelector (kubectl's NODE SELECTOR column).
Many DaemonSets choose their nodes with node affinity and
tolerations instead, which this does not show, so an empty selector
does not mean "every node"; desired_number_scheduled is the count.
update_strategy : str
Update strategy type ("RollingUpdate" or "OnDelete").
images : list[str]
Container images of the current pod template, in container order. A
pod running a different image is from an earlier revision.
age : datetime.timedelta
Age of the DaemonSet (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list DaemonSets fails.
Inputs: namespace. Returns: result
get_deployment_summariesA
Retrieves a list of DeploymentSummary objects for deployments in a given namespace or all namespaces.
Similar to `kubectl get deployements`.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list deployments from. If None, lists deployments from all namespaces.
Returns
-------
list of DeploymentSummary
A list of DeploymentSummary objects, each providing a summary of a deployment's status with the following fields:
name : str
Name of the deployment.
namespace : str
Namespace in which the deployment is running.
total_replicas : int
Total number of replicas desired for this deployment.
ready_replicas : int
Number of replicas that are currently ready.
up_to_date_replicas : int
Number of replicas that are up to date.
available_replicas : int
Number of replicas that are available.
age : datetime.timedelta
Age of the deployment (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list deployments fails.
Inputs: namespace. Returns: result
get_eventsA
Lists cluster- or namespace-wide events with optional server-side filtering.
Unlike `get_pod_events` (which is scoped to a single named pod), this supports
the sweep queries common in capacity/storage runbooks — e.g. all "Evicted"
events, or all "FailedScheduling" events — where the affected pods often have no
stable name to look up. Note Kubernetes expires an event record about an hour
(by default) after its last occurrence, so events that stopped repeating
earlier than that are gone.
Parameters
----------
namespace : Optional[str], default=None
Namespace to list events from. If None, lists across all namespaces.
reason : Optional[str], default=None
If set, only return events with this reason (e.g. "Evicted",
"FailedScheduling", "BackOff").
involved_kind : Optional[str], default=None
If set, only return events whose involved object is of this kind (e.g.
"Pod", "Node", "PersistentVolumeClaim").
involved_name : Optional[str], default=None
If set, only return events whose involved object has this name.
event_type : Optional[str], default=None
If set, only return events of this type ("Normal" or "Warning").
Returns
-------
list of EventSummary
Matching events. Each EventSummary has the following fields:
last_seen : Optional[datetime.timedelta]
Time since the event was last seen (if available).
first_seen : Optional[datetime.timedelta]
Time since the first occurrence combined into this record (if
available).
count : Optional[int]
How many occurrences Kubernetes combined into this record: repeats
of the same event on the same object are counted in one record
rather than listed separately. The count covers first_seen to
last_seen, not the object's lifetime - a record that stops repeating
expires (after 1h by default), and a later repeat starts a new one.
For a crash-looping container, count the "Created" or "Started"
events to get restarts. "BackOff" is emitted repeatedly while the
kubelet waits to restart, so its count is several times the number
of restarts. count divided by (first_seen - last_seen) is the
average rate over that window, not the current back-off.
A record can lag behind what it counts. By default the kubelet
writes at most one event update per object and event type
("Normal" or "Warning") every 5 minutes, once a burst of 25 is
used up; occurrences in between are counted but only written with
the next update, which then jumps by several at once. So count and
last_seen describe the most recent *written* occurrence and can
trail reality by several occurrences and tens of minutes. They lag
together, so the rate above still holds, but last_seen is not the
time of the last restart: for that, use the container status
(last_state.finished_at, state.started_at from
get_pod_container_statuses) or PodSummary.last_restart, which come
from the kubelet's status rather than from events. "Pulled",
"Created" and "Started" share one write budget, so their counts for
the same restarts can differ by a few; don't compare counts across
reasons.
type : str
Type of the event ("Normal" or "Warning").
reason : str
Reason for the event.
object : str
The involved object as "Kind/name" (or just the name when the kind is
unavailable).
message : str
Message describing the event.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list events fails.
Inputs: reason, namespace, event_type, involved_kind, +1 more. Returns: result
get_job_summariesA
Retrieves a list of JobSummary objects for Jobs in a given namespace or all
namespaces, similar to kubectl get jobs.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list Jobs from. If None, lists from all namespaces.
Returns
-------
list of JobSummary
A list of JobSummary objects, each with the following fields:
name : str
Name of the Job.
namespace : str
Namespace of the Job.
owner : Optional[str]
Name of the owning CronJob, if this Job was created by one.
active : int
Number of actively running pods.
succeeded : int
Number of pods that completed successfully.
failed : int
Number of pods that terminated in failure.
start_time : Optional[datetime.timedelta]
Time since the Job started (None if not started).
completion_time : Optional[datetime.timedelta]
Time since the Job completed (None if not complete).
conditions : list[str]
Status condition types currently True (e.g. ["Complete"], ["Failed"]).
age : datetime.timedelta
Age of the Job (current time minus creation timestamp).
containers : list[ContainerTemplateSummary]
The Job pod template's containers, each with name, image, and literal
environment values.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list Jobs fails.
Inputs: namespace. Returns: result
get_logs_for_cronjobA
Retrieves logs from the most-recent run of a CronJob.
Finds the newest Job owned by the CronJob, then returns the logs of that Job's
most-recently-created pod. Removes the need to manually locate the right
short-lived pod for a CronJob's last tick.
Parameters
----------
cronjob_name : str
Name of the CronJob.
namespace : str, optional
Namespace of the CronJob (default is "default").
container_name : str, optional
Container within the pod. If None, defaults to the first container.
tail : int, optional
Number of lines from the end of the log (default: last 1000).
since_seconds : int, optional
If set, only return logs newer than this many seconds.
previous : bool, default False
If True, return logs from the previous terminated container instance.
See `get_logs_for_pod_and_container` for the cases - CrashLoopBackOff, a
restart racing the call, a reclaimed log file - where that legitimately
returns the same text as `previous=False`, or a 200 whose body is an
error message.
Returns
-------
str, optional
Log content, or None if the CronJob has no jobs/pods yet.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call fails.
Inputs: cronjob_name (required), tail, previous, namespace, +2 more. Returns: result
get_logs_for_jobA
Retrieves logs from the most-recently-created pod of a Job.
Job pods are short-lived, so this convenience finds the newest pod belonging to
the Job (via its `job-name` label) and returns its logs, saving the caller from
listing pods and sorting by creation time.
Parameters
----------
job_name : str
Name of the Job.
namespace : str, optional
Namespace of the Job (default is "default").
container_name : str, optional
Container within the pod. If None, defaults to the first container.
tail : int, optional
Number of lines from the end of the log (default: last 1000).
since_seconds : int, optional
If set, only return logs newer than this many seconds.
previous : bool, default False
If True, return logs from the previous terminated container instance.
See `get_logs_for_pod_and_container` for the cases - CrashLoopBackOff, a
restart racing the call, a reclaimed log file - where that legitimately
returns the same text as `previous=False`, or a 200 whose body is an
error message.
Returns
-------
str, optional
Log content, or None if the Job has no pods.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call fails.
Inputs: job_name (required), tail, previous, namespace, +2 more. Returns: result
get_logs_for_pod_and_containerA
Retrieves logs from a Kubernetes pod and container.
Args:
pod_name (str): The name of the pod.
namespace (str): The namespace of the pod.
container_name (str, optional): The name of the container within the pod.
If None, defaults to the first container.
tail (int, optional): Number of lines to return from the end of the log.
If None, defaults to the last 1000 lines. Pass a
larger value to retrieve more history, or a small
value for a bounded recent slice.
since_seconds (int, optional): If set, only return logs newer than this
many seconds. Combines with `tail` (both limits
apply).
previous (bool, default False): If True, return logs from the *previous*
terminated instance of the container instead of the
current one. Indispensable for crashloop analysis,
where the current instance's logs are empty or
post-restart. Fails if there is no previous instance.
Three Kubernetes behaviors make `previous=True`
look as though it were ignored or broken; none is a
fault in this tool, and all are worth knowing before
drawing a conclusion from the output:
* While a container sits in CrashLoopBackOff there
is no running instance, so the kubelet serves the
most recently *terminated* one for `previous=False`
as well - both calls then return the same text.
Check the container's state with
`get_pod_container_statuses` before concluding the
flag had no effect.
* 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
legitimately return identical bytes for both.
Compare the log timestamps, not just the flag.
* Once the previous instance's log file has been
reclaimed, the API answers 200 with the text
`unable to retrieve container logs for <id>` -
a successful call whose body is an error message,
not a raised error.
Only the single most recent terminated instance is
retained by the kubelet; there is no way to reach
further back than one.
Returns:
str: The log content as text, with real newlines. An empty string if the
container has produced no log output (never None).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to fetch logs fails or an unexpected error occurs.
Inputs: pod_name (required), tail, previous, namespace, +2 more. Returns: result
get_namespacesA
Return a summary of the namespaces for this Kubernetes cluster, similar to that
returned by kubectl get namespace.
Parameters
----------
None
This function does not take any parameters.
Returns
-------
list of NamespaceSummary
List of namespace summary objects. Each NamespaceSummary has the following fields:
name : str
Name of the namespace.
status : str
Status phase of the namespace.
age : datetime.timedelta
Age of the namespace (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list namespaces fails.
Inputs: no named parameters. Returns: result
get_node_summariesA
Return a summary of the nodes for this Kubernetes cluster, similar to that
returned by kubectl get nodes -o wide.
Parameters
----------
None
This function does not take any parameters.
Returns
-------
list of NodeSummary
List of node summary objects. Each NodeSummary has the following fields:
name : str
Name of the node.
status : str
Status of the node (Ready, NotReady, etc.).
roles : list[str]
List of roles for the node (e.g., ['control-plane', 'master']).
age : datetime.timedelta
Age of the node (current time minus creation timestamp).
version : str
Kubernetes version running on the node.
internal_ip : Optional[str]
Internal IP address of the node.
external_ip : Optional[str]
External IP address of the node (if available).
os_image : Optional[str]
Operating system image running on the node.
kernel_version : Optional[str]
Kernel version of the node.
container_runtime : Optional[str]
Container runtime version on the node.
capacity : dict[str, str]
Total capacity of the node keyed by resource name (e.g. "cpu",
"memory", "ephemeral-storage", "pods"). Empty if unavailable.
allocatable : dict[str, str]
Resources allocatable to pods (capacity minus system-reserved),
keyed by resource name. Empty if unavailable.
conditions : dict[str, str]
Node conditions keyed by type with their status, e.g.
{"Ready": "True", "MemoryPressure": "False", "DiskPressure": "False"}.
conditions_since : dict[str, datetime.timedelta]
For each condition in `conditions`, the time since its status last
changed (the API's lastTransitionTime), e.g. how long the node has
been Ready, or how long ago a MemoryPressure episode ended. A
condition with no transition time is left out.
It changes only when the status does. A node or kubelet restart in
which the node comes back Ready without having been marked NotReady
or Unknown in between does not reset it: on a minikube cluster
stopped and started together with its control plane, Ready stayed
"True since the node was created" across a reboot. So a long Ready
age does not mean the node or its pods have been running that long,
and a short one can come from a brief NotReady flap rather than a
reboot. To date a node's last start, use its "Starting" or
"Rebooted" events (`get_events(involved_kind="Node")`, while they
last - events usually expire after an hour), or the `started_at` of
containers that start with the node, such as kube-proxy and the
kube-system control-plane containers (`get_pod_container_statuses`).
taints : list[str]
Taints on the node formatted as "key=value:effect" (value omitted
when empty).
labels : dict[str, str]
All labels on the node. Useful for identifying node pool / instance
type and spotting version skew across pools.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list nodes fails.
Inputs: no named parameters. Returns: result
get_pod_container_statusesA
Get the status for all containers in a specified Kubernetes pod.
Parameters
----------
pod_name : str
Name of the pod to retrieve container statuses for.
namespace : str, optional
Namespace of the pod (default is "default").
Returns
-------
list of ContainerStatus
List of container status objects for the specified pod. Each ContainerStatus has the following fields:
pod_name : str
Name of the pod.
namespace : str
Namespace of the pod.
container_name : str
Name of the container.
image : str
Image name.
ready : bool
Whether the container is currently passing its readiness check.
The value will change as readiness probes keep executing.
restart_count : int
Number of times the container has restarted.
started : Optional[bool]
Started indicates whether the container has finished its postStart
lifecycle hook and passed its startup probe.
stop_signal : Optional[str]
Stop signal for the container.
state : Optional[ContainerState]
Current state of the container.
last_state : Optional[ContainerState]
Last state of the container. When Terminated, ``ran_for`` is how long
that instance ran (``finished_at - started_at``). It is not the
restart interval, which adds the back-off named in a Waiting
``state``'s message, and it is not how much time the container's log
covers: a process can stop logging long before it is killed.
volume_mounts : list[VolumeMountStatus]
Status of volume mounts for the container
resource_requests : dict[str, str]
Describes the minimum amount of compute resources required. If Requests
is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
resource_limits : dict[str, str]
Describes the maximum amount of compute resources allowed.
allocated_resources : dict[str, str]
Compute resources allocated for this container by the node.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to read the pod fails.
Inputs: pod_name (required), namespace. Returns: result
get_pod_eventsA
Get events for a specific Kubernetes pod. This is equivalent to the kubectl command:
`kubectl get events -n NAMESPACE --field-selector involvedObject.name=POD_NAME,involvedObject.kind=Pod`
Parameters
----------
pod_name : str
Name of the pod to retrieve events for.
namespace : str, optional
Namespace of the pod (default is "default").
Returns
-------
list of EventSummary
List of events associated with the specified pod. Each EventSummary has the following fields:
last_seen : Optional[datetime.timedelta]
Time since the most recent occurrence of the event (if available).
first_seen : Optional[datetime.timedelta]
Time since the first occurrence combined into this record (if
available).
count : Optional[int]
How many occurrences Kubernetes combined into this record: repeats
of the same event on the same object are counted in one record
rather than listed separately. The count covers first_seen to
last_seen, not the object's lifetime - a record that stops repeating
expires (after 1h by default), and a later repeat starts a new one.
For a crash-looping container, count the "Created" or "Started"
events to get restarts. "BackOff" is emitted repeatedly while the
kubelet waits to restart, so its count is several times the number
of restarts. count divided by (first_seen - last_seen) is the
average rate over that window, not the current back-off.
A record can lag behind what it counts. By default the kubelet
writes at most one event update per object and event type
("Normal" or "Warning") every 5 minutes, once a burst of 25 is
used up; occurrences in between are counted but only written with
the next update, which then jumps by several at once. So count and
last_seen describe the most recent *written* occurrence and can
trail reality by several occurrences and tens of minutes. They lag
together, so the rate above still holds, but last_seen is not the
time of the last restart: for that, use the container status
(last_state.finished_at, state.started_at from
get_pod_container_statuses) or PodSummary.last_restart, which come
from the kubelet's status rather than from events. "Pulled",
"Created" and "Started" share one write budget, so their counts for
the same restarts can differ by a few; don't compare counts across
reasons.
type : str
Type of the event.
reason : str
Reason for the event.
object : str
The object this event applies to.
message : str
Message describing the event.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list events fails.
Inputs: pod_name (required), namespace. Returns: result
get_pod_specA
Retrieves the spec for a given pod in a specific namespace.
Args:
pod_name (str): The name of the pod.
namespace (str): The namespace the pod belongs to (defaults to "default").
Returns
-------
dict[str, Any]
The pod's spec object, containing its desired state. It is converted
from a V1PodSpec to a dictionary. Key fields include:
containers : list of kubernetes.client.V1Container
List of containers belonging to the pod. Each container defines its image,
ports, environment variables, resource requests/limits, etc.
init_containers : list of kubernetes.client.V1Container, optional
List of initialization containers belonging to the pod.
volumes : list of kubernetes.client.V1Volume, optional
List of volumes mounted in the pod and the sources available for
the containers.
node_selector : dict, optional
A selector which must be true for the pod to fit on a node.
Keys and values are strings.
restart_policy : str
Restart policy for all containers within the pod.
Common values are "Always", "OnFailure", "Never".
service_account_name : str, optional
Service account name in the namespace that the pod will use to
access the Kubernetes API.
dns_policy : str
DNS policy for the pod. Common values are "ClusterFirst", "Default".
priority_class_name : str, optional
If specified, indicates the pod's priority_class via its name.
node_name : str, optional
NodeName is a request to schedule this pod onto a specific node.
Raises
------
K8SConfigError
If unable to initialize the K8S API
K8sApiError
If the pod is not found, configuration fails, or any other API error occurs.
Inputs: pod_name (required), namespace. Returns: object
get_pod_summariesA
Retrieves a list of PodSummary objects for pods in a given namespace or all namespaces.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list pods from. If None, lists pods from all namespaces.
Returns
-------
list of PodSummary
A list of PodSummary objects, each providing a summary of a pod's status with the following fields:
name : str
Name of the pod.
namespace : str
Namespace in which the pod is running.
total_containers : int
Total number of containers in the pod.
ready_containers : int
Number of containers currently in ready state.
restarts : int
Total number of restarts for all containers in the pod.
last_restart : Optional[datetime.timedelta]
Time since a container of this pod last terminated - the most
recent last_state.finished_at across its containers - or None if
none has. For a container waiting in CrashLoopBackOff this is its
last crash, not a restart: it has not been started again yet. Taken
from the kubelet's status, so it is current, unlike event records.
age : datetime.timedelta
Age of the pod (current time minus creation timestamp).
ip : Optional[str]
Pod IP address (None if not assigned).
node : Optional[str]
Name of the node where the pod is running (None if not scheduled).
owner : Optional[str]
The pod's controlling owner, as "Kind/name" - the object that
created and manages it, e.g. "DaemonSet/otel-collector-agent",
"StatefulSet/valkey-cart", "Job/backup-29318400". None for a bare
pod nobody manages. This is the direct owner only: a Deployment's
pods are owned by one of its replica sets ("ReplicaSet/ad-7d9f8c6b5"),
whose `owner_deployment` (`get_replicaset_summaries`) names the
Deployment; likewise a Job's `owner` names its CronJob. Static
(mirror) pods such as the control plane's report "Node/<node name>".
Group pods into workloads by this field rather than by stripping
name suffixes.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list pods fails.
Inputs: namespace. Returns: result
get_pvc_summariesA
Retrieves a list of PVCSummary objects for PersistentVolumeClaims in a given
namespace or all namespaces, similar to kubectl get pvc.
For each PVC, this also resolves which pods currently mount it (by scanning pod
volumes in the same scope), which is useful for spotting orphaned PVCs — claims
with an empty `mounted_by` and no owning pod.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list PVCs from. If None, lists from all namespaces.
Returns
-------
list of PVCSummary
A list of PVCSummary objects, each with the following fields:
name : str
Name of the PVC.
namespace : str
Namespace of the PVC.
status : str
Phase of the PVC ("Bound", "Pending", or "Lost").
volume_name : Optional[str]
Name of the bound PersistentVolume (None if unbound).
capacity : Optional[str]
Storage capacity (e.g. "10Gi"); falls back to the requested size when
the claim is not yet bound.
access_modes : list[str]
Access modes (e.g. ["ReadWriteOnce"]).
storage_class : Optional[str]
StorageClass backing the claim.
mounted_by : list[str]
Names of pods (in the same scope) currently mounting this PVC. Empty
for orphaned/unmounted claims.
age : datetime.timedelta
Age of the PVC (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list PVCs fails.
Inputs: namespace. Returns: result
get_replicaset_summariesA
Retrieves a list of ReplicaSetSummary objects, similar to `kubectl get replicasets`
but including each replica set's deployment revision and container images.
A Deployment's replica sets are its revision history: every update to a Deployment
creates a new replica set carrying that revision's pod template, and older replica
sets are retained (scaled to zero). Listing them for one deployment therefore shows
when it last changed and what its image was at each revision - which is how you
answer "did something change recently?" without access to deployment tooling or
version control.
Results are grouped by namespace and owning deployment, and within each
deployment sorted by revision, oldest first. So when filtered to a single
deployment, the last entry is that deployment's current revision.
Replica sets with no revision (see `revision` below) sort *first*, ahead of
revision 1, rather than being dropped. They have no place in any deployment's
history, so they are listed before it rather than appended to it. In practice
they are only visible in an unfiltered listing: a replica set without a
revision has no owning deployment either, so passing `deployment` filters them
out, and the "last entry is the current revision" guarantee above is unaffected.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list replica sets from. If None, lists from all namespaces.
deployment : Optional[str], default=None
If given, return only replica sets owned by this deployment. This is the common
case: one deployment's revision history.
Returns
-------
list of ReplicaSetSummary
A list of ReplicaSetSummary objects with the following fields:
name : str
Name of the replica set.
namespace : str
Namespace in which the replica set is defined.
owner_deployment : Optional[str]
Name of the Deployment that owns this replica set, or None if it is
standalone (not managed by a Deployment).
revision : Optional[int]
The deployment revision this replica set represents, taken from the
`deployment.kubernetes.io/revision` annotation. None when that
annotation is absent, which means no Deployment created this replica
set - only the Deployment controller writes it. A hand-written replica
set, or one created by another controller, therefore has no revision
(and no `owner_deployment`), and appears in neither `kubectl rollout
history` nor a `deployment`-filtered call here. Also None if the
annotation is present but not an integer.
desired_replicas : int
Replicas desired for this replica set. Old revisions are scaled to 0.
current_replicas : int
Replicas currently running.
ready_replicas : int
Replicas currently ready.
images : list[str]
Container images in this revision's pod template, in container order.
Comparing this across revisions shows what an upgrade changed.
age : datetime.timedelta
Age of the replica set (current time minus creation timestamp). For the
newest revision this is how long ago the deployment last changed.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list replica sets fails.
Inputs: namespace, deployment. Returns: result
get_service_summariesA
Retrieves a list of ServiceSummary objects for services in a given namespace or all namespaces.
Similar to kubectl get services.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list services from. If None, lists services from all namespaces.
Returns
-------
list of ServiceSummary
A list of ServiceSummary objects, each providing a summary of a service's status with the following fields:
name : str
Name of the service.
namespace : str
Namespace in which the service is running.
type : str
Type of the service (ClusterIP, NodePort, LoadBalancer, ExternalName).
cluster_ip : Optional[str]
Cluster IP address assigned to the service (None for ExternalName services).
external_ip : Optional[str]
External IP address if applicable (for LoadBalancer services).
ports : list[PortInfo]
List of ports (and their protocols) exposed by the service.
age : datetime.timedelta
Age of the service (current time minus creation timestamp).
selector : dict[str, str]
The label selector the service uses to choose backing pods. Empty
for services without a selector (e.g. ExternalName, or manually
managed Endpoints).
labels : dict[str, str]
Labels on the service object.
annotations : dict[str, str]
Annotations on the service object.
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list services fails.
Inputs: namespace. Returns: result
get_statefulset_summariesA
Retrieves a list of StatefulSetSummary objects for StatefulSets in a given
namespace or all namespaces, similar to kubectl get statefulsets.
Parameters
----------
namespace : Optional[str], default=None
The specific namespace to list StatefulSets from. If None, lists from all
namespaces.
Returns
-------
list of StatefulSetSummary
A list of StatefulSetSummary objects, each with the following fields:
name : str
Name of the StatefulSet.
namespace : str
Namespace in which the StatefulSet runs.
total_replicas : int
Desired number of replicas.
ready_replicas : int
Number of replicas currently ready.
current_replicas : int
Number of replicas created by the current revision.
update_strategy : str
Update strategy type (e.g. "RollingUpdate", "OnDelete").
service_name : Optional[str]
Name of the governing (headless) service.
age : datetime.timedelta
Age of the StatefulSet (current time minus creation timestamp).
Raises
------
K8sConfigError
If unable to initialize the K8S API.
K8sApiError
If the API call to list StatefulSets fails.
Inputs: namespace. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 21 tools
Most tools target clearly distinct Kubernetes resources and levels (nodes, pods, deployments, services, logs, etc.), and the descriptions explicitly distinguish similar tools. However, get_pod_events overlaps with get_events, which can also filter by involved_kind and involved_name, creating a minor selection ambiguity.
All tool names use snake_case with a consistent get_ prefix and resource-oriented nouns. The patterns get_<resource>_summaries, get_<resource>, and get_logs_for_<entity> are predictable and easy to scan.
With 21 tools, the set is on the heavy side for a read-only inspection server. Many resource summaries are justified, but the surface could be consolidated (e.g. pod-specific events and generalized events, or separate job/cronjob log helpers).
Core workload inspection is well covered: pods, containers, logs, events, deployments, replica sets, stateful sets, daemon sets, jobs, cronjobs, services, configmaps, PVCs, nodes, and namespaces. Notable gaps remain for common Kubernetes resources such as ingresses, endpoints, secrets, storage classes, persistent volumes, HPA, and RBAC objects.
Maintenance
Related MCP Connectors
The Cortex MCP server provides read-only access to real-time engineering context from the Cortex developer portal, allowing AI coding assistants to answer natural language questions about your organization's catalog (microservices, libraries, domains, teams, infrastructure), scorecards (engineering standards and best practices), initiatives (goals and deadlines), and Engineering Intelligence metrics. It includes tools for querying documentation, tracking personal entities, and accessing AI-assisted insights across the entire Cortex ecosystem.
The Google GKE MCP server is a managed Model Context Protocol server that provides AI applications with tools to manage Google Kubernetes Engine (GKE) clusters and Kubernetes resources. It exposes a structured, discoverable interface that allows AI agents to interact with GKE and Kubernetes APIs, enabling them to inspect cluster configurations, retrieve Kubernetes resource YAMLs, monitor operations like cluster upgrades, diagnose issues, and optimize costs—all without needing to parse text output or use complex kubectl commands.
Provides read access to your GKE and Kubernetes resources.
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server for Kubernetes that allows querying cluster information and diagnosing issues through natural language interfaces like Claude.7MIT
- AlicenseAqualityBmaintenanceRead-only MCP server for safe Kubernetes inspection, diagnosis, and debugging. Supports Kubernetes core, Helm, Argo Workflows, and Argo CD.2174 npm5MIT
- AlicenseAqualityDmaintenanceA read-only MCP server for inspecting Kubernetes clusters, allowing LLMs to list resources, describe pods, and read logs without mutation.5MIT
- AlicenseAqualityDmaintenanceExposes a Kubernetes cluster to MCP-compatible AI clients, enabling read-only and optional write operations on cluster resources like pods, deployments, and namespaces through natural language.91MIT