Stops agents double-firing side effects: 1,000 racing duplicates, exactly one execution.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
π‘ Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
Your swarm doesn't need more memory. It needs a causal fence around tool side effects.
Run the attack yourself:
EffectFence is a causal concurrency fence for multi-agent tool calls. When more than one agent (or retry, or re-dispatch) can end up trying to run the same side-effecting operation β charge a card, send a payout, provision a resource β EffectFence guarantees exactly one attempt ever executes it: same-instant races are decided by an atomic compare-exchange reservation, and late duplicates get the recorded outcome replayed instead of running again. Every effect that does run gets a content-addressed certificate chained to whatever it was causally built on.
It ships as a Rust library (effectfence::fence) and as a stdio MCP server exposing three tools β fence_prepare, fence_commit, fence_abort β so agents can route side-effecting tool calls through the fence instead of racing each other directly.
In a multi-agent gateway, more than one caller can end up trying to run the same effect:
Naively, any of these double-runs the effect. Naively rejecting every duplicate with no memory of outcome is also wrong: if the first attempt crashed, the effect never runs at all, and a duplicate that arrives after success gets an error instead of the result it needs. EffectFence closes all of it with optimistic concurrency control (OCC) plus an intent ledger: attempts don't block each other, exactly one executes, and every other attempt learns what actually happened.
Four pieces compose into the fencing protocol:
The intent ledger is what stops duplicates, not just races. Every effect carries an intent β a stable id for the logical action (e.g. "charge:order-123"). Attempts sharing an intent are the same action: the first is admitted and holds a lease; concurrent duplicates are told an attempt is in flight; duplicates arriving after success get the recorded certificate replayed verbatim; duplicates after a failure are fenced (the side effect may or may not have fired β that must be reconciled, not blindly retried) until explicitly cleared. Crashed holders lose their lease after a TTL so the action isn't stuck forever.
Vector clocks (VectorClock) track causal "happened-before" relationships across agents β one logical counter per agent, joined via elementwise-max merge, compared via a le partial order, with a concurrent check for genuinely unordered events and a stable SHA-256 digest for inclusion in certificates.
OCC read-sets (ReadSetEntry) record the causal dependencies a decision was based on: "when I decided to act, domain D was at sequence S." Both prepare_effect_fence and commit_effect_cert validate every entry against live state β if anything moved, the attempt is rejected as stale rather than allowed to act on outdated information.
CAS domain fencing is where same-instant races are decided. Each domain (a named contention scope, e.g. "order:123") has an AtomicU64 sequence counter. The decision is a single atomic compare_exchange β exactly one concurrent caller can win for any given expected sequence. (Precision note: the counter lookup sits behind a short mutex; only the race decision itself is lock-free. Ideas for a fully lock-free path are welcome.)
Every committed effect becomes an EffectCert: a SHA-256 content hash over {intent, parent, domain, seq, tool, args, result, vector_clock, read_set, agent}, chained to a parent cert hash for causal lineage. Two certs with the same hash are, by definition, records of the same effect β EffectCert::verify() recomputes the hash and confirms it hasn't been tampered with or hand-built incorrectly.
This is an in-memory, single-process fence β state lives behind an Arc and is lost on restart. That's enough to close races and duplicates between concurrent threads/tasks in one gateway process. Two things it deliberately does not do (yet):
SETNX+CAS in Redis, or an optimistic version column in Postgres) β the types here are meant to carry over directly to that backend.Memory is bounded: finished outcomes expire after a configurable TTL (FenceConfig::result_ttl, swept by EffectFence::sweep), queries never create tracking state, and domain counters are tiny and manually evictable (evict_domain).
A concurrent duplicate of the same intent gets Err(FenceError::IntentInFlight); a same-instant race on the domain gets Err(FenceError::DomainRace); either way it must not run the effect.
Listed in the official MCP Registry as
mcp-name: io.github.aurumflux20/effectfence
With a Rust toolchain (rustup.rs):
Or build from a clone of this repo:
Claude Code (one command):
(If you built from source instead of cargo install, use the full path: claude mcp add effectfence -- /path/to/target/release/effectfence.)
Claude Desktop β add to claude_desktop_config.json:
Any project (team-shared) β commit a .mcp.json at the project root:
That's it β no configuration, no environment variables, no accounts. The server holds its fence state in memory for the life of the process.
It exposes:
fence_prepare β { intent, domain, tool, args, agent, read_set?, parent?, known_clock? } β {status: "fresh", prepared} when this attempt wins (run the tool, then report back), or {status: "already_done", cert} when this exact action already ran (use the recorded result β do NOT run the tool). Errors mean do not run.fence_commit β { prepared, result } β {status: "committed", cert}. Later duplicates of the intent now replay this cert.fence_abort β { prepared, reason } β {status: "aborted"}. The intent stays fenced until reconciled and cleared.Tool input schemas are generated automatically from the Rust types (via schemars), so any MCP client can introspect them with tools/list.
tests/chaos_test.rs uses real OS threads to prove the two guarantees separately: a forced same-instant domain race (synchronization deliberately constructed so the collision is guaranteed, not hoped for) admits exactly one winner every time, and 16 concurrent duplicates of one intent admit exactly one execution β with late duplicates replaying the committed cert. A 32-thread stress test additionally asserts sequence numbers are never double-allocated.
tools/fencescan.py finds tools in an MCP server that could fire the same effect
twice. No install, no dependencies, no network:
It reports candidates with evidence and deliberately renders no verdict, because an outsider reading a repository usually cannot prove a double-fire β the guard often lives in a service the repo calls, or in a sibling SDK, and a tool whose name sounds like a write may only return a payload for someone else to sign. Output includes an explicit list of what it cannot see.
It was rewritten after hand-verification killed 4 of its first 7 "confirmations". Each failure is now a fixed behaviour rather than a caveat:
| It got this wrong | Why | Now |
|---|---|---|
| Flagged read-only tools | A flat window after a tool name ran into the next tool, so reads inherited writes' vocabulary | Brace-matched to the tool's own block; a read verb in the name vetoes |
| Said a repo had no idempotency when it had a whole module | \b(idempotβ¦) cannot match deriveIdempotencyKey β no word boundary before a camelCase capital | Anchors removed; the same blindness hid requestId, clientToken |
| Found no writes anywhere | Writes live in shared helpers, not in the tool declaration | Collected per repo as corroboration, never claimed as "this tool writes" |
Missed method: cond ? "POST" : "GET" | String literals were stripped before matching, deleting the HTTP verb itself | Matched on the raw line |
| Printed "AT RISK" | That is an accusation, and it was wrong 4 times in 7 | No verdict field exists |
If it flags something in your server and you want a second pair of eyes, open an issue β a wrong accusation costs more than a missed one, so a false positive here is worth reporting too.
once (Python)Same problem, other runtime. once (pip install once-kernel) is the Python idempotency kernel built on the same idea: a side effect runs exactly once under retries, webhook redelivery, and concurrent workers. It goes further on durability β a Postgres store, heartbeat leases with fence tokens so a stale worker can't resurrect after its lease is reclaimed, and RFC 8785 canonical payload fingerprints.
Use EffectFence when your fence lives in Rust or in front of an MCP server; use once when the side effect is Python and you want a durable store. effectfence wrap has been proven fencing once's own MCP server.
The libraries are free and stay free. If you want help applying them to a codebase that already moves money β a fixed-scope audit of every side-effecting path, storm-tested, with a CI test that keeps it fenced β see SUPPORT.md or email hello@aurumflux.co.
If it isn't a fit we'll say so.
MIT β see LICENSE.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/effectfence)<a href="https://allmcps.com/mcp/effectfence"><img src="https://allmcps.com/api/badge/effectfence?style=directory" alt="EffectFence on AllMCPs" /></a>