Skip to content
IsoKron

Research / Study · Write correctness

Lost updates in four MCP servers: GitHub, Grafana, PostHog and Kubernetes

Terms used here
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.
Pair (concurrent pair)
Two calls released together against one object. A pair counts as lost when both calls returned success and one of the two changes was missing afterwards.
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.
Read-merge-write
Read the whole object, change one part of it, write the whole object back. Without a version check, a change someone else made in between is overwritten.
Version check
A save carries the version it was based on, and the server rejects it if someone saved in between, instead of overwriting their change.

Self-funded research by IsoKron, conducted and written with AI assistance. Not commissioned or endorsed by GitHub, Grafana Labs, PostHog, the maintainers of kubernetes-mcp-server or Red Hat; IsoKron is not affiliated with any of them. IsoKron sells MCP server audits.

Updated 12 November 2026: the Kubernetes MCP server audit was added.

We audited the MCP servers that GitHub, Grafana and PostHog publish for their own products, and the open-source containers/kubernetes-mcp-server. In the first three, two writes sent at the same moment to the same object could lose one of the changes, and both callers were told their write had succeeded. In the Kubernetes server, a full copy of an object re-applied through the tool without its resourceVersion, after another writer had changed a field, set that field back. This page puts the four results side by side. Each row links to the full audit.

The reason to look at this class of bug now is that it was rare when one person used one client. It stops being rare when several agents work on the same backlog, dashboard or project at once.

The four results

ServerWhat was testedLost through the MCP serverThrough the platform’s own APIWhere the defect is
GitHub MCP server v2.1.0Two updates to different issue fields of one issue19 of 30 pairs0 of 28 pairs, using GitHub’s per-field updateIn the MCP tool
Grafana MCP server v2.2.0Two patches to different panels of one dashboard30 of 30 pairs, on Grafana 13 and on Grafana 11Legacy save endpoint: 30 of 30 on Grafana 13; 20 of 30 on Grafana 11, one save rejected in the other 10. Kubernetes-style API on Grafana 13: 0 of 30, one save rejected in every pairIn the MCP tool and in the legacy save endpoint
PostHog MCP serverTwo updates to different fields of one insight7 of 11 pairs, through the hosted server6 of 11 pairsIn PostHog’s API; the MCP server passes it through
Kubernetes MCP server v0.1.2A full copy re-applied through the tool after another writer changed a fieldWithout resourceVersion, the other writer’s change lost: replicas 10 of 10, a ConfigMap key 10 of 10. With the stale resourceVersion kept: refused with 409 in 10 of 10The same body without force, straight to the API: 409 conflict, 3 of 3Not filed as a bug: the tool applies with force: true, which upstream chose on purpose
Figure 1: Two simultaneous writes to one object, through three of the MCP servers and through each vendor’s own API

GitHub MCP server v2.1.0

19 of 30 lost · Through the MCP server

0 of 28 lost · GitHub’s per-field update

Grafana MCP server v2.2.0, on Grafana 13

30 of 30 lost · Through the MCP server

30 of 30 lost · Legacy save endpoint

0 of 30 lost, one call rejected in 30 · Kubernetes-style API

Grafana MCP server v2.2.0, on Grafana 11

30 of 30 lost · Through the MCP server

20 of 30 lost, one call rejected in 10 · Legacy save endpoint

PostHog MCP server

7 of 11 lost · Through the hosted MCP server

6 of 11 lost · PostHog’s API

Source: the table above, from the three linked audits.

A pair counts as lost when both calls returned success and one of the two changes was missing afterwards. All pairs were deliberately simultaneous. The figure covers the three servers tested with simultaneous pairs; the Kubernetes trials were a stale copy re-applied after another writer’s change, so they are in the table only. None of these numbers says how often this happens in normal use.

Four different causes

GitHub: the MCP tool rewrites more than it was asked to change. To set one issue field, the tool reads every field value, merges in the change and writes the whole set back, with no version check. Two agents changing different fields overwrite each other. GitHub’s own per-field update does not have the problem, which is why the control lost nothing. The MCP layer introduced the loss.

Grafana: the MCP tool switches off a check, and the endpoint behind it has a gap of its own. Patch mode reads the dashboard, applies the patch and saves the whole dashboard with overwrite forced on, so Grafana’s version check never runs. Grafana rejected a stale save in 10 of 10 trials when the check was allowed to run. But two saves that arrive at exactly the same moment through the legacy save endpoint were both accepted even with the check on. Grafana 13’s Kubernetes-style API did not have that gap: it rejected one of two simultaneous saves in 30 of 30 pairs.

PostHog: the loss is in the product’s API. The same two requests sent straight to PostHog’s API lost a write about as often as they did through the MCP server. The MCP server added nothing and removed nothing. An agent hits it more easily than a person does, because agents work in parallel.

Kubernetes: forced apply is 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). 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. Neither native session in that audit 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, the agent’s forced apply set it back to 100 four seconds later, and the agent told the user that the other keys were unchanged. Where the server does use optimistic concurrency, it held: 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. A related design gap, K8S-04, is that 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).

The distinction matters when you read an audit of any MCP server. “The MCP server loses writes” can mean the server is wrong, that it faithfully exposes a backend that is, or that it made a deliberate trade-off. The control against the platform’s own API is what tells them apart, and it should be in the report.

What happened next

ServerReportedStatus on 12 November 2026
GitHubIssue 3751Our fix is open as pull request 3781, with no maintainer review as of 12 November 2026. In our test of that approach a patched build lost 0 of 12 pairs
GrafanaPull request 1518Open; as of 12 November 2026 it was awaiting maintainer review, rebased onto the current main branch and mergeable. The proposed fix saves through the Kubernetes-style API where it exists and lost 0 of 60 pairs on Grafana 13. Grafana 11 has only the legacy endpoint; there the restored version check reports stale saves but still lost 14 of 30 simultaneous pairs
PostHogPull request 131519Fixed for insights. A maintainer re-created the change as pull request 131620, with IsoKron as co-author, and merged it about 17 hours after we opened ours. Cohorts and dashboards lost writes in the same tests and, as of 12 November 2026, are not yet fixed
KubernetesNot filedForced apply is a deliberate upstream decision, and K8S-04 is a design gap; neither was filed. 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

PostHog’s response is the one to copy: a small fix, reviewed and merged the next morning.

What this does not show

  • The counts are small: 11 to 30 pairs per cell for GitHub, Grafana and PostHog, with the two calls released together on purpose, and 10 trials per arm for Kubernetes (3 for the API control and for the injected scale conflict), on one machine, one cluster or one test account.
  • Real agents did task work through the server in all four audits. The lost-update counts did not come from those sessions: they come from scripted pairs and trials, so they show that a collision or a stale copy loses a write, not how often agents collide on their own.
  • Four servers is not a survey. We chose them because they are widely used and open source, not at random.
  • In the three simultaneous-pair cases the lost change can be put back by writing it again, and at Grafana it stays in the dashboard’s version history. The harm is that nothing tells the agent or the user that it needs to.
  • The findings were found with AI assistance and checked as each audit describes. We sell audits of this kind, so read the method sections with that in mind.

How to check a server you run

  1. Pick a tool that updates part of an object: one field, one panel, one tag.
  2. Send two calls that change different parts of the same object at the same moment, from two separate server processes. Repeat at least ten times.
  3. Read the object back. If both calls said success and one change is missing, you have a lost update.
  4. Repeat with the same two changes sent straight to the product’s API. If the API loses nothing, the MCP tool is the cause. If the API loses them too, the fix belongs in the API, and the tool should at least surface a conflict where the API offers one.

The full method, including the controls and the ways we got it wrong before getting it right, is in How to test an MCP server without fooling yourself.

Changelog

  • 10 November 2026: published.
  • 12 November 2026: status column dated 12 November; Grafana’s pull requests rebased and mergeable; a figure added.
  • 12 November 2026: the figure marks pairs in which one call was rejected with a conflict, instead of counting them as both changes kept; the Grafana 11 control row says one save was rejected in the other 10 pairs.
  • 12 November 2026: the Kubernetes MCP server audit added: a fourth row, its cause and its status. The title and address now cover four servers; the old address redirects here.

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: Every figure is taken from the four linked audits, which give the method, the controls and the limits for each.

View as Markdown (for agents and scripts).