# Kubernetes MCP server v0.1.2: Helm installs ignore the namespace argument, and some multi-document applies drop documents

- URL: https://isokron.ai/research/kubernetes-mcp-server-audit
- Published: 2026-11-12
- Author: Joshua Bryan, IsoKron
- Publisher: IsoKron (https://isokron.ai)
- Protocol tested: stdio; the server answered protocol revision 2025-11-25 in our catalog sweeps (not recorded for the agent runs)
- Software tested: containers/kubernetes-mcp-server v0.1.2 (commit 66553fed), built from source with go1.26.11; findings re-run on main at commit 13bfb96f. Target: a local kind cluster, Kubernetes 1.37.0, single node, with KubeVirt v1.9.0 CRDs, Tekton Pipelines v1.16.0 and local Helm charts
- Client: Scripted probe client and raw-API, kubectl and Helm CLI controls, no model in the loop; native usability sessions and the benchmark: Claude Agent SDK through mcp-audit agent, Claude Opus 5.5 (claude-opus-5-5), plus Claude Sonnet 5.5 (claude-sonnet-5-5) on benchmark set A
- Reproduction material: https://github.com/isokron-ai/kubernetes-mcp-writepath-experiment

> helm_install sent chart objects to the wrong namespace in 3 of 3 runs; two separator shapes made applies drop documents yet report success. 17 bugs reported.

## Key results

- **What we found:** `helm_install` created the chart’s objects in the kubeconfig namespace instead of the `namespace` argument in 3 of 3 runs, where the Helm CLI as the same identity used the right one. A `--- ` or `--- # comment` separator made a multi-document apply write only the first document and report success, in 3 of 3 runs for each shape. 17 bugs, 7 design gaps and 2 intended behaviors in all; the 25 items from the frozen probes all still reproduced on upstream main.
- **Upstream status, 12 November 2026:** 15 issues filed on 12 November, [1658](https://github.com/containers/kubernetes-mcp-server/issues/1658) to [1672](https://github.com/containers/kubernetes-mcp-server/issues/1672), plus a comment on [1584](https://github.com/containers/kubernetes-mcp-server/issues/1584). All are open, with no maintainer response as of 12 November 2026. Patches with tests for all 17 bugs are offered in the issues, and fix pull requests [1675](https://github.com/containers/kubernetes-mcp-server/pull/1675) to [1684](https://github.com/containers/kubernetes-mcp-server/pull/1684) are open for 10 of the 15 issues.
- **If you run this server:** On v0.1.2, check where a Helm chart’s objects landed, write plain `---` separators and read objects back after a multi-document apply, send only the fields you mean to change, and never rely on `dryRun: true`: the tools ignore it and the write is real. [What this means for teams](#what-this-means-for-teams-running-the-server).

*Self-funded research by IsoKron, conducted and written with AI assistance. Not commissioned or endorsed by the maintainers of kubernetes-mcp-server or by Red Hat; IsoKron is not affiliated with them. IsoKron sells MCP server audits, and this article shows the method it sells.*

We audited `containers/kubernetes-mcp-server` v0.1.2 (commit 66553fed, released 4 November 2026) between 10 and 12 November 2026, against a local kind cluster running Kubernetes 1.37.0. The project is young and moving fast: v0.1.x is an early release line, and by 11 November upstream main was already 25 commits past it. Every finding was tested in scripted probe runs with no model in the loop, each against a control: the same operation sent straight to the Kubernetes API, or through `kubectl` or the Helm CLI as the same identity.

The finding with the cleanest control is in `helm_install`. Given `namespace: X`, it stored the Helm release record in X but created the chart's objects in the kubeconfig's namespace, in 3 of 3 probe runs on v0.1.2 and 3 of 3 on main; `helm install -n X` as the same identity put everything in X. The second is in multi-document applies: a separator written as `--- ` (trailing space) or `--- # comment` made `resources_create_or_update` write only the first document and report success, in 3 of 3 runs for each shape, where `kubectl` applied every document.

The register holds 17 bugs, 7 design gaps and 2 behaviors upstream intends. The bugs were reported on 12 November as 15 issues and one comment, each with a patch offered. In a before/after benchmark on six tasks committed to git before any change was designed, v0.1.2 and a research build with the bug fixes and 19 usability changes both completed 30 of 30 runs; the research build used 59.2% fewer tool calls and cost 37.0% less per verified success, most of it on one clean-up task.

Status as of 12 November 2026: all 15 issues are open, and fix pull requests for 10 of them are open. As of 12 November 2026 there was no maintainer response.

## What this means for teams running the server

- **Helm (v0.1.2, and main as of 12 November):** after `helm_install`, check where the chart's objects landed. Charts that template `{{ .Release.Namespace }}` install correctly; others go to the kubeconfig's namespace. Pointing the kubeconfig context at the target namespace should avoid it; we infer this from the cause and did not test it.
- **Multi-document YAML:** write separators as a bare `---` and read the objects back after the apply. A partial failure does not say which documents were already applied.
- **No dry run:** the write tools have no dry-run argument and ignore unknown ones. Called with `dryRun: true`, the tool made a real write in 3 of 3 runs, where the API's own `dryRun=All` stored nothing in 3 of 3.
- **Concurrent edits:** send only the fields you mean to change, or keep the `resourceVersion` you read in the body so a stale apply is refused.
- **Annotations and filters:** do not rely on `disable_destructive` or on confirmation rules keyed on `destructiveHint` to stop `pods_run`, which writes but is annotated non-destructive. RBAC is the boundary, as upstream's own documentation says: give the server identity only what it needs.
- **Configuration:** check the served tool list after setting `enabled_tools` or `disabled_tools`; a misspelled tool name is accepted silently.

## Setup and boundary

Everything ran on a single-node kind cluster (kind v0.33.0) on one Mac, seeded with throwaway objects. No real cluster and no one else's data was touched.

| Item | Detail |
| --- | --- |
| Server | v0.1.2 built from source, over stdio; upstream main 13bfb96f for the re-run. Toolsets `core` and `config` (21 tools) by default; `helm`, `tekton` and `kubevirt` where a probe or task needed them |
| Target | Kubernetes 1.37.0, KubeVirt v1.9.0 CRDs only (no VM ran), Tekton Pipelines v1.16.0, local Helm charts, the API server's audit log on |
| Identities | The server ran as a ServiceAccount that can write only in the run's namespace and cannot read Secrets. The Helm CLI control used that identity; most other controls and the benchmark checker ran as the cluster admin |
| Clients | Probes: a scripted client, no model. Agents: Claude Opus 5.5 (`claude-opus-5-5`) through the Claude Agent SDK, with the server's tools only; Claude Sonnet 5.5 (`claude-sonnet-5-5`) on benchmark set A. The client denied every call to `configuration_view`, which returns the kubeconfig with the server's token |

Every mutating request in a run's namespace in the API audit log was matched to a recorded tool call or control step, so no server write went unrecorded. Token figures come in two kinds throughout: benchmark tokens and cost are provider-reported (input, output, cache writes and cache reads; USD), and sizes of what the server returns are cl100k estimates of the text.

## Headline finding: `helm_install` ignores the namespace argument for chart objects (K8S-20)

`helm_install` passes its `namespace` argument to Helm only for release storage. Helm's kube client then falls back to the kubeconfig's namespace (`default` when the context sets none) for every object whose template sets no `metadata.namespace`, which is most charts. The cause is in `newAction` in `pkg/helm/helm.go`; the Helm CLI sets the client's namespace from `--namespace`.

| Arm | v0.1.2 | main 13bfb96f |
| --- | --- | --- |
| `helm_install` with `namespace` set, chart without a templated namespace | Objects sent to `default`; install failed with a 403 naming `default`; release record left `failed` in the requested namespace: 3 of 3 | Same: 3 of 3 |
| Control: `helm install -n`, same identity and chart | Everything created in the requested namespace | Not re-run (no server involved) |
| Control: a chart that templates its namespace, through the tool | Installed correctly: 3 of 3 | 3 of 3 |
| Fixed build | Not reproduced | — |

- **Observed and inferred.** Our server identity could not write `default`, so every install failed loudly, and the API audit log shows the refused creates going there. With an identity that can write the kubeconfig namespace, the objects would land there and the call would report success; that follows from the code, but we did not run it. The `failed` release record also blocks the name until removed.
- **What it does to an agent.** In the native usability session the agent read the 403, concluded that the chart hard-coded `namespace: default`, and told the user to edit the chart's templates. The chart was fine.
- **Fix.** Set the kube client's namespace in `newAction`, as the CLI does: issue [1658](https://github.com/containers/kubernetes-mcp-server/issues/1658). Objects installed before a fix stay where they landed, and the issue says so.

## Second finding: multi-document applies (K8S-06, K8S-15, K8S-01)

`resources_create_or_update` splits multi-document YAML itself instead of using a YAML stream reader, and applies the documents one at a time.

| Input | Through the tool, v0.1.2 | Control |
| --- | --- | --- |
| Separator `--- ` (trailing space) | Success; only the first document applied: 3 of 3 | `kubectl` applied both; the second document alone applied: 3 of 3 |
| Separator `--- # second` | Success; only the first document applied: 3 of 3 | `kubectl` applied both |
| Trailing `---` followed by a newline | Whole call failed with a bare `EOF`: 3 of 3 | Each document alone applied: 3 of 3 |
| A comment-only document in the middle | First applied, then ``no matches for kind ""``: 3 of 3 | `kubectl` applied the file |
| Three documents, the second invalid (K8S-01) | First applied, third never sent; the error named only the invalid one: 3 of 3, and the same for three other failure shapes | `kubectl apply --server-side` applied the first and third and reported the second |

- **K8S-06 and K8S-15** (issue [1659](https://github.com/containers/kubernetes-mcp-server/issues/1659)) share one patch: split as `kubectl`'s YAML reader does, and skip empty and comment-only documents.
- **K8S-01** (issue [1660](https://github.com/containers/kubernetes-mcp-server/issues/1660)): stopping at the first refusal is defensible; not saying which documents were already written is the bug, and our fix names them. The research build also dry-runs every document first and writes nothing if one is refused. That prevents the partial writes we tested, but it is not a transaction: the cluster can change between the dry run and the write.

## Forced apply and lost updates: upstream's choice, measured, not filed

`resources_create_or_update` applies with server-side apply and `force: true`. Upstream chose this on purpose (issue [799](https://github.com/containers/kubernetes-mcp-server/issues/799)); it has come up again in issues [445](https://github.com/containers/kubernetes-mcp-server/issues/445) and [1164](https://github.com/containers/kubernetes-mcp-server/issues/1164) and pull request [1184](https://github.com/containers/kubernetes-mcp-server/pull/1184). We measured what it means for an agent and did not file it.

The API has two protections for this edit: server-side apply without force refuses to take over fields another manager owns, and a `resourceVersion` in the body makes a stale write fail. Forcing switches off the first, so only the second remains, and only if the client keeps the `resourceVersion` it read.

| Arm | Result |
| --- | --- |
| A full copy re-applied through the tool after another writer changed a field, without `resourceVersion` | The other writer's change lost: replicas 10 of 10, a ConfigMap key 10 of 10 |
| The same copy with its stale `resourceVersion` kept | Refused with 409 in 10 of 10 |
| Control: the same body without force, straight to the API | 409 conflict: 3 of 3 |

Neither native session ever sent a `resourceVersion`. In the concurrent-edit job, another writer changed `MAX_CONNECTIONS` from 100 to 200 about half a second after the agent read the ConfigMap; four seconds later the agent's forced apply set it back to 100, and the agent told the user that the other keys were unchanged. The research build adds `resources_patch` with an optional `resourceVersion` precondition, in the shape of upstream's open proposal [1352](https://github.com/containers/kubernetes-mcp-server/issues/1352), and makes apply without force the default. That reverses a deliberate upstream decision, so it is a proposal, not a fix.

## Three more bugs that cross what an operator configures

- **K8S-25: `vm_create` renders `workload` unescaped** (issue [1661](https://github.com/containers/kubernetes-mcp-server/issues/1661)). The tool fills a YAML text template and passes it to the multi-document apply, so a `workload` of the form `<image>\n---\n<manifest>` also applies the extra manifest, and the call answers "VirtualMachine created successfully": 3 of 3 on v0.1.2 and on main. The bound on impact: the extra object is written with the server's own identity, so RBAC still decides what it can be and `denied_resources` still applies; nothing bypasses RBAC or grants a permission. What breaks is the tool's scope: with only `vm_create` enabled, an agent can still write any kind the identity may write. It is an argument-handling bug; upstream already describes tool filters as best-effort (pull request [1612](https://github.com/containers/kubernetes-mcp-server/pull/1612)).
- **K8S-02: `pods_run` is annotated non-destructive but overwrites** (issue [1662](https://github.com/containers/kubernetes-mcp-server/issues/1662)). Given a port, it force-applies a Service named like the Pod. Over an existing Service with the same port, the type became ClusterIP, the nodePort was dropped and the selector was replaced whole: 3 of 3. Annotated `destructiveHint: false`, it is the only writing tool still served under `disable_destructive = true` (16 tools), and confirmation rules keyed on that hint do not match it. We did not test any client's confirmation flow.
- **K8S-14: misspelled tool names are accepted** (issue [1668](https://github.com/containers/kubernetes-mcp-server/issues/1668)). `enabled_tools = ["resources_gett"]` started a server with 0 tools; `disabled_tools = ["pods_deleet"]` served all 21. A misspelled toolset name stopped the server from starting, 3 of 3.

The other bugs, the design gaps and the intended behaviors are in the [findings register](https://isokron.ai/research/kubernetes-mcp-server-audit#findings-register) at the end.

## How the findings were checked

| Stage | Count |
| --- | --- |
| Candidates | 31: 26 from desk analysis of the pinned source, 5 from a first hand-driven usability pass |
| Frozen probes on v0.1.2, each with a control | 36 probe runs and 2 control runs, 3 or 10 repetitions per arm: 25 reproduced, 3 not reproduced, 3 measurements or controls |
| Re-run on upstream main 13bfb96f | 29 probe runs: 25 of 25 still reproduced, with the same per-arm counts |
| Refuters | Three agents in clean contexts challenged the 25: 10 held as written, 13 narrowed, 2 judged intended upstream, none refuted; one raised a new candidate, K8S-17N |
| Register | 26 items: 17 bugs, 7 design gaps, 2 intended. 25 from the frozen probes, plus K8S-17N |
| Reported upstream | 17 bugs in 15 issues (K8S-06 and K8S-15 share one) and one comment on issue 1584 (K8S-09) |
| Patches | 16 bug-fix patches for the 17 bugs, built as a series of 14 and a series of 2; 19 usability patches |
| Regression series | Every frozen experiment against the fixed and research builds, 60 runs, expected outcomes committed before the first run: every item as expected |
| Benchmark | 244 measured agent runs, in two task sets reported separately |

**K8S-17N has weaker validation than the rest.** A refuter found it while checking K8S-17: when RBAC refuses the release lookup, `helm_uninstall` answers "Release X not found" as a success. It was never in a frozen probe series or the regression series. It rests on the refuter's raw run and a reproduction outside the harness on 12 November, on v0.1.2 and on main; the fixed build and the Helm CLI returned the 403.

**The refuters.** The refuters saw the claims and source first, then our evidence and their own raw runs on the cluster. The model they ran on was not recorded. They limited K8S-25 to one argument and K8S-02 to a same-port Service, and reclassified K8S-04, K8S-05, K8S-10 and K8S-21 as design gaps, citing parity with `kubectl` or Helm's defaults.

**Regression and evidence.** "As expected" means each bug's tested cases stopped reproducing on both builds, and each design gap changed on the research build or, where the change is opt-in, stayed as it was. For 14 research-build runs the toolkit's audit-log check failed because the log had outgrown what it could read at once; a streamed check written afterwards covers all 60 runs and is labeled post-hoc. Each series was recorded on a hash-chained log and anchored off-box, the benchmark at its freeze and the five earlier series later, on 11 November, once the anchor key was available. None is yet closed into an Evidence Pack, so none can be verified independently today; we will publish the packs when they are closed.

## What held up

- **Optimistic concurrency where the server uses it.** `resources_scale` sends the `resourceVersion` it read: an injected conflict came back as an error with nothing changed (3 of 3), and real contention surfaced 10 of 10 times.
- **Unknown fields.** On Kubernetes 1.37, server-side apply refused misspelled fields (3 of 3) and the tool passed the error through. CRDs that preserve unknown fields were not tested.
- **Tekton cancel of a finished run.** The admission webhook refused it and the tool reported the refusal, 3 of 3.
- **RBAC.** No probe produced a write the server identity's RBAC did not allow; across the regression series it had 0 accepted writes outside the probe namespaces. This says nothing about tool filters, a separate, best-effort layer.
- **Agent reports.** Where a benchmark run failed, the agent usually said so: 4 of the 40 failed runs ended with a false "done".

## Usability pass: what the server costs an agent
On 11 November, Claude Opus 5.5 used the unmodified server as an ordinary MCP client on 12 everyday jobs: one plain request each, no hints and no mention of an audit. A checker read the cluster at the agent's answer and again 45 seconds later. A second session of seven targeted jobs exercised partial failures, bulk changes, many reads, a name collision and a concurrent edit. Together they cost $2.28.

| Session | Jobs passed at the answer | Tool calls | Model calls | Cost |
| --- | --- | --- | --- | --- |
| First, 12 everyday jobs | 9 of 12 (10 after 45 seconds) | 133 | 72 | $1.52 |
| Second, 7 targeted jobs | 5 of 7 | 76 | 31 | $0.76 |

Besides the Helm and concurrent-edit failures above, the clean-up job (delete every object with a label) took 60 tool calls and a quarter of the first session's spend: with no list across kinds and no delete by selector, the agent tried 33 kinds one by one. The debug-pod job ended with a premature "done", because the delete returned success while the pod was still terminating.

Of the 36,165 response tokens in the first session, 13,416 were data the jobs needed, 22,717 were echo (mostly write results returning the whole object, and list columns no job read), and 32 were a lookup of the available kinds. `managedFields` in write results alone was 6,373 tokens, 17.6% of everything the server returned.

**Figure 1: What the server returned in the first native session, by part (cl100k tokens)**

|  | Needed by the job | Echo, not needed | Lookup |
| --- | --- | --- | --- |
| 12 everyday jobs, 133 tool calls | 13416 | 22717 | 32 |

Source: the first native session’s response tokens, as split in the paragraph above.

The sessions produced a change list of 32 items: 29 built and 3 held back. Four of the 29 are bug fixes already in the register; the other 25 went into the 19 usability patches: results that say what happened (gone, pending on a finalizer, or terminating), batch and multi-get operations, `resources_patch` and a create-only mode, bounded waits instead of polling, trimmed write results, and better descriptions, annotations and argument checks.

## Benchmark: v0.1.2 against the research build
On 12 November (UTC) we ran 244 measured agent runs against v0.1.2 and the research build: v0.1.2 plus the 16 bug-fix patches and the 19 usability patches, with 25 tools against 21. Every run is counted: 204 passed, 40 failed, none inconclusive or stopped by the 40-turn limit. Correctness is a checker reading the cluster after the run, never the agent's account. Runs went one at a time in a seeded shuffled order, each in a fresh namespace; they cost 18.88 USD.

First, the limits. Probe counts such as 3 of 3 show that a behavior occurs under the stated conditions, not how often. The benchmark runs each task 5 times per build, which shows spread, not a rate; no significance test is claimed. There was no bug-fixes-only arm. "Cost per verified success" is the total cost of all runs divided by the runs that passed.

### Set A: tasks committed before the changes (K1 to K6)

Scale and verify; change one ConfigMap key; apply a three-object manifest; roll back a bad image; label six of nine Deployments; delete every object with a label, one held by a finalizer for 20 seconds. The prompts were committed to git at 00:06 UTC on 11 November, before any change was designed (the first change list was committed at 01:48 UTC), and not edited since. They were frozen on the evidence chain only with the benchmark on 12 November, and K3's manifest was written then from the draft's description of its shape.

Opus 5.5, 30 runs per build:

| Measure | v0.1.2 | Research build | Change |
| --- | --- | --- | --- |
| Verified completion | 30 of 30 | 30 of 30 | none |
| Cost per verified success, USD | 0.0975 | 0.0614 | −37.0% |
| Tokens per verified success | 66,796 | 64,574 | −3.3% |
| Tool calls per run, mean | 9.73 | 3.97 | −59.2% |
| Model turns per run, mean | 4.20 | 3.33 | −20.6% |
| Failed tool calls, total | 11 | 0 | |
| Totals: tool calls; cost, USD | 292; 2.9250 | 119; 1.8424 | |

With 30 of 30 on both builds, the Wilson 95% interval for completion is 0.89 to 1.00 on each.

**The saving is concentrated.** K6, the selective clean-up, accounts for 145 of the 173 fewer tool calls and 59.8% of the cost saving; K3, K5 and K6 together for 93.4%. Without K6, cost per verified success falls from 0.0695 to 0.0521 USD (−25.1%). On the single-object tasks K1 and K2 the research build used more tokens per success (+43.7% and +44.0%): the bigger catalog, with nothing to save.

| Task | Tool calls, median [min–max] | Cost per run, median, USD |
| --- | --- | --- |
| K1 Scale and verify | 2 [2–3] → 2 [2–2] | 0.0340 → 0.0352 |
| K2 One ConfigMap key | 2 [2–2] → 2 [2–2] | 0.0395 → 0.0415 |
| K3 Apply a manifest | 5 [4–5] → 1 [1–1] | 0.0606 → 0.0385 |
| K4 Roll back a bad image | 5 [4–6] → 4 [4–5] | 0.0953 → 0.0720 |
| K5 Label six of nine | 7 [7–8] → 7 [3–7] | 0.1106 → 0.0637 |
| K6 Selective clean-up | 36 [34–42] → 8 [8–9] | 0.2224 → 0.1093 |

**Why tokens barely move while cost falls.** The research catalog is 10,499 cl100k tokens against 6,807, 54% more, and the first turn's context grew from about 11.8 thousand to 17.5 thousand provider tokens. The catalog is re-read from the prompt cache every turn, and cache reads are the cheapest class: for Opus, at the rates implied by the reported costs, 0.20 USD per million against 5.00 for a cache write and 20.00 for output. The research build saves output and cache writes and spends cache reads, so the cost result depends on this provider's pricing; the tool-call result does not.

**Sonnet and the observer arm.** Sonnet 5.5 also completed 30 of 30 on both builds; tool calls fell from 7.40 to 3.70 per run and cost per verified success by 29.3%, while tokens per success rose 4.4%. With the server started directly instead of behind our capture proxy (24 runs), every run passed and the research build was still 29% cheaper per run.

**Where the change comes from.** An accounting model over the transcripts, not an ablation, splits the change by type ([appendix](https://isokron.ai/research/kubernetes-mcp-server-audit#where-the-change-comes-from)). On set A the bug fixes contribute nothing, because every task already passed on v0.1.2. The larger catalog adds 28.7% of the baseline's tokens per success but 3.9% of its cost. Of the 37.0% cost saving, 19.1 points are not attributed to any type, mostly shorter final answers and cache state carried between runs.

### Set B: tasks from the usability sessions (secondary; favors the research build by construction)

Set B is nine native session jobs with their prompts unchanged, plus F1b, a twin of K3 with `--- # comment` separators and one invalid Deployment. They were chosen after the changes were designed, from the sessions that produced them, so set B **favors the research build by construction**. Opus 5.5, 50 runs per build. The recorded verdicts are primary; the last column corrects our own F2 checker after the runs and is labeled post-hoc.

| Measure | v0.1.2 | Research, as recorded | Research, F2 corrected (post-hoc) |
| --- | --- | --- | --- |
| Verified completion | 25 of 50 | 35 of 50 | 39 of 50 |
| Completion, Wilson 95% interval | 0.50 [0.37–0.63] | 0.70 [0.56–0.81] | 0.78 [0.65–0.87] |
| Cost per verified success, USD | 0.2284 | 0.1250 | 0.1122 |
| Tokens per verified success | 148,584 | 130,781 | 117,368 |
| Tool calls per run, mean | 13.16 | 5.30 | 5.30 |
| False "done" claims | 2 | 2 | 0 |

As recorded, completion rose by 20 percentage points (25/50 → 35/50) and cost per verified success fell 45.3%; with the post-hoc correction, by 28 percentage points (25/50 → 39/50) and 50.9%.

**Figure 2: Set B tasks that changed or failed, runs passing out of 5 (Opus 5.5)**

|  | v0.1.2 | Research build |
| --- | --- | --- |
| F3 Helm chart into a namespace | 0 | 5 |
| F9 Concurrent edit | 0 | 5 |
| F2 Debug pod, as recorded | 0 | 0 |
| F2 Debug pod, post-hoc | 0 | 4 |
| F4 Clean-up with a finalizer-held object | 0 | 0 |
| F8 Name collision | 0 | 0 |

Source: the set B per-task results in the text; F2 shown as recorded and with our post-hoc checker correction.

- **F3, Helm into a namespace: 0 of 5 → 5 of 5.** The K8S-20 fix, the only set B gain the model credits to a bug fix. All five baseline agents reported the failure.
- **F9, concurrent edit: 0 of 5 → 5 of 5.** Baseline agents lost the concurrent change every time; research agents used `resources_patch`.
- **F2, debug pod: our checker was wrong.** A criterion we added before the runs, "the server created a Pod", counted only a `create`, but the server creates pods with a server-side apply PATCH; our pre-check made its pod with `kubectl run` and missed it. Every F2 run is recorded as failed. Re-scored from each run's audit log with every other criterion as recorded, F2 is 0 of 5 on v0.1.2 and 4 of 5 on the research build, whose delete can wait until the pod is gone.
- **F4 and F8 failed on both builds:** no agent removed the test finalizer, and agents asked the user rather than write into another team's ConfigMap. These are task-design questions, not server defects.
- Research agents used more tokens per run at the median (99,300 against 74,096): the bigger catalog, and tasks finished that baseline agents gave up on.

### Benchmark limits
- Five runs per task and build; repetitions measure spread within one prompt, not variety across Kubernetes work.
- Set A was committed to git before the changes but frozen on the evidence chain only with the benchmark. Set B favors the research build by construction, and F2's corrected view is post-hoc.
- The research build is measured as a whole, with no bug-fixes-only arm; the by-type split is an accounting model with a large unattributed share.
- Prompt caching carries across runs; the shuffled order exposes both builds equally. Compaction was not tested.
- One client, two models, one cluster, agents running as the operator's own user on macOS. Other clients and pricing will give other numbers.

## Upstream status, 12 November 2026
All 17 bugs were reported on 12 November 2026 in `containers/kubernetes-mcp-server`, as issues [1658](https://github.com/containers/kubernetes-mcp-server/issues/1658) to [1672](https://github.com/containers/kubernetes-mcp-server/issues/1672) and one [comment on issue 1584](https://github.com/containers/kubernetes-mcp-server/issues/1584), which has the same root cause as K8S-09. Checked on 12 November: all 15 issues were open, with no comments or labels yet, which is to be expected on the day of filing. Each issue was searched for duplicates first, credits related upstream work, and offers a patch with tests. Upstream asks for a DCO sign-off on every commit, so each pull request below carries one. The issues without a pull request ([1660](https://github.com/containers/kubernetes-mcp-server/issues/1660), [1661](https://github.com/containers/kubernetes-mcp-server/issues/1661), [1662](https://github.com/containers/kubernetes-mcp-server/issues/1662), [1663](https://github.com/containers/kubernetes-mcp-server/issues/1663), [1668](https://github.com/containers/kubernetes-mcp-server/issues/1668) and the comment on 1584) are held back until the maintainers agree on the approach.

Fix pull requests, opened on 12 November 2026:

- [Pull request 1675](https://github.com/containers/kubernetes-mcp-server/pull/1675) for issue 1658: open
- [Pull request 1676](https://github.com/containers/kubernetes-mcp-server/pull/1676) for issue 1659: open
- [Pull request 1677](https://github.com/containers/kubernetes-mcp-server/pull/1677) for issue 1664: open
- [Pull request 1678](https://github.com/containers/kubernetes-mcp-server/pull/1678) for issue 1665: open
- [Pull request 1679](https://github.com/containers/kubernetes-mcp-server/pull/1679) for issue 1666: open
- [Pull request 1680](https://github.com/containers/kubernetes-mcp-server/pull/1680) for issue 1667: open
- [Pull request 1681](https://github.com/containers/kubernetes-mcp-server/pull/1681) for issue 1669: open
- [Pull request 1682](https://github.com/containers/kubernetes-mcp-server/pull/1682) for issue 1670: open
- [Pull request 1683](https://github.com/containers/kubernetes-mcp-server/pull/1683) for issue 1671: open
- [Pull request 1684](https://github.com/containers/kubernetes-mcp-server/pull/1684) for issue 1672: open

The bug-fix patches apply to main as of 13bfb96f; one (K8S-14) needed a fix-up for main's new `opt_in_tools` option. On 12 November main had moved to ec37a146; none of its four new commits touched a file the issues cite. Design gaps were not filed, and neither was the change to forced apply. We will record maintainer responses and fixes in the changelog below.

## Rebuild the builds
The patch series and the research build are published in the experiment repository [isokron-ai/kubernetes-mcp-writepath-experiment](https://github.com/isokron-ai/kubernetes-mcp-writepath-experiment). The baseline is v0.1.2 built from source (`931b5f5a…`); the fixed build (`bac15e14…`) adds the 16 bug-fix patches, and the research build (`167a5a65…`) adds the 19 usability patches. The fixed and research builds each gave the same binary hash when built twice, and upstream's test suite (`go test -race ./...`) passes on all three. The patch series add 20, 4 and 41 test methods. The research build is an experiment, not an upstream release, and changes behavior users rely on, such as forced apply; use upstream's server for real work.

## Limitations

- One cluster distribution (kind), one Kubernetes version (1.37.0; no claim for later releases), one node. Not tested: OpenShift Route code paths, running VirtualMachines (KubeVirt findings rest on the CRDs), the kiali, kcp, netobserv and observability toolsets, the HTTP transport, and load.
- Fault injection served the probes only; no agent met an injected API fault or retried after a timeout.
- K8S-20's silent case, where the install reports success, follows from the code but was not run.
- K8S-17N was never in a frozen series; 14 regression runs rely on a post-hoc audit-log check.
- The findings were found with AI assistance, the refuters' model is not recorded, and the run records are not yet closed into verifiable Evidence Packs. The benchmark's limits are under [Benchmark limits](https://isokron.ai/research/kubernetes-mcp-server-audit#benchmark-limits).

## About this research

IsoKron performs fixed-price MCP server audits with this method: probes against a pinned build with raw-API controls, a native agent usability pass, clean-context refutation, re-checks against upstream main, tested patches and filing upstream; the before/after benchmark is an optional, quoted add-on. Details: [MCP server audits](https://isokron.ai/mcp-audit).

## Appendix: findings register
K8S-nn is a register item; K8S-17N is the candidate a refuter raised while checking K8S-17. K1 to K6 and F1a to F9 are benchmark tasks. Severity is our judgment of operational impact, not a security rating; where a refuter narrowed a finding, the wording is the narrowed one. "Main" means the same probe reproduced on main 13bfb96f.

**17 bugs, reported upstream on 12 November 2026:**

| ID | What v0.1.2 does | Severity | Evidence | Upstream |
| --- | --- | --- | --- | --- |
| K8S-20 | `helm_install` creates chart objects in the kubeconfig namespace, not the `namespace` argument | Medium | Probe 3 of 3; Helm CLI control; main | [1658](https://github.com/containers/kubernetes-mcp-server/issues/1658) |
| K8S-06, K8S-15 | Separators `--- ` and `--- # c` drop later documents with success; a trailing `---` fails with `EOF` | Medium | Probe 3 of 3 per shape; `kubectl` control | [1659](https://github.com/containers/kubernetes-mcp-server/issues/1659) |
| K8S-01 | A multi-document apply stops at the first refusal and does not say what was applied | Medium | Probe 3 of 3, four failure shapes; `kubectl` control | [1660](https://github.com/containers/kubernetes-mcp-server/issues/1660) |
| K8S-25 | `vm_create` renders `workload` unescaped; a value with a newline and `---` applies extra documents, within the identity's RBAC | Medium (argument handling) | Probe 3 of 3; quoted-scalar control; main | [1661](https://github.com/containers/kubernetes-mcp-server/issues/1661) |
| K8S-02 | `pods_run` is annotated non-destructive but force-applies over an existing same-port Service | Medium (annotation) | Probe 3 of 3; non-forced apply 409 | [1662](https://github.com/containers/kubernetes-mcp-server/issues/1662) |
| K8S-08 | `vm_create` over an existing VM name overwrites it and answers "created successfully" | Medium | Probe 3 of 3; create returns 409 | [1663](https://github.com/containers/kubernetes-mcp-server/issues/1663) |
| K8S-19 | `pods_exec` drops stdout and stderr when the command exits non-zero | Medium (usability) | Probe 3 of 3; `kubectl exec` shows both | [1664](https://github.com/containers/kubernetes-mcp-server/issues/1664) |
| K8S-03 | `pods_delete` reports success when deleting its Service is refused | Low to medium | Probe 3 of 3; injected fault 3 of 3 | [1665](https://github.com/containers/kubernetes-mcp-server/issues/1665) |
| K8S-17N | `helm_uninstall` answers "Release X not found" with success when RBAC refuses the lookup | Low to medium | Refuter's raw run; reproduced outside the harness on v0.1.2 and main; no frozen probe | [1666](https://github.com/containers/kubernetes-mcp-server/issues/1666) |
| K8S-09 | Wrongly typed `params` (Tekton) and `networks` (`vm_create`) are dropped silently | Low | Probe 3 of 3 each | [Comment on 1584](https://github.com/containers/kubernetes-mcp-server/issues/1584) |
| K8S-11 | `pods_run`: a negative port skips the Service; ports of 2^31 and above wrap | Low | Probe 3 of 3; the API refuses both | [1667](https://github.com/containers/kubernetes-mcp-server/issues/1667) |
| K8S-14 | Misspelled names in `enabled_tools` and `disabled_tools` are accepted silently | Low (usability) | 3 server starts per case | [1668](https://github.com/containers/kubernetes-mcp-server/issues/1668) |
| K8S-07 | Write results include `managedFields`; reads strip them | Low (usability) | Probe 3 of 3: 766 of 2,194 bytes | [1669](https://github.com/containers/kubernetes-mcp-server/issues/1669) |
| K8S-12 | `vm_clone` is annotated idempotent, but every call creates a new clone | Low (annotation) | Probe 3 of 3 | [1670](https://github.com/containers/kubernetes-mcp-server/issues/1670) |
| K8S-23 | Namespaced lists make a SelfSubjectAccessReview even when a namespace is given | Low (efficiency) | API audit log, 3 of 3 | [1671](https://github.com/containers/kubernetes-mcp-server/issues/1671) |
| K8S-24 | `events_list` prints timestamps in the server's local time zone, not RFC 3339 UTC | Low | Probe 3 of 3 | [1672](https://github.com/containers/kubernetes-mcp-server/issues/1672) |

Sixteen rows, seventeen bugs: K8S-06 and K8S-15 share an issue and a patch. K8S-09 and K8S-19 also have a design-gap half that is not in the issue: ignored unknown arguments (the `dryRun: true` case), and stderr hidden behind non-empty stdout, discussed upstream in pull request 1174.

**7 design gaps, not filed** (as in our earlier audits, issues are for bugs only):

| ID | Behavior | Evidence | Related upstream |
| --- | --- | --- | --- |
| K8S-04 | `resources_delete` sends no uid or `resourceVersion` precondition, so it deleted an object recreated after the agent's read | 10 of 10; `kubectl` has no uid precondition either | — |
| K8S-05 | `helm_install` waits 5 minutes with no rollback option; a failed release blocks the name; a client timeout hides the outcome | 3 of 3; 10 of 10 calls hit a 60-second client limit | [1334](https://github.com/containers/kubernetes-mcp-server/pull/1334) |
| K8S-10 | `resources_delete` answers "deleted successfully" while a finalizer holds the object | 3 of 3 | — |
| K8S-13 | `vm_lifecycle` start and restart overwrite the VM's run strategy | 3 of 3 each | [1655](https://github.com/containers/kubernetes-mcp-server/pull/1655), [1012](https://github.com/containers/kubernetes-mcp-server/pull/1012) |
| K8S-16 | Tekton restart copies only `spec`; labels set on the run are lost | 3 of 3 | — |
| K8S-21 | After a failed uninstall the release stays `uninstalling`, and `helm_list` shows nothing (Helm's default filter, passed through) | 3 of 3; the Helm CLI behaves the same | — |
| K8S-22 | Deleting a Job orphans its pods: no propagation policy is sent, and the API's default for Jobs orphans | 3 of 3; with `Background` the pod is removed | — |

**2 intended by upstream:** K8S-17 (`helm_uninstall` of a release that does not exist returns success, asserted by an upstream test; the RBAC-refused case is the bug K8S-17N) and K8S-18 (read tools return the object as text and as `structuredContent`, per pull request 1232 and the MCP specification).

### Where the change comes from
Each tool call in the transcripts gets one role (recovery after a failed call, wait or poll, batch, multi-get, edit, or other), and its share of the run's tokens and cost follows from where it sits in the conversation; result tokens are estimated as characters divided by 3. Only the combined build was measured, so this is an accounting model, not an ablation. Each share is the type's change as a percentage of the baseline value, so the rows add up to the total; negative is a saving. Set B is shown as recorded.

| Type of change | Set A tokens | Set A cost | Set B tokens | Set B cost |
| --- | --- | --- | --- | --- |
| Bug fixes (completion) | +0.0% | +0.0% | −14.3% | −14.3% |
| Results that say what happened | −4.5% | −4.7% | −20.5% | −22.7% |
| Batch and multi-get | −2.0% | −9.2% | +2.5% | −12.8% |
| Fewer edit round trips | −5.7% | −4.2% | −0.0% | −0.2% |
| Waits instead of polling | −1.3% | −5.5% | −3.1% | −5.1% |
| Smaller results | +1.2% | +1.7% | +1.0% | +1.7% |
| Larger catalog | +28.7% | +3.9% | +25.4% | +3.3% |
| Not attributed | −19.6% | −19.1% | −3.0% | +4.8% |
| **Total** | **−3.3%** | **−37.0%** | **−12.0%** | **−45.3%** |

"Smaller results" is slightly positive because per call the research build's results are a little larger; the echo saving shows up as fewer calls, booked under batch and edit.

## Changelog

- **12 November 2026:** published.

---

HTML version: https://isokron.ai/research/kubernetes-mcp-server-audit

**How this was tested.** AI agents run much of the testing: scripted checks, attempts to disprove each finding, and real tasks that use the server the way an agent would. I also test every server myself on a separate machine and check each finding against the AI results. A finding appears here only if it holds up in both. — [Joshua Bryan](https://isokron.ai/about)

For this piece: Findings were found with AI assistance and reproduced in scripted probe runs with no model in the loop, each against a control sent straight to the Kubernetes API or through the kubectl or Helm CLI. Three refuter agents in clean contexts challenged the 25 reproduced items; the model they ran on was not recorded. The 25 were re-run on upstream main. Benchmark correctness is a mechanical checker’s verdict on the cluster, never a model’s; the totals were recomputed from the per-run table.
