k8stools 2.0.4__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. k8stools-2.2.0/CHANGELOG.md +210 -0
  2. {k8stools-2.0.4 → k8stools-2.2.0}/CLAUDE.md +58 -2
  3. {k8stools-2.0.4 → k8stools-2.2.0}/PKG-INFO +90 -5
  4. {k8stools-2.0.4 → k8stools-2.2.0}/README.md +88 -4
  5. {k8stools-2.0.4 → k8stools-2.2.0}/designs/mock-state-capture.md +8 -3
  6. {k8stools-2.0.4 → k8stools-2.2.0}/docs/ROADMAP.md +5 -39
  7. {k8stools-2.0.4 → k8stools-2.2.0}/pyproject.toml +2 -2
  8. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/__init__.py +1 -1
  9. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/capture.py +24 -0
  10. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/k8s_tools.py +348 -64
  11. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mcp_client.py +3 -2
  12. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mcp_server.py +27 -0
  13. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mock_state.py +113 -5
  14. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mock_tools.py +7 -0
  15. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_capture.py +139 -1
  16. k8stools-2.2.0/tests/test_cluster_binding.py +276 -0
  17. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_duration_format.py +1 -0
  18. k8stools-2.2.0/tests/test_events.py +164 -0
  19. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_k8s_tools.py +44 -1
  20. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_k8s_tools_realk8s.py +58 -0
  21. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mock_state.py +79 -2
  22. {k8stools-2.0.4 → k8stools-2.2.0}/uv.lock +1 -1
  23. k8stools-2.0.4/redaction-bug.md +0 -25
  24. k8stools-2.0.4/time-bug-report.md +0 -159
  25. {k8stools-2.0.4 → k8stools-2.2.0}/.claude/settings.local.json +0 -0
  26. {k8stools-2.0.4 → k8stools-2.2.0}/.github/workflows/unit-tests.yml +0 -0
  27. {k8stools-2.0.4 → k8stools-2.2.0}/.gitignore +0 -0
  28. {k8stools-2.0.4 → k8stools-2.2.0}/.python-version +0 -0
  29. {k8stools-2.0.4 → k8stools-2.2.0}/.vscode/mcp.json +0 -0
  30. {k8stools-2.0.4 → k8stools-2.2.0}/.vscode/settings.json +0 -0
  31. {k8stools-2.0.4 → k8stools-2.2.0}/LICENSE +0 -0
  32. {k8stools-2.0.4 → k8stools-2.2.0}/chat-with-cluster.png +0 -0
  33. {k8stools-2.0.4 → k8stools-2.2.0}/envrc.template +0 -0
  34. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/fixtures/otel-demo.json +0 -0
  35. {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/redaction.py +0 -0
  36. {k8stools-2.0.4 → k8stools-2.2.0}/tests/conftest.py +0 -0
  37. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_log_decoding.py +0 -0
  38. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mcp_client.py +0 -0
  39. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mock_tools.py +0 -0
  40. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_new_tools.py +0 -0
  41. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_redaction.py +0 -0
  42. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_replicaset_tool.py +0 -0
  43. {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_version.py +0 -0
@@ -0,0 +1,210 @@
1
+ # Changelog
2
+
3
+ All notable changes to k8stools. Versions follow [semantic versioning](https://semver.org/);
4
+ dates are release-tag dates.
5
+
6
+ ## 2.2.0 — 2026-09-28
7
+
8
+ ### Added
9
+ - **`count` and `first_seen` on events** (issue #8). `get_pod_events` and
10
+ `get_events` now report how many occurrences Kubernetes combined into each
11
+ event record, and how long ago the first one was, next to `last_seen`. For a
12
+ crash-looping container this gives the restart rate directly: on one cluster,
13
+ a `Created` count of 579 over 2 days 8 hours is a restart every ~5.8 minutes,
14
+ a number agents kept getting wrong from the other durations. The tool
15
+ descriptions explain the window the count covers and warn that `BackOff`
16
+ counts are several times the number of restarts. `print_pod_events` and
17
+ `print_events` gain a COUNT column.
18
+ - Captures store both fields; captures from before 2.2.0 replay them as `None`.
19
+
20
+ ### Fixed
21
+ - **Replayed `captured_at` was on a different clock** (issue #7).
22
+ `get_cluster_info` returned the date in the capture file while every other
23
+ replayed timestamp was re-anchored to the server's start, so a pod killed
24
+ "5 minutes ago" appeared beside a capture taken a month ago. `captured_at` is
25
+ now the replay's anchor (the server's start time); the recorded date is in the
26
+ startup log and on `MockState.captured_at`.
27
+ - **Replayed logs kept their original timestamps.** Log line timestamps were not
28
+ re-anchored, so they contradicted every other replayed time, and
29
+ `since_seconds` (compared against the replay's "now") returned an empty log for
30
+ any capture older than the window, as if the container had logged nothing.
31
+ The kubelet's timestamp prefix on each line is now moved like every other
32
+ datetime, keeping its original precision and format. Timestamps an application
33
+ writes inside its messages are left as recorded.
34
+ - **Events recorded through `events.k8s.io/v1` had no time.** Their core/v1 view
35
+ leaves `lastTimestamp` empty and records the time in `eventTime` (and
36
+ repetition in `series`), so `last_seen` came back `None`. Both are now read
37
+ when the older fields are empty.
38
+
39
+ ## 2.1.0 — 2026-09-28
40
+
41
+ ### Added
42
+ - **Cluster selection.** `k8s-mcp-server` and `k8s-capture-state` take
43
+ `--kubeconfig PATH` and `--context NAME`; the context can also be pinned with
44
+ the `K8STOOLS_CONTEXT` environment variable. The order is flag, then
45
+ `KUBECONFIG` / `K8STOOLS_CONTEXT`, then `~/.kube/config` and its
46
+ `current-context`. The MCP server binds at startup and logs which cluster it is
47
+ answering from. Direct Python callers can call `k8s_tools.configure(kubeconfig,
48
+ context)`. See README, "Selecting a cluster".
49
+ - **`get_cluster_info` tool.** Reports which cluster the tools answer from: source
50
+ (`kubeconfig`, `in-cluster` or `capture`), context, API server URL, kubeconfig
51
+ path and server version. It never returns credentials. Replayed, it reports
52
+ `source="capture"` and when the capture was taken, so an agent can tell replayed
53
+ evidence from a live cluster.
54
+ - **Captures record their cluster**: the context, server URL and server version
55
+ are stored in a new optional `cluster` key, which `get_cluster_info` reads on
56
+ replay. The kubeconfig path is left out: it names a file on the capturing
57
+ machine. Older captures still load; older readers ignore the key.
58
+ - **`ContainerStateTerminated.ran_for`**: how long a terminated container instance
59
+ ran (`finished_at - started_at`), as a whole-second duration. It gives the
60
+ number an agent otherwise has to subtract, and often mixes up with how much
61
+ time the container's log covers or how often it restarts.
62
+ `get_pod_container_statuses` documents what it is not.
63
+ - `k8s_tools.Interval`, a duration type for a fixed span between two moments, next
64
+ to `Duration` (an age). Capture replay advances ages with the replay clock but
65
+ reads intervals back unchanged.
66
+
67
+ ### Changed
68
+ - **A cluster selection that cannot be loaded is an error.** A given
69
+ `--kubeconfig`, `--context` or `K8STOOLS_CONTEXT` that fails to load (missing
70
+ file, misspelled context) stops the MCP server and fails a capture, with the new
71
+ `K8sClusterSelectionError`. Previously every load failure fell back to in-cluster
72
+ config, which inside a pod meant silently binding to the pod's own cluster. With
73
+ no selection, the in-cluster fallback is unchanged, and the server still starts
74
+ when no configuration can be loaded.
75
+ - **All tools share one cluster binding, made once.** The core, apps and batch API
76
+ clients used to load the kubeconfig separately, each on first use, so a
77
+ `kubectl config use-context` between a server's first pod query and its first
78
+ deployment query left it answering from two clusters. A running server now
79
+ ignores later changes to the kubeconfig's current context.
80
+ - **k8stools no longer sets the kubernetes client's process-wide default
81
+ configuration.** Its binding uses a configuration of its own. Python code that
82
+ relied on k8stools having loaded the kubeconfig for its own kubernetes calls
83
+ must now load it itself (`kubernetes.config.load_kube_config()`).
84
+ - `--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`,
85
+ which serve a capture rather than a cluster.
86
+ - `k8s-mcp-client` passes `K8STOOLS_CONTEXT` through to the server it starts, as
87
+ it already did `KUBECONFIG`.
88
+
89
+ ## 2.0.4 — 2026-09-19
90
+
91
+ ### Fixed
92
+ - **Pod logs arrived as a single unusable `b'...'` line** (issue #6). Since
93
+ kubernetes 36 the log endpoint's body reaches the client as `bytes`, and the
94
+ generated client turned it into `repr(bytes)`. Logs are now decoded where the
95
+ API is called, with invalid bytes replaced rather than failing the call, which
96
+ fixes the log tools, the Job/CronJob log readers and `k8s-capture-state` at once.
97
+ - Redaction handles `bytes` values explicitly.
98
+
99
+ ### Documentation
100
+ - `previous=True` has three Kubernetes behaviors that look like bugs (identical
101
+ output during CrashLoopBackOff, a restart shifting the window between calls, and
102
+ a reclaimed log answering 200 with an error message). They are documented in the
103
+ tool's description and the README rather than changed.
104
+
105
+ ## 2.0.3 — 2026-09-17
106
+
107
+ ### Fixed
108
+ - **Duration fields failed strict schema validation.** Ages serialized with
109
+ fractional seconds (`"P2DT5M27.978616S"`), which the `format: "duration"` grammar
110
+ their own schema declares does not allow, so an MCP client that validates
111
+ structured output (ajv does) rejected every list tool on every row. Durations are
112
+ now whole, non-negative seconds (`k8s_tools.Duration`); a cluster clock ahead of
113
+ ours no longer yields a negative age.
114
+ - **Redaction blacked out whole values.** One secret-shaped substring replaced an
115
+ entire ConfigMap value, making config-as-JSON unreadable. Tokens (AWS keys, JWTs)
116
+ are now replaced where they sit. PEM private keys are still removed whole. A value
117
+ that parses as JSON is searched field by field, which also catches a
118
+ `{"password": ...}` inside it that was previously missed.
119
+
120
+ ## 2.0.2 — 2026-09-12
121
+
122
+ Includes the 2.0.1 packaging fix, which was tagged but not published.
123
+
124
+ ### Changed
125
+ - `k8s-mcp-client` always starts the server with the current interpreter
126
+ (`python -m k8stools.mcp_server`) instead of looking for `k8s-mcp-server` next
127
+ to `argv[0]`, and no longer prints a leftover debug line.
128
+ - `twine` and `build` dropped from the dev dependencies (uv replaces both).
129
+
130
+ ### Fixed
131
+ - `rich` and `pydantic` are declared as runtime dependencies. `k8s-mcp-client`
132
+ imported `rich`, which was only a dev dependency, so it failed with
133
+ `ModuleNotFoundError` on any install from PyPI (2.0.0 and all earlier releases).
134
+
135
+ ## 2.0.1 — 2026-09-12 (tagged, not published)
136
+
137
+ - The packaging fix listed under 2.0.2.
138
+
139
+ ## 2.0.0 — 2026-09-12
140
+
141
+ ### Added
142
+ - **Mock state capture and replay.** `k8s-capture-state` snapshots a live cluster
143
+ into one JSON file, and `k8s-mcp-server --state-file FILE` replays it through all
144
+ 19 tools, so agents can be tested against realistic scenarios with no cluster.
145
+ `--state-time frozen` pins every age for repeatable test suites.
146
+ Previous-instance logs are captured, and captures can be written and read
147
+ gzipped. Design: `designs/mock-state-capture.md`.
148
+
149
+ ### Changed (breaking)
150
+ - `mock_tools` serves one captured cluster (the built-in OTel Demo snapshot by
151
+ default): a pod not in the capture does not exist for any tool, instead of
152
+ getting a synthesized answer.
153
+ - Redaction's key-name rule matches whole words and exempts a field named exactly
154
+ `key`, which in the Kubernetes API is always structural. On a real 38-pod cluster
155
+ this went from 139 redactions to 5, still catching all 3 real secrets.
156
+
157
+ ## 1.2.0 — 2026-09-11
158
+
159
+ ### Added
160
+ - `get_replicaset_summaries`: a Deployment's replica sets, which are its change
161
+ history, sorted oldest revision first. Filtered to one deployment, the last entry
162
+ is its current revision, so "when did this last change, and what changed" is one
163
+ call.
164
+
165
+ ## 1.1.0 — 2026-09-06
166
+
167
+ Delivered the Priority-1 batch from a heavy RCA user's feature request, plus the
168
+ cross-cutting redaction ask, and upgraded core dependencies.
169
+
170
+ ### Added
171
+ - `get_configmap_summaries` / `get_configmap`: ConfigMap listing and full-content
172
+ read.
173
+ - `get_cronjob_summaries` / `get_job_summaries`: CronJob and Job spec and status,
174
+ including the pod template's container images and env; `owner` links a Job back
175
+ to its CronJob.
176
+ - `get_logs_for_job` / `get_logs_for_cronjob`: find the most recent pod of a Job,
177
+ or the most recent run of a CronJob, and return its logs.
178
+ - `get_pvc_summaries`: PVC listing with `mounted_by` pod resolution, which surfaces
179
+ orphaned PVCs.
180
+ - `get_events`: cluster- or namespace-wide events with server-side filtering
181
+ (`reason`, `involved_kind`, `involved_name`, `event_type`).
182
+ - `get_statefulset_summaries`: StatefulSet summaries mirroring
183
+ `DeploymentSummary`.
184
+ - `get_logs_for_pod_and_container`: `tail`, `since_seconds` and `previous`
185
+ (previous-instance logs, for crash-loop analysis).
186
+ - `get_node_summaries`: `capacity`, `allocatable`, `conditions`, `taints`, `labels`.
187
+ - `get_service_summaries`: `selector`, `labels`, `annotations`.
188
+ - **Secret redaction**: one pass at the MCP server's output boundary, on by
189
+ default, opt-out with `--no-redact` or `K8STOOLS_REDACT=0`. It matches by value
190
+ shape (AWS keys, JWTs, PEM private keys) and by key/env-var name
191
+ (`key|secret|token|password|credential`), replacing matches with a visible
192
+ `[REDACTED]` marker. Direct Python callers get raw values and can call
193
+ `redaction.redact_object` themselves.
194
+
195
+ ### Changed
196
+ - `mcp` 1.12 → 2.1.1 (FastMCP → MCPServer), `kubernetes` 33.1 → 36.0.3.
197
+
198
+ ## 1.0.1 — 2025-07-29
199
+
200
+ - README fixes.
201
+
202
+ ## 1.0.0 — 2025-07-28
203
+
204
+ - First stable release: kubectl-style read-only tools for namespaces, nodes, pods,
205
+ container statuses, pod events, pod specs, logs, deployments and services, with
206
+ mock versions, as Python functions or through an MCP server.
207
+
208
+ ## 0.1.0, 0.1.2 — 2025-07-21, 2025-07-23
209
+
210
+ - Initial releases (0.1.1 was tagged but not published).
@@ -24,11 +24,21 @@ tests/
24
24
  test_capture.py - Capture serialization round-trip and capture-time redaction
25
25
  test_k8s_tools_realk8s.py - Integration tests (real cluster, auto-skipped if unreachable)
26
26
  test_mock_tools.py - Tests for mock_tools module (incl. parity with k8s_tools.TOOLS)
27
+ test_events.py - Event aggregation (count/first_seen), incl. the events.k8s.io fallback
28
+ test_cluster_binding.py - Cluster selection: configure(), the shared binding, get_cluster_info, server flags
27
29
  test_log_decoding.py - Log decoding at the API boundary (bytes vs str), `previous` plumbing, log redaction
28
30
  test_mcp_client.py - MCP client tests
29
31
  test_version.py - Asserts pyproject and package __version__ agree
30
32
  ```
31
33
 
34
+ ## Changelog
35
+
36
+ `CHANGELOG.md` records user-visible changes per release, newest first. Add to the
37
+ top entry (`## X.Y.Z — unreleased` until tagged) in the same change that makes
38
+ them, and call out behavior changes under **Changed**. It links from the README and
39
+ from the package metadata (`[project] urls`), so it shows on PyPI. `docs/ROADMAP.md`
40
+ holds only planned and deferred work.
41
+
32
42
  ## Environment setup
33
43
 
34
44
  ```bash
@@ -76,6 +86,12 @@ This is the same rule `MockState`'s replay clock already applies, for the same
76
86
  reason. `tests/test_duration_format.py` checks each model's serialized output
77
87
  against the format its own schema declares.
78
88
 
89
+ `Duration` means an *age* (time from some moment until now). For a fixed span
90
+ between two moments — `ContainerStateTerminated.ran_for` — use `k8s_tools.Interval`,
91
+ which has the same normalization and schema. The difference is capture replay:
92
+ `MockState` advances every age by the time since the server started, so a span
93
+ declared as `Duration` grows on replay (a 60s run read back as 1d+60s a day in).
94
+
79
95
  Use snake_case field names. Include `pod_name`/`namespace` in container-level models for context.
80
96
 
81
97
  All tools are collected in the `TOOLS` list in `k8s_tools.py` for agent/MCP registration.
@@ -109,7 +125,7 @@ raise K8sConfigError("Could not load kube config") # config/connection problem
109
125
  raise K8sApiError(f"Error fetching pods: {e}") # API operation failures
110
126
  ```
111
127
 
112
- ## Global API client
128
+ ## Cluster binding and the global API clients
113
129
 
114
130
  Lazy singleton initialization pattern used throughout:
115
131
 
@@ -119,7 +135,33 @@ if K8S is None:
119
135
  K8S = _get_api_client()
120
136
  ```
121
137
 
122
- `_get_api_client()` calls `config.load_kube_config()`, which respects the `KUBECONFIG` environment variable.
138
+ `_get_api_client()`, `_get_apps_v1_api_client()` and `_get_batch_v1_api_client()`
139
+ all build on **one** `ApiClient`, the process's cluster binding (`_BINDING`),
140
+ resolved once by `configure(kubeconfig, context)` — called at startup by both CLIs —
141
+ or lazily on first use. Invariants:
142
+
143
+ - **One binding for every API group.** The helpers used to load the kubeconfig
144
+ separately on each group's first use, and each API object snapshots the global
145
+ default `Configuration` when constructed, so a `kubectl config use-context`
146
+ between a server's first pod query and its first deployment query split it
147
+ across two clusters. Don't reintroduce a per-helper `load_kube_config()`.
148
+ - **An explicit selection never falls back to in-cluster config.** The client
149
+ raises the same `ConfigException` for a misspelled context as for a missing
150
+ kubeconfig; falling back would bind a server in a pod to the pod's own cluster.
151
+ An explicit selection that fails raises `K8sClusterSelectionError` (a
152
+ `K8sConfigError`), which stops the MCP server; with no selection, the server
153
+ starts anyway and each call reports the problem, as it always has.
154
+ - **The binding never touches the process-wide default `Configuration`** (it passes
155
+ `client_configuration`), so library callers' own kubernetes clients are left alone.
156
+ - Resolution: `--kubeconfig` / argument, then `KUBECONFIG`, then `~/.kube/config`;
157
+ `--context` / argument, then `K8STOOLS_CONTEXT`, then `current-context`. There is
158
+ deliberately no `K8STOOLS_KUBECONFIG`: `KUBECONFIG` already is that variable.
159
+ The kubernetes client reads `KUBECONFIG` once, at import
160
+ (`KUBE_CONFIG_DEFAULT_LOCATION`), so setting it at runtime has no effect.
161
+
162
+ `get_cluster_info` reports the binding. Its replay counterpart answers from the
163
+ capture's optional top-level `cluster` record (context, server, server version —
164
+ never the kubeconfig path) with `source="capture"`.
123
165
 
124
166
  ## Using tools directly in an agent
125
167
 
@@ -145,6 +187,10 @@ k8s-mcp-server --mock
145
187
  k8s-mcp-server --state-file incident-1234.json
146
188
  k8s-mcp-server --state-file incident-1234.json --state-time frozen
147
189
 
190
+ # Pin a cluster (default: KUBECONFIG / ~/.kube/config and its current-context)
191
+ k8s-mcp-server --kubeconfig ~/.kube/prod.yaml --context prod-eu
192
+ K8STOOLS_CONTEXT=prod-eu k8s-mcp-server
193
+
148
194
  # Disable secret redaction (on by default)
149
195
  k8s-mcp-server --no-redact
150
196
  ```
@@ -168,6 +214,16 @@ Invariants worth preserving when touching this code:
168
214
  replay — "deployment 8d old, current replica set 7h34m old, so it was upgraded
169
215
  7h34m ago". The replay clock quantizes to whole seconds and samples once per
170
216
  query to keep that exact; don't reintroduce per-field `datetime.now()`.
217
+ One clock for everything replay returns: datetimes are re-anchored so
218
+ capture time becomes server start time, and that includes
219
+ `get_cluster_info().captured_at` (the anchor itself) and the kubelet's
220
+ timestamp prefix on each log line (`_reanchor_log`). Anything that reports a
221
+ time from the file verbatim contradicts the rest. Tests that capture and
222
+ replay at the same instant cannot see this; use a capture date in the past.
223
+ Only ages advance: an `Interval` field is stored under the same `_seconds` key
224
+ but read back unchanged. Replay tests that freeze the clock can't tell the two
225
+ apart (frozen elapsed time is zero), so test a new duration field on an
226
+ advancing clock with a `server_start_time` in the past.
171
227
  - **`MockState` must match the real tool's documented behavior**, not just its
172
228
  types — filter semantics, result ordering (`get_replicaset_summaries` re-sorts
173
229
  at query time), and which failures raise vs. return empty.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: k8stools
3
- Version: 2.0.4
3
+ Version: 2.2.0
4
4
  Summary: Tools and an MCP server for building Kubernetes agents
5
5
  Project-URL: Homepage, https://github.com/BenedatLLC/k8stools
6
+ Project-URL: Changelog, https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md
6
7
  Author: Jeff Fischer
7
8
  License: Apache License
8
9
  Version 2.0, January 2004
@@ -273,10 +274,14 @@ This installs three commands: `k8s-mcp-server` (the MCP server), `k8s-mcp-client
273
274
  (a client for manual testing), and `k8s-capture-state` (snapshot a cluster to a
274
275
  replayable file).
275
276
 
277
+ What changed in each release is in the
278
+ [changelog](https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md).
279
+
276
280
  ## Current tools
277
281
 
278
282
  These are the tools we define:
279
283
 
284
+ * `get_cluster_info` - which cluster the tools are answering from: kubeconfig context, API server URL and version, or the capture being replayed
280
285
  * `get_namespaces` - get a list of namespaces, like `kubectl get namespace`
281
286
  * `get_node_summaries` - get a list of nodes, like `kubectl get nodes -o wide` (includes capacity/allocatable/conditions/taints/labels)
282
287
  * `get_pod_summaries` - get a list of pods, like `kubectl get pods -o wide`
@@ -333,13 +338,25 @@ result = agent.run_sync("What is the status of the pods in my cluster?")
333
338
  print(result.output)
334
339
  ```
335
340
 
341
+ The tools use `KUBECONFIG` (or `~/.kube/config`) and its current context. To pin a
342
+ cluster instead, call `configure` before the first tool call:
343
+
344
+ ```python
345
+ from k8stools import k8s_tools
346
+
347
+ k8s_tools.configure(kubeconfig="~/.kube/prod.yaml", context="prod-eu")
348
+ ```
349
+
350
+ See [Selecting a cluster](#selecting-a-cluster) for how the selection is resolved.
351
+
336
352
  ### Using via MCP
337
353
  The script `k8s-mcp-server` provides an MCP server for the same set of tools.
338
354
  Here are the command line arguments for the server:
339
355
  ```
340
356
  usage: k8s-mcp-server [-h] [--transport {streamable-http,stdio}] [--host HOST] [--port PORT]
341
- [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug] [--mock]
342
- [--state-file FILE] [--state-time {advancing,frozen}] [--no-redact]
357
+ [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
358
+ [--kubeconfig PATH] [--context NAME] [--mock] [--state-file FILE]
359
+ [--state-time {advancing,frozen}] [--no-redact]
343
360
 
344
361
  Run the MCP server.
345
362
 
@@ -352,6 +369,11 @@ options:
352
369
  --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
353
370
  Log level [default: INFO]
354
371
  --debug Enable debug mode [default: False]
372
+ --kubeconfig PATH Kubeconfig file to use [default: KUBECONFIG, then ~/.kube/config]
373
+ --context NAME Kubeconfig context to use [default: K8STOOLS_CONTEXT, then the
374
+ kubeconfig's current-context]. A kubeconfig or context that is given but
375
+ cannot be loaded stops the server rather than falling back to another
376
+ cluster.
355
377
  --mock Run mock versions of the tools that don't need a cluster
356
378
  --state-file FILE Serve a captured cluster snapshot from FILE (implies --mock)
357
379
  --state-time {advancing,frozen}
@@ -359,6 +381,48 @@ options:
359
381
  --no-redact Disable secret redaction of tool output (on by default)
360
382
  ```
361
383
 
384
+ #### Selecting a cluster
385
+
386
+ Each server answers from exactly one cluster, chosen at startup:
387
+
388
+ | | Kubeconfig file | Context |
389
+ |---|---|---|
390
+ | 1st | `--kubeconfig PATH` | `--context NAME` |
391
+ | 2nd | `KUBECONFIG` | `K8STOOLS_CONTEXT` |
392
+ | 3rd | `~/.kube/config` | the file's `current-context` |
393
+
394
+ With none of these given and no usable kubeconfig, the server falls back to the
395
+ in-cluster service account, as when it runs in a pod.
396
+
397
+ The server logs the binding when it starts:
398
+
399
+ ```
400
+ WARNING:root:Bound to cluster: context 'prod-eu' at https://prod-eu.example:6443 (kubeconfig: /home/me/.kube/prod.yaml)
401
+ ```
402
+
403
+ and the `get_cluster_info` tool reports it to an agent, so an agent can check which
404
+ cluster it is talking to without leaving MCP.
405
+
406
+ A few rules keep a server from quietly answering from the wrong cluster:
407
+
408
+ * **An explicit selection never falls back.** If a `--kubeconfig`, `--context` or
409
+ `K8STOOLS_CONTEXT` is given but cannot be loaded (a missing file, a misspelled
410
+ context), the server exits with an error. It does not fall back to in-cluster
411
+ config, which in a pod would silently mean *that pod's* cluster.
412
+ * **The binding is made once.** Every tool shares it, and it does not follow later
413
+ changes to the kubeconfig: running `kubectl config use-context` does not
414
+ repoint a server that is already running.
415
+ * **`--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`**,
416
+ which serve a capture rather than a cluster.
417
+
418
+ To serve two clusters, run two servers:
419
+
420
+ ```sh
421
+ k8s-mcp-server --transport=streamable-http --port=8001 --context prod-eu
422
+ k8s-mcp-server --transport=streamable-http --port=8002 \
423
+ --kubeconfig ~/.kube/eks.yaml --context staging
424
+ ```
425
+
362
426
  #### Use MCP with the stdio transport
363
427
  The *stdio* transport is best for use with local Coding Agents, like GitHub CoPilot or Cursor.
364
428
  It is the default, so you can run the `k8s-mcp-server` script without arguments. Here's an example
@@ -381,7 +445,8 @@ This assumes the following:
381
445
  1. The Python virtual environment is expected to be in `.venv` under the root of your VSCode workspace
382
446
  2. You have installed the k8stools package into your workspace
383
447
  3. The environment file `.envrc` contains any variables you need defined. In particular, you may need to
384
- set `KUBECONFIG` to point to your `kubectl` config file.
448
+ set `KUBECONFIG` to point to your `kubectl` config file, and `K8STOOLS_CONTEXT` to pin a context
449
+ (or pass `--kubeconfig` / `--context` in `args`).
385
450
 
386
451
  #### Use MCP with the streamable HTTP transport
387
452
  The *streamable http* transport is enabled with the command line option `--transport=streamable-http`. It will
@@ -466,7 +531,8 @@ k8s-mcp-server --state-file incident-1234.json
466
531
  ### `k8s-capture-state` options
467
532
 
468
533
  ```
469
- usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE] [--no-logs]
534
+ usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
535
+ [--kubeconfig PATH] [--context NAME] [--no-logs]
470
536
  [--max-log-lines MAX_LOG_LINES] [--no-previous-logs]
471
537
  [--no-redact]
472
538
  [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
@@ -479,6 +545,11 @@ options:
479
545
  captured in full.
480
546
  -o FILE, --output FILE
481
547
  Output file [default: k8s-state-<timestamp>.json]
548
+ --kubeconfig PATH Kubeconfig file to capture from [default: KUBECONFIG,
549
+ then ~/.kube/config]
550
+ --context NAME Kubeconfig context to capture from [default:
551
+ K8STOOLS_CONTEXT, then the kubeconfig's current-
552
+ context]
482
553
  --no-logs Skip container logs
483
554
  --max-log-lines MAX_LOG_LINES
484
555
  Log lines to capture per container [default: 1000]
@@ -507,11 +578,16 @@ Everything the tools can read, so that every tool answers on replay:
507
578
 
508
579
  | | |
509
580
  |---|---|
581
+ | 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) |
510
582
  | Cluster-wide | namespaces, nodes |
511
583
  | Per namespace | deployments, replica sets, services, statefulsets, cronjobs, jobs, PVCs, events |
512
584
  | ConfigMaps | summary **and** full contents, so `get_configmap` works too |
513
585
  | Per pod | summary, labels, container statuses, spec, and per-container logs |
514
586
 
587
+ A cluster is selected exactly as for the MCP server (see [Selecting a
588
+ cluster](#selecting-a-cluster)), and a selection that cannot be loaded fails the
589
+ capture.
590
+
515
591
  `--namespace` restricts only the namespaced resources; namespaces and nodes are
516
592
  always captured in full so the cluster still makes sense.
517
593
 
@@ -608,6 +684,15 @@ long session cannot see ages drift between its first tool call and its last.
608
684
  a suite that believed it had frozen the clock and had not would be flaky with no
609
685
  visible cause.)
610
686
 
687
+ Absolute times are replayed the same way: every timestamp a tool returns is moved
688
+ so the moment of capture becomes the moment the server started. A container killed
689
+ 5 minutes before the capture reads as killed 5 minutes before server start, and
690
+ `get_cluster_info`'s `captured_at` is that start time, not the date in the file,
691
+ so the agent sees one consistent clock. Log line timestamps (the kubelet's
692
+ prefix) are moved too, so `since_seconds` works on a capture of any age;
693
+ timestamps an application writes inside its own log messages are left as
694
+ recorded. The file's own capture date is in the server's startup log.
695
+
611
696
  ### Replaying from Python
612
697
 
613
698
  For a test suite, skip the server and load the capture directly. `MockState` has
@@ -54,10 +54,14 @@ This installs three commands: `k8s-mcp-server` (the MCP server), `k8s-mcp-client
54
54
  (a client for manual testing), and `k8s-capture-state` (snapshot a cluster to a
55
55
  replayable file).
56
56
 
57
+ What changed in each release is in the
58
+ [changelog](https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md).
59
+
57
60
  ## Current tools
58
61
 
59
62
  These are the tools we define:
60
63
 
64
+ * `get_cluster_info` - which cluster the tools are answering from: kubeconfig context, API server URL and version, or the capture being replayed
61
65
  * `get_namespaces` - get a list of namespaces, like `kubectl get namespace`
62
66
  * `get_node_summaries` - get a list of nodes, like `kubectl get nodes -o wide` (includes capacity/allocatable/conditions/taints/labels)
63
67
  * `get_pod_summaries` - get a list of pods, like `kubectl get pods -o wide`
@@ -114,13 +118,25 @@ result = agent.run_sync("What is the status of the pods in my cluster?")
114
118
  print(result.output)
115
119
  ```
116
120
 
121
+ The tools use `KUBECONFIG` (or `~/.kube/config`) and its current context. To pin a
122
+ cluster instead, call `configure` before the first tool call:
123
+
124
+ ```python
125
+ from k8stools import k8s_tools
126
+
127
+ k8s_tools.configure(kubeconfig="~/.kube/prod.yaml", context="prod-eu")
128
+ ```
129
+
130
+ See [Selecting a cluster](#selecting-a-cluster) for how the selection is resolved.
131
+
117
132
  ### Using via MCP
118
133
  The script `k8s-mcp-server` provides an MCP server for the same set of tools.
119
134
  Here are the command line arguments for the server:
120
135
  ```
121
136
  usage: k8s-mcp-server [-h] [--transport {streamable-http,stdio}] [--host HOST] [--port PORT]
122
- [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug] [--mock]
123
- [--state-file FILE] [--state-time {advancing,frozen}] [--no-redact]
137
+ [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
138
+ [--kubeconfig PATH] [--context NAME] [--mock] [--state-file FILE]
139
+ [--state-time {advancing,frozen}] [--no-redact]
124
140
 
125
141
  Run the MCP server.
126
142
 
@@ -133,6 +149,11 @@ options:
133
149
  --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
134
150
  Log level [default: INFO]
135
151
  --debug Enable debug mode [default: False]
152
+ --kubeconfig PATH Kubeconfig file to use [default: KUBECONFIG, then ~/.kube/config]
153
+ --context NAME Kubeconfig context to use [default: K8STOOLS_CONTEXT, then the
154
+ kubeconfig's current-context]. A kubeconfig or context that is given but
155
+ cannot be loaded stops the server rather than falling back to another
156
+ cluster.
136
157
  --mock Run mock versions of the tools that don't need a cluster
137
158
  --state-file FILE Serve a captured cluster snapshot from FILE (implies --mock)
138
159
  --state-time {advancing,frozen}
@@ -140,6 +161,48 @@ options:
140
161
  --no-redact Disable secret redaction of tool output (on by default)
141
162
  ```
142
163
 
164
+ #### Selecting a cluster
165
+
166
+ Each server answers from exactly one cluster, chosen at startup:
167
+
168
+ | | Kubeconfig file | Context |
169
+ |---|---|---|
170
+ | 1st | `--kubeconfig PATH` | `--context NAME` |
171
+ | 2nd | `KUBECONFIG` | `K8STOOLS_CONTEXT` |
172
+ | 3rd | `~/.kube/config` | the file's `current-context` |
173
+
174
+ With none of these given and no usable kubeconfig, the server falls back to the
175
+ in-cluster service account, as when it runs in a pod.
176
+
177
+ The server logs the binding when it starts:
178
+
179
+ ```
180
+ WARNING:root:Bound to cluster: context 'prod-eu' at https://prod-eu.example:6443 (kubeconfig: /home/me/.kube/prod.yaml)
181
+ ```
182
+
183
+ and the `get_cluster_info` tool reports it to an agent, so an agent can check which
184
+ cluster it is talking to without leaving MCP.
185
+
186
+ A few rules keep a server from quietly answering from the wrong cluster:
187
+
188
+ * **An explicit selection never falls back.** If a `--kubeconfig`, `--context` or
189
+ `K8STOOLS_CONTEXT` is given but cannot be loaded (a missing file, a misspelled
190
+ context), the server exits with an error. It does not fall back to in-cluster
191
+ config, which in a pod would silently mean *that pod's* cluster.
192
+ * **The binding is made once.** Every tool shares it, and it does not follow later
193
+ changes to the kubeconfig: running `kubectl config use-context` does not
194
+ repoint a server that is already running.
195
+ * **`--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`**,
196
+ which serve a capture rather than a cluster.
197
+
198
+ To serve two clusters, run two servers:
199
+
200
+ ```sh
201
+ k8s-mcp-server --transport=streamable-http --port=8001 --context prod-eu
202
+ k8s-mcp-server --transport=streamable-http --port=8002 \
203
+ --kubeconfig ~/.kube/eks.yaml --context staging
204
+ ```
205
+
143
206
  #### Use MCP with the stdio transport
144
207
  The *stdio* transport is best for use with local Coding Agents, like GitHub CoPilot or Cursor.
145
208
  It is the default, so you can run the `k8s-mcp-server` script without arguments. Here's an example
@@ -162,7 +225,8 @@ This assumes the following:
162
225
  1. The Python virtual environment is expected to be in `.venv` under the root of your VSCode workspace
163
226
  2. You have installed the k8stools package into your workspace
164
227
  3. The environment file `.envrc` contains any variables you need defined. In particular, you may need to
165
- set `KUBECONFIG` to point to your `kubectl` config file.
228
+ set `KUBECONFIG` to point to your `kubectl` config file, and `K8STOOLS_CONTEXT` to pin a context
229
+ (or pass `--kubeconfig` / `--context` in `args`).
166
230
 
167
231
  #### Use MCP with the streamable HTTP transport
168
232
  The *streamable http* transport is enabled with the command line option `--transport=streamable-http`. It will
@@ -247,7 +311,8 @@ k8s-mcp-server --state-file incident-1234.json
247
311
  ### `k8s-capture-state` options
248
312
 
249
313
  ```
250
- usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE] [--no-logs]
314
+ usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
315
+ [--kubeconfig PATH] [--context NAME] [--no-logs]
251
316
  [--max-log-lines MAX_LOG_LINES] [--no-previous-logs]
252
317
  [--no-redact]
253
318
  [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
@@ -260,6 +325,11 @@ options:
260
325
  captured in full.
261
326
  -o FILE, --output FILE
262
327
  Output file [default: k8s-state-<timestamp>.json]
328
+ --kubeconfig PATH Kubeconfig file to capture from [default: KUBECONFIG,
329
+ then ~/.kube/config]
330
+ --context NAME Kubeconfig context to capture from [default:
331
+ K8STOOLS_CONTEXT, then the kubeconfig's current-
332
+ context]
263
333
  --no-logs Skip container logs
264
334
  --max-log-lines MAX_LOG_LINES
265
335
  Log lines to capture per container [default: 1000]
@@ -288,11 +358,16 @@ Everything the tools can read, so that every tool answers on replay:
288
358
 
289
359
  | | |
290
360
  |---|---|
361
+ | 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) |
291
362
  | Cluster-wide | namespaces, nodes |
292
363
  | Per namespace | deployments, replica sets, services, statefulsets, cronjobs, jobs, PVCs, events |
293
364
  | ConfigMaps | summary **and** full contents, so `get_configmap` works too |
294
365
  | Per pod | summary, labels, container statuses, spec, and per-container logs |
295
366
 
367
+ A cluster is selected exactly as for the MCP server (see [Selecting a
368
+ cluster](#selecting-a-cluster)), and a selection that cannot be loaded fails the
369
+ capture.
370
+
296
371
  `--namespace` restricts only the namespaced resources; namespaces and nodes are
297
372
  always captured in full so the cluster still makes sense.
298
373
 
@@ -389,6 +464,15 @@ long session cannot see ages drift between its first tool call and its last.
389
464
  a suite that believed it had frozen the clock and had not would be flaky with no
390
465
  visible cause.)
391
466
 
467
+ Absolute times are replayed the same way: every timestamp a tool returns is moved
468
+ so the moment of capture becomes the moment the server started. A container killed
469
+ 5 minutes before the capture reads as killed 5 minutes before server start, and
470
+ `get_cluster_info`'s `captured_at` is that start time, not the date in the file,
471
+ so the agent sees one consistent clock. Log line timestamps (the kubelet's
472
+ prefix) are moved too, so `since_seconds` works on a capture of any age;
473
+ timestamps an application writes inside its own log messages are left as
474
+ recorded. The file's own capture date is in the server's startup log.
475
+
392
476
  ### Replaying from Python
393
477
 
394
478
  For a test suite, skip the server and load the capture directly. `MockState` has