Found a duplicate-execution bug in an MCP tool via static analysis (no LLM)

mbelckadi1 pts0 comments

Case Study: NEXUM-004 IdempotencyMissing on a real MCP spec — Nexum

CASE STUDY<br>May 17, 2026

NEXUM-004 on a real spec: how a public MCP storefront closed an IdempotencyMissing finding in a day

This is not a demo spec. Fabian Williams (@fabianwilliams) built a public, MCP-callable storefront — a real purchase flow, live on fabswill.com. When Nexum's rules were walked against his spec by hand, one of them failed: NEXUM-004. Here's what happened, in his own words and his own pull request.

The spec

Fabian's storefront serves one MCP-callable endpoint that produces an identical, protocol-neutral<br>audit-trail receipt regardless of caller — he tested it against both hosted Claude Desktop and a<br>fully-local Qwen3.6 27B running offline on his laptop, same six runtime checks, same result. He<br>posted it as a thread on X<br>on May 16, 2026.

Mehdi replied the next day: the runtime checks looked solid, but what did the spec look like<br>before it reached the client? The receipt-URL pattern — one endpoint satisfying both<br>the security audit and the finance billing view — was interesting from a blast-radius angle: an<br>endpoint that serves two consumers is also one endpoint an agent can call for both.

Fabian took the offer. The Nexum scanner binary wasn't public yet, so he walked his<br>purchase_free_bundle spec against the manifest's five enumerated rules by hand.<br>Four passed or didn't apply. One failed.

The finding — NEXUM-004 IdempotencyMissing

HIGH<br>purchase_free_bundle is a mutating MCP tool: it mints a receipt, upserts a contact in<br>Brevo, mints a JOSE-signed download token, and dispatches a fulfillment email. The tool's<br>inputSchema exposed no Idempotency-Key. Any agent that retries on<br>timeout — the default retry behavior in most LLM-SDK client policies — would double-issue the<br>bundle and double-fire the email.

Why the runtime guards didn't catch it

Fabian's storefront already had runtime governance in place: a 5-requests-per-hour rate limit per<br>IP, and a $5/day cost ceiling. Neither one fires on this bug. Two retries inside the same hour<br>both land comfortably under the rate-limit cap on first try. A duplicate free-bundle issue costs<br>nowhere near the daily ceiling. As Fabian put it, that's exactly the defense-in-depth gap Nexum's<br>framing predicts: runtime guards and static spec review catch different classes of failure. A<br>spec-level check on the schema itself catches what request-volume and cost-based guards<br>structurally cannot see.

The fix — PR #1

Fabian shipped the fix the same day, merged May 17, 2026:

Schema. Added idempotency_key (optional, 8–128 chars, pattern [A-Za-z0-9._-]) to the MCP tool's inputSchema, with the tool description updated to recommend it for production agents.

HTTP header. Both /api/a2a/mcp and /api/a2a/purchase also accept a standard Idempotency-Key request header — the tool argument wins if both are present.

Storage. A new idempotency.ts module backed by Azure Table Storage. PartitionKey and RowKey are hashed values — no raw key or email is stored.

Replay semantics, Stripe-style. On every request with a valid key, the table is checked before input validation runs. On a hit, the original receipt is loaded from Blob storage and returned exactly as-is.

Only successes are cached. Failure receipts are never stored, so a transient downstream failure (a Brevo blip, for example) doesn't get poisoned by a cached error — the agent's retry re-runs the flow as intended.

Fail open. A missing cache hit or a failed write are both swallowed rather than surfaced as errors. Idempotency is a safety belt here, not a new failure mode.

Known v1 limitations

Fabian documented these in the PR itself, rather than glossing over them:

Concurrent requests with the same key can both miss the cache and both run the flow — last-write-wins on the table, and two receipts get written to Blob. A Stripe-style in-progress lock (set state to processing under optimistic concurrency, return 409 on collision) is the planned v2 hardening.

No table TTL yet — mappings live until manually reaped. Negligible cost at demo volume; flagged for revisit past 10k+ rows.

Fabian Williams<br>@fabianwilliams

“Credit where it is due @MBelckadi — your trust-manifest is a clean static-analysis schema.”

May 17, 2026

Why this matters

Runtime guards — rate limits, cost ceilings — govern behavior: how often and how much.<br>They say nothing about whether a single call, executed exactly once as designed, is safe to retry.<br>A static read of the spec catches that class of risk before the first agent ever calls the tool —<br>defense-in-depth on top of runtime governance, not a replacement for it. That's the whole premise<br>behind the Nexum Cert.

Scan your API

Is your MCP server agent-safe?

Upload your OpenAPI spec and get a Nexum Cert + PDF report in seconds.<br>Free. No account required.

Scan your API free →<br>View the registry

spec nexum fabian tool runtime agent

Related Articles