Skip to content
IsoKron

Research / Public audit · Write correctness

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

Test setup
Protocol
stdio; the server answered protocol revision 2025-11-25 in our catalog sweeps (not recorded for the agent runs)
Software
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

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, as of 12 November 2026
15 issues filed on 12 November, 1658 to 1672, plus a comment on 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 to 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.
Terms used here
Control
The same change sent through the vendor’s own API, without the MCP server. If the control loses nothing, the MCP server causes the loss. If the control loses writes too, the platform behind it has the problem as well, and the MCP server can still add to it, for example by switching off a check the API offers.
Refuter
A separate AI agent, started in a clean context without the conversation that produced a finding, that tries to break it using the claim, the source code and the reproduction or its captures. It confirms the finding, narrows it to the conditions where it holds, or refutes it.
Tool annotation
A hint a tool declares about itself, such as destructiveHint. Clients use annotations for confirmation prompts; nothing enforces them.
Lost update
Two writers change the same object at nearly the same moment. Both are told the write succeeded, but one change is gone afterwards.
End-state check
A run is graded by reading what actually changed in the product afterwards, not by what the agent said it did.
Tool catalog (tools/list)
The list of tools, with names, descriptions and input schemas, that an MCP server offers to the agent.
Median [min–max]
The middle value of the runs, with the lowest and highest in brackets. With a few runs per build (three or four in these articles), non-overlapping ranges are a coarse screen, not a significance test.
cl100k tokens
Token counts from the cl100k tokenizer, used as a common baseline. It is not Claude’s tokenizer, so the figures are estimates of size.
Hash-chained log
A log in which each entry includes the hash of the one before, so a later edit to any entry shows.

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.

ItemDetail
Serverv0.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
TargetKubernetes 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
IdentitiesThe 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
ClientsProbes: 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.

Armv0.1.2main 13bfb96f
helm_install with namespace set, chart without a templated namespaceObjects sent to default; install failed with a 403 naming default; release record left failed in the requested namespace: 3 of 3Same: 3 of 3
Control: helm install -n, same identity and chartEverything created in the requested namespaceNot re-run (no server involved)
Control: a chart that templates its namespace, through the toolInstalled correctly: 3 of 33 of 3
Fixed buildNot 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. 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.

InputThrough the tool, v0.1.2Control
Separator --- (trailing space)Success; only the first document applied: 3 of 3kubectl applied both; the second document alone applied: 3 of 3
Separator --- # secondSuccess; only the first document applied: 3 of 3kubectl applied both
Trailing --- followed by a newlineWhole call failed with a bare EOF: 3 of 3Each document alone applied: 3 of 3
A comment-only document in the middleFirst applied, then `no matches for kind ""`: 3 of 3kubectl 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 shapeskubectl apply --server-side applied the first and third and reported the second
  • K8S-06 and K8S-15 (issue 1659) share one patch: split as kubectl’s YAML reader does, and skip empty and comment-only documents.
  • K8S-01 (issue 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); it has come up again in issues 445 and 1164 and pull request 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.

ArmResult
A full copy re-applied through the tool after another writer changed a field, without resourceVersionThe other writer’s change lost: replicas 10 of 10, a ConfigMap key 10 of 10
The same copy with its stale resourceVersion keptRefused with 409 in 10 of 10
Control: the same body without force, straight to the API409 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, 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). 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).
  • K8S-02: pods_run is annotated non-destructive but overwrites (issue 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). 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 at the end.

How the findings were checked

StageCount
Candidates31: 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 control36 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 13bfb96f29 probe runs: 25 of 25 still reproduced, with the same per-arm counts
RefutersThree 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
Register26 items: 17 bugs, 7 design gaps, 2 intended. 25 from the frozen probes, plus K8S-17N
Reported upstream17 bugs in 15 issues (K8S-06 and K8S-15 share one) and one comment on issue 1584 (K8S-09)
Patches16 bug-fix patches for the 17 bugs, built as a series of 14 and a series of 2; 19 usability patches
Regression seriesEvery frozen experiment against the fixed and research builds, 60 runs, expected outcomes committed before the first run: every item as expected
Benchmark244 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.

SessionJobs passed at the answerTool callsModel callsCost
First, 12 everyday jobs9 of 12 (10 after 45 seconds)13372$1.52
Second, 7 targeted jobs5 of 77631$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)

12 everyday jobs, 133 tool calls

13416 needed by the job, 22717 echo, not needed, 32 lookup (36165)

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:

Measurev0.1.2Research buildChange
Verified completion30 of 3030 of 30none
Cost per verified success, USD0.09750.0614−37.0%
Tokens per verified success66,79664,574−3.3%
Tool calls per run, mean9.733.97−59.2%
Model turns per run, mean4.203.33−20.6%
Failed tool calls, total110None
Totals: tool calls; cost, USD292; 2.9250119; 1.8424None

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.

TaskTool calls, median [min–max]Cost per run, median, USD
K1 Scale and verify2 [2–3] → 2 [2–2]0.0340 → 0.0352
K2 One ConfigMap key2 [2–2] → 2 [2–2]0.0395 → 0.0415
K3 Apply a manifest5 [4–5] → 1 [1–1]0.0606 → 0.0385
K4 Roll back a bad image5 [4–6] → 4 [4–5]0.0953 → 0.0720
K5 Label six of nine7 [7–8] → 7 [3–7]0.1106 → 0.0637
K6 Selective clean-up36 [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). 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.

Measurev0.1.2Research, as recordedResearch, F2 corrected (post-hoc)
Verified completion25 of 5035 of 5039 of 50
Completion, Wilson 95% interval0.50 [0.37–0.63]0.70 [0.56–0.81]0.78 [0.65–0.87]
Cost per verified success, USD0.22840.12500.1122
Tokens per verified success148,584130,781117,368
Tool calls per run, mean13.165.305.30
False “done” claims220

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)

F3 Helm chart into a namespace

v0.1.20
Research build5

F9 Concurrent edit

v0.1.20
Research build5

F2 Debug pod, as recorded

v0.1.20
Research build0

F2 Debug pod, post-hoc

v0.1.20
Research build4

F4 Clean-up with a finalizer-held object

v0.1.20
Research build0

F8 Name collision

v0.1.20
Research build0

Each row is drawn to its own scale.

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 to 1672 and one comment on issue 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, 1661, 1662, 1663, 1668 and the comment on 1584) are held back until the maintainers agree on the approach.

Fix pull requests, opened on 12 November 2026:

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. 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.

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.

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:

IDWhat v0.1.2 doesSeverityEvidenceUpstream
K8S-20helm_install creates chart objects in the kubeconfig namespace, not the namespace argumentMediumProbe 3 of 3; Helm CLI control; main1658
K8S-06, K8S-15Separators --- and --- # c drop later documents with success; a trailing --- fails with EOFMediumProbe 3 of 3 per shape; kubectl control1659
K8S-01A multi-document apply stops at the first refusal and does not say what was appliedMediumProbe 3 of 3, four failure shapes; kubectl control1660
K8S-25vm_create renders workload unescaped; a value with a newline and --- applies extra documents, within the identity’s RBACMedium (argument handling)Probe 3 of 3; quoted-scalar control; main1661
K8S-02pods_run is annotated non-destructive but force-applies over an existing same-port ServiceMedium (annotation)Probe 3 of 3; non-forced apply 4091662
K8S-08vm_create over an existing VM name overwrites it and answers “created successfully”MediumProbe 3 of 3; create returns 4091663
K8S-19pods_exec drops stdout and stderr when the command exits non-zeroMedium (usability)Probe 3 of 3; kubectl exec shows both1664
K8S-03pods_delete reports success when deleting its Service is refusedLow to mediumProbe 3 of 3; injected fault 3 of 31665
K8S-17Nhelm_uninstall answers “Release X not found” with success when RBAC refuses the lookupLow to mediumRefuter’s raw run; reproduced outside the harness on v0.1.2 and main; no frozen probe1666
K8S-09Wrongly typed params (Tekton) and networks (vm_create) are dropped silentlyLowProbe 3 of 3 eachComment on 1584
K8S-11pods_run: a negative port skips the Service; ports of 2^31 and above wrapLowProbe 3 of 3; the API refuses both1667
K8S-14Misspelled names in enabled_tools and disabled_tools are accepted silentlyLow (usability)3 server starts per case1668
K8S-07Write results include managedFields; reads strip themLow (usability)Probe 3 of 3: 766 of 2,194 bytes1669
K8S-12vm_clone is annotated idempotent, but every call creates a new cloneLow (annotation)Probe 3 of 31670
K8S-23Namespaced lists make a SelfSubjectAccessReview even when a namespace is givenLow (efficiency)API audit log, 3 of 31671
K8S-24events_list prints timestamps in the server’s local time zone, not RFC 3339 UTCLowProbe 3 of 31672

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):

IDBehaviorEvidenceRelated upstream
K8S-04resources_delete sends no uid or resourceVersion precondition, so it deleted an object recreated after the agent’s read10 of 10; kubectl has no uid precondition either—
K8S-05helm_install waits 5 minutes with no rollback option; a failed release blocks the name; a client timeout hides the outcome3 of 3; 10 of 10 calls hit a 60-second client limit1334
K8S-10resources_delete answers “deleted successfully” while a finalizer holds the object3 of 3—
K8S-13vm_lifecycle start and restart overwrite the VM’s run strategy3 of 3 each1655, 1012
K8S-16Tekton restart copies only spec; labels set on the run are lost3 of 3—
K8S-21After 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-22Deleting a Job orphans its pods: no propagation policy is sent, and the API’s default for Jobs orphans3 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 changeSet A tokensSet A costSet B tokensSet 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.

Get new research by email

New MCP audits and research, about twice a month, plus occasional IsoKron service news. Unsubscribe anytime.

Sent with Kit. See our privacy page.

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

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.

View as Markdown (for agents and scripts).