cascade-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cascade/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """cascade — cascade-resolution routing router, MCP server, and simulator.
2
+
3
+ Modules:
4
+ cascade_routing core resolution router (WINNER/FORK/RECOMPUTE/OCC arms)
5
+ server MCP stdio server wrapping the router as tools
6
+ cascade_sim standalone regime simulator
7
+ """
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ cascade_routing.py — pure OCC vs pure cascade vs per-field HYBRID.
4
+ Fair head-to-head: forked/committed fields keep contending (no freeze), so
5
+ conflict volumes are comparable across policies. Tracks BOTH costs:
6
+ recomputes wasted expensive re-runs (OCC overpays these under churn)
7
+ silent_errors committed-but-actually-wrong values (cascade risks these)
8
+
9
+ Each field has a TRUE tolerance (drift its answer can absorb). Hybrid routes
10
+ zero-tolerance fields to OCC (safe) and tolerant fields to the semantic cascade
11
+ with materiality = measured tolerance. tol_safety>1 models OVER-estimating it.
12
+ fresh_loser_redo_prob models the assumption that a fresh loser re-runs anyway.
13
+ """
14
+ from __future__ import annotations
15
+ import math
16
+ import random
17
+ from dataclasses import dataclass
18
+ from collections import Counter
19
+
20
+ @dataclass
21
+ class Field:
22
+ id: str; level: int; deps: list
23
+ rev: int = 0; value: float = 1.0
24
+ true_tol: float = 0.25; policy: str = "cascade"; materiality: float = 0.20
25
+
26
+ @dataclass
27
+ class Write:
28
+ tier: int; conf: float; read_rev: dict; read_val: dict
29
+
30
+ @dataclass
31
+ class Config:
32
+ n_levels: int = 3; fields_per_level: int = 6; deps_per_field: int = 3
33
+ conf_levels: tuple = (0.5, 0.7, 0.85, 0.95, 0.99)
34
+ rounds: int = 12000
35
+ source_write_prob: float = 0.25; contention_prob: float = 0.30
36
+ width: tuple = (2, 3); lag: int = 4; value_drift: float = 0.08
37
+ frac_zero_tol: float = 0.30; zero_tol: float = 0.01; gen_tol: float = 0.25
38
+ policy: str = "hybrid"; global_materiality: float = 0.20
39
+ route_threshold: float = 0.05; tol_safety: float = 1.0
40
+ # multiplicative log-normal noise on the per-field tolerance ESTIMATE.
41
+ # tol_safety is systematic bias; tol_est_noise is the honest "you measured
42
+ # it, but imperfectly" spread. At >0 the hybrid over-estimates on ~half its
43
+ # fields even with tol_safety=1 -> silent errors appear (the safety=1 zero
44
+ # is a perfect-knowledge artifact, not a property of the design).
45
+ tol_est_noise: float = 0.0
46
+ fresh_loser_redo_prob: float = 0.0; seed: int = 0
47
+
48
+ def drift(w, F):
49
+ return max((abs(F[d].value/v0 - 1.0) if v0 else 0.0 for d, v0 in w.read_val.items()), default=0.0)
50
+ def rev_stale(w, F):
51
+ return any(F[d].rev > s for d, s in w.read_rev.items())
52
+ def bump(f, rng, cfg):
53
+ f.rev += 1; f.value *= (1.0 + rng.gauss(0.0, cfg.value_drift))
54
+
55
+ def build(cfg, rng):
56
+ F, by = {}, []
57
+ for lvl in range(cfg.n_levels):
58
+ ids = [f"L{lvl}_{i}" for i in range(cfg.fields_per_level)]; by.append(ids)
59
+ for fid in ids:
60
+ deps = [] if lvl == 0 else rng.sample(
61
+ [x for p in by[:lvl] for x in p], min(cfg.deps_per_field, cfg.fields_per_level*lvl))
62
+ f = Field(fid, lvl, deps)
63
+ if lvl > 0:
64
+ f.true_tol = cfg.zero_tol if rng.random() < cfg.frac_zero_tol else cfg.gen_tol
65
+ measured = f.true_tol * cfg.tol_safety
66
+ if cfg.tol_est_noise > 0.0:
67
+ measured *= math.exp(rng.gauss(0.0, cfg.tol_est_noise))
68
+ if cfg.policy == "occ": f.policy = "occ"
69
+ # occ_value: OCC decision rule (commit-any-fresh, all losers
70
+ # rerun, no fork/authority) on the VALUE predicate. Isolates the
71
+ # staleness-predicate win from the routing/arbitration win.
72
+ elif cfg.policy == "occ_value":
73
+ f.policy, f.materiality = "occ_value", cfg.global_materiality
74
+ elif cfg.policy == "cascade": f.policy, f.materiality = "cascade", cfg.global_materiality
75
+ else:
76
+ if measured < cfg.route_threshold: f.policy = "occ"
77
+ else: f.policy, f.materiality = "cascade", measured
78
+ F[fid] = f
79
+ return F
80
+
81
+ def resolve(f, grp, F, cfg, rng):
82
+ if f.policy == "occ":
83
+ fresh = [w for w in grp if not rev_stale(w, F)]
84
+ if fresh: return True, len(grp) - 1, 0, "OCC_COMMIT"
85
+ return False, len(grp), 0, "OCC_ALLABORT"
86
+ if f.policy == "occ_value":
87
+ # value predicate + OCC accounting: commit one fresh, ALL losers rerun
88
+ # (no free adoption), no fork/authority. Can leak, because a flat global
89
+ # materiality has no idea of this field's true tolerance.
90
+ fresh = [w for w in grp if drift(w, F) <= f.materiality]
91
+ if not fresh: return False, len(grp), 0, "OCC_ALLABORT"
92
+ silent = 1 if drift(fresh[0], F) > f.true_tol else 0
93
+ return True, len(grp) - 1, silent, "OCC_COMMIT"
94
+ m = f.materiality
95
+ fresh = [w for w in grp if drift(w, F) <= m]; n_stale = len(grp) - len(fresh)
96
+ if not fresh: return False, len(grp), 0, "RECOMPUTE"
97
+ redo = n_stale + sum(1 for _ in range(len(fresh) - 1) if rng.random() < cfg.fresh_loser_redo_prob)
98
+ bt = min(w.tier for w in fresh); top = [w for w in fresh if w.tier == bt]
99
+ if len(top) > 1:
100
+ bc = max(w.conf for w in top); top = [w for w in top if w.conf == bc]
101
+ if len(top) > 1: return True, redo, 0, "FORK"
102
+ silent = 1 if drift(top[0], F) > f.true_tol else 0
103
+ return True, redo, silent, "WINNER"
104
+
105
+ def run(cfg):
106
+ rng = random.Random(cfg.seed); F = build(cfg, rng)
107
+ src = [f for f in F.values() if f.level == 0]; der = [f for f in F.values() if f.level > 0]
108
+ topo = sorted(F.values(), key=lambda f: f.level); pending, since, M = {}, {}, Counter()
109
+ for r in range(cfg.rounds):
110
+ for f in src:
111
+ if rng.random() < cfg.source_write_prob: bump(f, rng, cfg)
112
+ for f in der:
113
+ if f.id in pending: continue
114
+ if rng.random() < cfg.contention_prob:
115
+ grp = [Write(2, rng.choice(cfg.conf_levels),
116
+ {d: F[d].rev for d in f.deps}, {d: F[d].value for d in f.deps})
117
+ for _ in range(rng.randint(*cfg.width))]
118
+ pending[f.id] = grp; since[f.id] = r
119
+ for f in topo:
120
+ g = pending.get(f.id)
121
+ if g is None or r - since[f.id] < cfg.lag: continue
122
+ committed, redo, silent, arm = resolve(f, g, F, cfg, rng)
123
+ M["conflicts"] += 1; M["recomputes"] += redo; M["silent_errors"] += silent; M[arm] += 1
124
+ if committed: M["commits"] += 1; bump(f, rng, cfg) # keeps contending; no freeze
125
+ del pending[f.id]; del since[f.id]
126
+ return M
127
+
128
+ def rep(label, M):
129
+ c = M["conflicts"] or 1; cm = M["commits"] or 1
130
+ print(f" {label:<28} {M['recomputes']/c:4.2f} recompute/conflict "
131
+ f"{M['recomputes']/cm:5.2f} recompute/commit "
132
+ f"silent_err {M['silent_errors']:>4} conflicts {M['conflicts']:>6}")
133
+
134
+ def main():
135
+ print("=" * 104)
136
+ print("POLICY HEAD-TO-HEAD (lag=4; 30% price-like tol~0.01, 70% estimate-like tol~0.25)")
137
+ print("=" * 104)
138
+ print("\n[10] pure OCC(rev) vs OCC(value) vs pure CASCADE(0.20) vs HYBRID(routed)")
139
+ rep("pure OCC (rev-staleness)", run(Config(policy="occ")))
140
+ rep("OCC (value-staleness 0.20)", run(Config(policy="occ_value", global_materiality=0.20)))
141
+ rep("pure CASCADE (global 0.20)", run(Config(policy="cascade", global_materiality=0.20)))
142
+ rep("HYBRID (measured tol)", run(Config(policy="hybrid")))
143
+ print(" OCC(value) vs OCC(rev): the throughput gain that is JUST the predicate,")
144
+ print(" not the routing. OCC(value) silent_err > 0: the predicate alone is unsafe.")
145
+
146
+ print("\n[10b] HYBRID under NOISY (imperfect) tolerance measurement, tol_safety=1")
147
+ print(" the safety=1 zero-leak is a perfect-knowledge artifact; noise leaks:")
148
+ for nz in (0.0, 0.5, 1.0):
149
+ rep(f"hybrid tol_est_noise={nz:.1f}", run(Config(policy="hybrid", tol_est_noise=nz)))
150
+
151
+ print("\n[11] survives the 'fresh loser adopts winner free' assumption being switched off?")
152
+ for p in (0.0, 0.5, 1.0):
153
+ rep(f"hybrid redo_prob={p:.1f}", run(Config(policy="hybrid", fresh_loser_redo_prob=p)))
154
+
155
+ print("\n[12] over-estimating tolerance: PURE CASCADE (bites hard) vs HYBRID (only cascade-routed fields)")
156
+ for s in (1.0, 2.0, 5.0):
157
+ rep(f"cascade tol_safety={s:.0f}", run(Config(policy="cascade", global_materiality=0.20 * s)))
158
+ for s in (1.0, 2.0, 5.0):
159
+ rep(f"hybrid tol_safety={s:.0f}", run(Config(policy="hybrid", tol_safety=s)))
160
+ print(" Measure tolerance conservatively (safety<=1) -> silent_err stays 0.")
161
+ print("=" * 104)
162
+
163
+ if __name__ == "__main__":
164
+ main()
cascade/cascade_sim.py ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ cascade_sim.py — go/no-go instrumentation for the read-set-validated,
4
+ provenance-weighted resolution cascade (the "agent-mesh" thesis).
5
+
6
+ It simulates concurrent agent writes over a configurable dependency DAG and
7
+ classifies every conflict-resolution event into one of three arms:
8
+
9
+ FRESH_WINNER a live (non-stale) write wins on authority->confidence.
10
+ Fully automatic, no re-run, no human. THIS is the value:
11
+ the case where you beat OCC (no retry) and CodeCRDT (no LLM).
12
+
13
+ FORK two+ fresh writes tie on (authority, confidence). Lossless:
14
+ you defer to a human/high-tier agent instead of silently
15
+ dropping. Better than silent corruption, but not hands-off.
16
+
17
+ RECOMPUTE every competing write is premise-stale (all-stale). There is
18
+ no correct value to pick -> you must re-run. Here you are NO
19
+ BETTER than S-Bus OCC. This arm is the thesis-eroder.
20
+
21
+ Decision rule of thumb:
22
+ - high FRESH_WINNER across realistic churn -> strong GO
23
+ - RECOMPUTE dominates once churn is realistic -> "just use OCC", NO-GO
24
+ - FORK meaningful only if confidence is coarse -> design implication
25
+
26
+ The resolve() function below mirrors the Rust resolve_field() exactly:
27
+ staleness gate -> authority tier -> confidence -> fork-on-tie.
28
+ """
29
+
30
+ from __future__ import annotations
31
+ import random
32
+ from dataclasses import dataclass, field
33
+ from collections import Counter
34
+ from typing import Optional
35
+
36
+
37
+ # ----------------------------- data model -----------------------------------
38
+
39
+ @dataclass
40
+ class Field:
41
+ id: str
42
+ level: int
43
+ deps: list[str]
44
+ rev: int = 0 # monotonic per-field revision, bumped on commit
45
+ value: float = 1.0 # semantic content; perturbed on every commit/churn
46
+ forked: bool = False
47
+
48
+
49
+ @dataclass
50
+ class Write:
51
+ field_id: str
52
+ agent_id: int
53
+ authority_tier: int # lower = more authoritative (Tier 0 = human/verified)
54
+ confidence: float
55
+ read_set: dict[str, int] # dep_id -> dep.rev observed at read time
56
+ read_vals: dict[str, float] # dep_id -> dep.value observed at read time
57
+ issued_round: int
58
+
59
+
60
+ # ----------------------------- config ----------------------------------------
61
+
62
+ @dataclass
63
+ class Config:
64
+ # DAG shape
65
+ n_levels: int = 3
66
+ fields_per_level: int = 6
67
+ deps_per_field: int = 3 # read-set size for derived fields
68
+
69
+ # agents / authority
70
+ n_agents: int = 8
71
+ # P(tier). Tier 0 = human/verified source, Tier 1 = analyst, Tier 2 = swarm.
72
+ tier_probs: tuple[float, float, float] = (0.0, 0.0, 1.0) # homogeneous swarm
73
+
74
+ # confidence: a coarse discrete scale (realistic for LLM/scraper self-report).
75
+ # Set continuous=True to make exact ties vanish (fork arm collapses).
76
+ confidence_levels: tuple[float, ...] = (0.5, 0.7, 0.85, 0.95, 0.99)
77
+ continuous_confidence: bool = False
78
+
79
+ # dynamics
80
+ rounds: int = 4000
81
+ source_write_prob: float = 0.25 # P(a source field churns each round)
82
+ contention_prob: float = 0.30 # P(a derived field draws concurrent writes/round)
83
+ contention_width: tuple[int, int] = (2, 3) # inclusive range of concurrent writers
84
+ resolution_lag: int = 1 # rounds between read and resolve (time pressure)
85
+
86
+ # --- staleness model ---------------------------------------------------
87
+ # rev-staleness (default): a write is stale iff ANY dependency's rev moved.
88
+ # This is the harsh upper bound — it flags "HQ city updated" as
89
+ # invalidating a revenue estimate that never touched the HQ field's value.
90
+ # semantic-staleness: a write is stale iff a dependency's VALUE moved past
91
+ # `materiality` (relative). A rev can advance while the value drifts
92
+ # below threshold -> the write survives. This is CoAgent's "did the
93
+ # conflict actually invalidate my premise" test, made deterministic.
94
+ semantic_staleness: bool = False
95
+ materiality: float = 0.10 # relative value move that counts as stale
96
+ value_drift: float = 0.08 # std-dev of relative value move per event
97
+
98
+ seed: int = 0
99
+
100
+
101
+ # ----------------------------- core resolve ----------------------------------
102
+ # Mirrors Rust resolve_field(). Pure function of the competing writes + the
103
+ # already-resolved upstream revs (resolution happens in topological order, so
104
+ # every dependency is at its winning rev before this is called).
105
+
106
+ FRESH_WINNER = "FRESH_WINNER"
107
+ FORK = "FORK"
108
+ RECOMPUTE = "RECOMPUTE"
109
+
110
+
111
+ def is_stale(w: Write, fields: dict[str, Field], cfg: Config) -> bool:
112
+ if not cfg.semantic_staleness:
113
+ # rev-staleness: stale iff any dependency advanced past the recorded rev
114
+ return any(fields[dep].rev > seen for dep, seen in w.read_set.items())
115
+ # semantic-staleness: stale iff any dependency's VALUE moved past materiality
116
+ for dep, seen_val in w.read_vals.items():
117
+ now = fields[dep].value
118
+ base = abs(seen_val) or 1.0
119
+ if abs(now - seen_val) / base > cfg.materiality:
120
+ return True
121
+ return False
122
+
123
+
124
+ def resolve(writes: list[Write], fields: dict[str, Field],
125
+ cfg: Config) -> tuple[str, Optional[Write]]:
126
+ fresh = [w for w in writes if not is_stale(w, fields, cfg)]
127
+
128
+ if len(fresh) == 0:
129
+ return RECOMPUTE, None
130
+ if len(fresh) == 1:
131
+ return FRESH_WINNER, fresh[0]
132
+
133
+ # authority: lowest tier number wins
134
+ best_tier = min(w.authority_tier for w in fresh)
135
+ top = [w for w in fresh if w.authority_tier == best_tier]
136
+ if len(top) == 1:
137
+ return FRESH_WINNER, top[0]
138
+
139
+ # confidence: highest wins
140
+ best_conf = max(w.confidence for w in top)
141
+ winners = [w for w in top if w.confidence == best_conf]
142
+ if len(winners) == 1:
143
+ return FRESH_WINNER, winners[0]
144
+
145
+ # exact tie on (authority, confidence) -> lossless fork
146
+ return FORK, None
147
+
148
+
149
+ # ----------------------------- simulation ------------------------------------
150
+
151
+ def build_dag(cfg: Config, rng: random.Random) -> dict[str, Field]:
152
+ fields: dict[str, Field] = {}
153
+ by_level: list[list[str]] = []
154
+ for lvl in range(cfg.n_levels):
155
+ ids = [f"L{lvl}_{i}" for i in range(cfg.fields_per_level)]
156
+ by_level.append(ids)
157
+ for fid in ids:
158
+ if lvl == 0:
159
+ deps: list[str] = []
160
+ else:
161
+ pool = [x for prev in by_level[:lvl] for x in prev]
162
+ k = min(cfg.deps_per_field, len(pool))
163
+ deps = rng.sample(pool, k)
164
+ fields[fid] = Field(id=fid, level=lvl, deps=deps)
165
+ return fields
166
+
167
+
168
+ def draw_tier(cfg: Config, rng: random.Random) -> int:
169
+ r, acc = rng.random(), 0.0
170
+ for tier, p in enumerate(cfg.tier_probs):
171
+ acc += p
172
+ if r <= acc:
173
+ return tier
174
+ return len(cfg.tier_probs) - 1
175
+
176
+
177
+ def draw_conf(cfg: Config, rng: random.Random) -> float:
178
+ if cfg.continuous_confidence:
179
+ return rng.random()
180
+ return rng.choice(cfg.confidence_levels)
181
+
182
+
183
+ def perturb(f: Field, cfg: Config, rng: random.Random) -> None:
184
+ # a commit/churn moves the field's value by a random relative amount.
185
+ # some events move it a lot (material), some barely (immaterial) — which
186
+ # is exactly what semantic-staleness gets to distinguish and rev-staleness
187
+ # cannot. Multiplicative so value never hits 0 (read_vals stay well-defined).
188
+ f.value *= (1.0 + rng.gauss(0.0, cfg.value_drift))
189
+
190
+
191
+ def run(cfg: Config) -> Counter:
192
+ rng = random.Random(cfg.seed)
193
+ fields = build_dag(cfg, rng)
194
+ sources = [f for f in fields.values() if f.level == 0]
195
+ derived = [f for f in fields.values() if f.level > 0]
196
+ topo = sorted(fields.values(), key=lambda f: f.level)
197
+
198
+ pending: dict[str, list[Write]] = {} # field_id -> concurrent writes
199
+ pending_since: dict[str, int] = {}
200
+ arms: Counter = Counter()
201
+
202
+ for r in range(cfg.rounds):
203
+ # (a) source churn — single accepted write, bumps rev (not a conflict)
204
+ for f in sources:
205
+ if rng.random() < cfg.source_write_prob:
206
+ f.rev += 1
207
+ perturb(f, cfg, rng)
208
+
209
+ # (b) issue contention on derived fields not already contended
210
+ for f in derived:
211
+ if f.id in pending or f.forked:
212
+ continue
213
+ if rng.random() < cfg.contention_prob:
214
+ w = rng.randint(*cfg.contention_width)
215
+ writes = []
216
+ for _ in range(w):
217
+ read_set = {d: fields[d].rev for d in f.deps}
218
+ read_vals = {d: fields[d].value for d in f.deps}
219
+ writes.append(Write(
220
+ field_id=f.id,
221
+ agent_id=rng.randrange(cfg.n_agents),
222
+ authority_tier=draw_tier(cfg, rng),
223
+ confidence=draw_conf(cfg, rng),
224
+ read_set=read_set,
225
+ read_vals=read_vals,
226
+ issued_round=r,
227
+ ))
228
+ pending[f.id] = writes
229
+ pending_since[f.id] = r
230
+
231
+ # (c) resolve matured groups in topological order (upstream first, so an
232
+ # upstream commit this round can invalidate a downstream group)
233
+ for f in topo:
234
+ grp = pending.get(f.id)
235
+ if grp is None:
236
+ continue
237
+ if r - pending_since[f.id] < cfg.resolution_lag:
238
+ continue
239
+ arm, winner = resolve(grp, fields, cfg)
240
+ arms[arm] += 1
241
+ if arm == FRESH_WINNER:
242
+ f.rev += 1 # commit winner, bump rev (cascades)
243
+ perturb(f, cfg, rng) # ...and move value (may be immaterial)
244
+ elif arm == FORK:
245
+ f.rev += 1
246
+ perturb(f, cfg, rng)
247
+ f.forked = True # deferred; freeze for this run
248
+ # RECOMPUTE: nothing committed (losers would re-run)
249
+ del pending[f.id]
250
+ del pending_since[f.id]
251
+
252
+ return arms
253
+
254
+
255
+ def summarize(arms: Counter) -> dict[str, float]:
256
+ total = sum(arms.values()) or 1
257
+ return {k: 100.0 * arms.get(k, 0) / total for k in (FRESH_WINNER, FORK, RECOMPUTE)}
258
+
259
+
260
+ def pct_line(label: str, arms: Counter) -> str:
261
+ s = summarize(arms)
262
+ total = sum(arms.values())
263
+ return (f"{label:<28} "
264
+ f"winner {s[FRESH_WINNER]:5.1f}% "
265
+ f"fork {s[FORK]:5.1f}% "
266
+ f"recompute {s[RECOMPUTE]:5.1f}% "
267
+ f"(n={total})")
268
+
269
+
270
+ # ----------------------------- experiments -----------------------------------
271
+
272
+ def main() -> None:
273
+ print("=" * 92)
274
+ print("CASCADE GO/NO-GO — resolution-arm distribution")
275
+ print("winner = automatic win (beats OCC+CodeCRDT) | fork = lossless defer | "
276
+ "recompute = no better than OCC")
277
+ print("=" * 92)
278
+
279
+ base = Config()
280
+ print("\n[1] BASELINE (homogeneous Tier-2 swarm, coarse confidence, lag=1)")
281
+ print(" " + pct_line("baseline", run(base)))
282
+
283
+ print("\n[2] CHURN / TIME-PRESSURE SWEEP (resolution_lag = rounds between read and resolve)")
284
+ print(" higher lag = more upstream churn lands in the read->resolve window")
285
+ for lag in (0, 1, 2, 4, 8):
286
+ cfg = Config(resolution_lag=lag)
287
+ print(" " + pct_line(f"lag={lag}", run(cfg)))
288
+
289
+ print("\n[3] SOURCE-CHURN SWEEP (P a source field changes each round; lag=2)")
290
+ for p in (0.05, 0.15, 0.30, 0.50, 0.75):
291
+ cfg = Config(source_write_prob=p, resolution_lag=2)
292
+ print(" " + pct_line(f"source_write_prob={p:.2f}", run(cfg)))
293
+
294
+ print("\n[4] CONFIDENCE GRANULARITY (does the FORK arm survive real-valued confidence?)")
295
+ print(" " + pct_line("coarse 5-level", run(Config())))
296
+ print(" " + pct_line("continuous float", run(Config(continuous_confidence=True)))
297
+ )
298
+
299
+ print("\n[5] AUTHORITY DIVERSITY (homogeneous swarm vs. mixed tiers w/ occasional human)")
300
+ print(" " + pct_line("homogeneous (all Tier2)", run(Config(tier_probs=(0.0, 0.0, 1.0)))))
301
+ print(" " + pct_line("some analysts (T1/T2)", run(Config(tier_probs=(0.0, 0.35, 0.65)))))
302
+ print(" " + pct_line("rare human (T0/T1/T2)", run(Config(tier_probs=(0.1, 0.3, 0.6)))))
303
+
304
+ print("\n[6] DEPENDENCY WIDTH (read-set size; more deps = more staleness surface, lag=2)")
305
+ for d in (1, 2, 4, 6):
306
+ cfg = Config(deps_per_field=d, resolution_lag=2)
307
+ print(" " + pct_line(f"deps_per_field={d}", run(cfg)))
308
+
309
+ print("\n[7] SEMANTIC STALENESS (does 'value moved' beat 'rev moved'? the thesis test)")
310
+ print(" rev-staleness flags ANY upstream change; semantic clears immaterial ones.")
311
+ print(" -- materiality sweep (lag=4, where rev-staleness had 94% recompute) --")
312
+ print(" " + pct_line("rev-staleness (baseline)",
313
+ run(Config(resolution_lag=4))))
314
+ for m in (0.05, 0.10, 0.20, 0.40):
315
+ cfg = Config(resolution_lag=4, semantic_staleness=True, materiality=m)
316
+ print(" " + pct_line(f"semantic materiality={m:.2f}", run(cfg)))
317
+ print(" -- same lag sweep, semantic mode (compare to [2]) --")
318
+ for lag in (0, 1, 2, 4, 8):
319
+ cfg = Config(resolution_lag=lag, semantic_staleness=True, materiality=0.20)
320
+ print(" " + pct_line(f"semantic lag={lag}", run(cfg)))
321
+
322
+ print("\n" + "=" * 92)
323
+ print("Read the FRESH_WINNER column against your expected deployment churn.")
324
+ print("If winner stays high where your real churn lives -> GO.")
325
+ print("If recompute swamps it there -> the DAG machinery")
326
+ print("buys little over plain OCC; ship OCC instead.")
327
+ print("=" * 92)
328
+
329
+
330
+ if __name__ == "__main__":
331
+ main()
cascade/server.py ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ server.py — MCP server that wraps cascade_routing.py as a concurrency
4
+ controller for AI agents. Two main tools:
5
+
6
+ read_state(fields, agent_id)
7
+ Returns current value+rev of the requested fields and secretly logs
8
+ them as that agent's read-set (the rev/value snapshot the agent saw).
9
+
10
+ propose_update(field, proposed_value, confidence, authority_tier,
11
+ tolerance, agent_id, expected_writers)
12
+ Records the agent's proposed write against their logged read-set. When
13
+ the batch for `field` reaches `expected_writers`, runs the hybrid
14
+ resolve_field router (cr.resolve) on the whole batch, commits/bumps on
15
+ a winning arm, and returns the outcome. Earlier calls in the batch
16
+ return {"status": "pending"}; the final call returns the full result.
17
+
18
+ A configure tool builds the dependency DAG with per-field tolerance/policy
19
+ (mirrors cr.build). The resolver is called directly on cr.resolve/cr.bump so
20
+ outcomes are identical to in-process simulation.
21
+
22
+ The tool business logic lives in async *_impl functions so it can be unit-
23
+ tested in-process without the stdio transport; the @mcp decorators are thin
24
+ adapters that JSON-(de)serialize and call the impls.
25
+
26
+ Run as a stdio MCP server: python -m cascade.server
27
+ """
28
+ from __future__ import annotations
29
+ import json
30
+ import math
31
+ import os
32
+ import random
33
+ import sys
34
+ from dataclasses import asdict, replace
35
+ from typing import Any
36
+
37
+ import mcp.types as types
38
+ from mcp.server import Server
39
+ from mcp.server.stdio import stdio_server
40
+
41
+ from cascade import cascade_routing as cr
42
+
43
+
44
+ # ----------------------------- shared state ----------------------------------
45
+ # Module-global so the stdio server and in-process tests share one engine.
46
+
47
+ class CascadeState:
48
+ """Holds the field store, per-agent read-set log, and pending batches."""
49
+
50
+ def __init__(self) -> None:
51
+ self.fields: dict[str, cr.Field] = {}
52
+ # (agent_id, field_id_being_written) -> {dep_id: rev}
53
+ self.read_rev: dict[tuple[str, str], dict[str, int]] = {}
54
+ # (agent_id, field_id_being_written) -> {dep_id: value}
55
+ self.read_val: dict[tuple[str, str], dict[str, float]] = {}
56
+ self.pending: dict[str, list[cr.Write]] = {}
57
+ # regime-level config (set by configure)
58
+ self.tol_safety: float = 1.0
59
+ self.route_threshold: float = 0.05
60
+ self.value_drift: float = 0.08
61
+ self.fresh_loser_redo_prob: float = 0.0
62
+ self.n_levels = 3
63
+ self.fields_per_level = 6
64
+ self.deps_per_field = 3
65
+ self.frac_zero_tol = 0.30
66
+ self.zero_tol = 0.01
67
+ self.gen_tol = 0.25
68
+ # policy_mode set by configure; per-write re-routing only happens
69
+ # in "hybrid" mode (mirrors cr.build). "occ" and "cascade" fix the
70
+ # field's policy at configure time and propose_update must NOT clobber it.
71
+ self.policy_mode: str = "hybrid"
72
+ self.global_materiality: float = 0.20
73
+ # multiplicative log-normal noise on tolerance ESTIMATE (mirrors
74
+ # cr.Config.tol_est_noise). >0 -> hybrid over-estimates on ~half its
75
+ # fields even at tol_safety=1 -> silent errors (perfect-knowledge
76
+ # artifact test).
77
+ self.tol_est_noise: float = 0.0
78
+ self.rng: random.Random = random.Random(0)
79
+
80
+ def reset(self) -> None:
81
+ self.fields.clear()
82
+ self.read_rev.clear()
83
+ self.read_val.clear()
84
+ self.pending.clear()
85
+
86
+
87
+ state = CascadeState()
88
+
89
+
90
+ # ----------------------------- tool implementations --------------------------
91
+ # Pure async functions: take parsed python args, return python dicts.
92
+ # The MCP decorators below adapt these to JSON wire types.
93
+
94
+ async def configure_impl(
95
+ n_levels: int, fields_per_level: int, deps_per_field: int,
96
+ frac_zero_tol: float, zero_tol: float, gen_tol: float,
97
+ tol_safety: float, route_threshold: float, value_drift: float,
98
+ fresh_loser_redo_prob: float, seed: int,
99
+ policy_mode: str = "hybrid", global_materiality: float = 0.20,
100
+ tol_est_noise: float = 0.0,
101
+ ) -> dict:
102
+ """Build the DAG exactly like cr.build, then store regime config."""
103
+ state.rng = random.Random(seed)
104
+ state.n_levels = n_levels
105
+ state.fields_per_level = fields_per_level
106
+ state.deps_per_field = deps_per_field
107
+ state.frac_zero_tol = frac_zero_tol
108
+ state.zero_tol = zero_tol
109
+ state.gen_tol = gen_tol
110
+ state.tol_safety = tol_safety
111
+ state.route_threshold = route_threshold
112
+ state.value_drift = value_drift
113
+ state.fresh_loser_redo_prob = fresh_loser_redo_prob
114
+ state.policy_mode = policy_mode
115
+ state.global_materiality = global_materiality
116
+ state.tol_est_noise = tol_est_noise
117
+ state.reset()
118
+ # build DAG (mirrors cr.build)
119
+ by: list[list[str]] = []
120
+ for lvl in range(n_levels):
121
+ ids = [f"L{lvl}_{i}" for i in range(fields_per_level)]
122
+ by.append(ids)
123
+ for fid in ids:
124
+ if lvl == 0:
125
+ deps: list[str] = []
126
+ else:
127
+ pool = [x for p in by[:lvl] for x in p]
128
+ deps = state.rng.sample(pool, min(deps_per_field, len(pool)))
129
+ f = cr.Field(id=fid, level=lvl, deps=deps)
130
+ if lvl > 0:
131
+ f.true_tol = zero_tol if state.rng.random() < frac_zero_tol else gen_tol
132
+ measured = f.true_tol * tol_safety
133
+ if tol_est_noise > 0.0:
134
+ measured *= math.exp(state.rng.gauss(0.0, tol_est_noise))
135
+ if policy_mode == "occ":
136
+ f.policy = "occ"
137
+ elif policy_mode == "occ_value":
138
+ f.policy = "occ_value"
139
+ f.materiality = global_materiality
140
+ elif policy_mode == "cascade":
141
+ f.policy = "cascade"
142
+ f.materiality = global_materiality
143
+ else: # hybrid
144
+ if measured < route_threshold:
145
+ f.policy = "occ"
146
+ else:
147
+ f.policy = "cascade"
148
+ f.materiality = measured
149
+ state.fields[fid] = f
150
+ return {"status": "configured", "fields": list(state.fields.keys())}
151
+
152
+
153
+ async def read_state_impl(fields: list[str], agent_id: str,
154
+ write_field: str = "") -> dict:
155
+ """Return current value+rev of requested fields; secretly log them as the
156
+ caller's read-set keyed by (agent_id, write_field). write_field is the
157
+ field the agent intends to propose later (its deps are what's read)."""
158
+ snapshot = {}
159
+ log_rev: dict[str, int] = {}
160
+ log_val: dict[str, float] = {}
161
+ for fid in fields:
162
+ f = state.fields[fid]
163
+ snapshot[fid] = {"rev": f.rev, "value": f.value}
164
+ log_rev[fid] = f.rev
165
+ log_val[fid] = f.value
166
+ if write_field:
167
+ state.read_rev[(agent_id, write_field)] = log_rev
168
+ state.read_val[(agent_id, write_field)] = log_val
169
+ return {"fields": snapshot}
170
+
171
+
172
+ async def churn_impl(field: str) -> dict:
173
+ """Source-field churn: bump rev+value (mirrors the sim's source churn)."""
174
+ f = state.fields[field]
175
+ f.rev += 1
176
+ f.value *= (1.0 + state.rng.gauss(0.0, state.value_drift))
177
+ return {"field": field, "rev": f.rev, "value": f.value}
178
+
179
+
180
+ async def propose_update_impl(
181
+ field: str, proposed_value: Any, confidence: float,
182
+ authority_tier: int, tolerance: float, agent_id: str,
183
+ expected_writers: int,
184
+ ) -> dict:
185
+ """Record a proposed write. When the batch reaches expected_writers, run
186
+ cr.resolve on the whole batch and return the outcome. Uses `tolerance` as
187
+ the field's true_tol for this write and re-derives policy/materiality from
188
+ it (mirrors cr.build's hybrid branch), so callers passing the field's
189
+ constant true_tol reproduce the sim exactly."""
190
+ f = state.fields[field]
191
+ # look up the agent's logged read-set for this field
192
+ read_rev = state.read_rev.get((agent_id, field), {})
193
+ read_val = state.read_val.get((agent_id, field), {})
194
+ # if no logged read-set, snapshot current (agent didn't call read_state)
195
+ if not read_rev:
196
+ read_rev = {d: state.fields[d].rev for d in f.deps}
197
+ read_val = {d: state.fields[d].value for d in f.deps}
198
+ w = cr.Write(tier=authority_tier, conf=confidence,
199
+ read_rev=dict(read_rev), read_val=dict(read_val))
200
+ state.pending.setdefault(field, []).append(w)
201
+ batch = state.pending[field]
202
+ if len(batch) < expected_writers:
203
+ return {"status": "pending", "field": field,
204
+ "received": len(batch), "expected": expected_writers}
205
+ # batch complete -> resolve
206
+ # Set the field's true_tol from the caller-declared tolerance. Re-derive
207
+ # routing ONLY in hybrid mode AND only if the caller's tolerance differs
208
+ # from what configure_impl already set (mirrors cr.build's hybrid branch).
209
+ # In occ/cascade modes the policy/materiality were fixed at configure time
210
+ # and must NOT be clobbered per-write. Re-deriving when tolerance is
211
+ # unchanged would also wipe any tol_est_noise applied at build time.
212
+ if abs(f.true_tol - tolerance) > 1e-12:
213
+ f.true_tol = tolerance
214
+ if state.policy_mode == "hybrid":
215
+ measured = tolerance * state.tol_safety
216
+ if state.tol_est_noise > 0.0:
217
+ measured *= math.exp(state.rng.gauss(0.0, state.tol_est_noise))
218
+ if measured < state.route_threshold:
219
+ f.policy = "occ"
220
+ else:
221
+ f.policy = "cascade"
222
+ f.materiality = measured
223
+ cfg = cr.Config(value_drift=state.value_drift,
224
+ fresh_loser_redo_prob=state.fresh_loser_redo_prob,
225
+ tol_safety=state.tol_safety,
226
+ tol_est_noise=state.tol_est_noise,
227
+ route_threshold=state.route_threshold)
228
+ committed, redo, silent, arm = cr.resolve(f, batch, state.fields, cfg, state.rng)
229
+ # staleness accounting at resolve time (for the CSV). occ uses rev-stale;
230
+ # occ_value, cascade, and hybrid all use the value-predicate drift.
231
+ if f.policy == "occ":
232
+ n_stale = sum(1 for ww in batch if cr.rev_stale(ww, state.fields))
233
+ else:
234
+ n_stale = sum(1 for ww in batch if cr.drift(ww, state.fields) > f.materiality)
235
+ # winner tier/conf
236
+ if arm in ("WINNER", "FORK", "OCC_COMMIT"):
237
+ fresh = [ww for ww in batch
238
+ if (not cr.rev_stale(ww, state.fields) if f.policy == "occ"
239
+ else cr.drift(ww, state.fields) <= f.materiality)]
240
+ bt = min(ww.tier for ww in fresh)
241
+ top = [ww for ww in fresh if ww.tier == bt]
242
+ bc = max(ww.conf for ww in top)
243
+ top = [ww for ww in top if ww.conf == bc]
244
+ win_tier = top[0].tier
245
+ top_conf = top[0].conf
246
+ else:
247
+ win_tier = -1
248
+ top_conf = -1.0
249
+ if committed:
250
+ cr.bump(f, state.rng, cfg)
251
+ del state.pending[field]
252
+ return {
253
+ "status": "resolved", "field": field, "arm": arm,
254
+ "recomputes": redo, "silent_error": silent,
255
+ "committed": committed, "n_writers": len(batch), "n_stale": n_stale,
256
+ "win_tier": win_tier, "top_confidence": top_conf,
257
+ }
258
+
259
+
260
+ async def get_field_impl(field: str) -> dict:
261
+ """Inspect a field's current state (for tests/debugging)."""
262
+ f = state.fields[field]
263
+ return {"id": f.id, "level": f.level, "deps": f.deps, "rev": f.rev,
264
+ "value": f.value, "true_tol": f.true_tol,
265
+ "policy": f.policy, "materiality": f.materiality}
266
+
267
+
268
+ # ----------------------------- MCP server wiring -----------------------------
269
+
270
+ server = Server("cascade-routing-controller")
271
+
272
+
273
+ @server.list_tools()
274
+ async def list_tools() -> list[types.Tool]:
275
+ return [
276
+ types.Tool(
277
+ name="configure",
278
+ description="Build the dependency DAG and set regime parameters.",
279
+ inputSchema={
280
+ "type": "object",
281
+ "properties": {
282
+ "n_levels": {"type": "integer"},
283
+ "fields_per_level": {"type": "integer"},
284
+ "deps_per_field": {"type": "integer"},
285
+ "frac_zero_tol": {"type": "number"},
286
+ "zero_tol": {"type": "number"},
287
+ "gen_tol": {"type": "number"},
288
+ "tol_safety": {"type": "number"},
289
+ "route_threshold": {"type": "number"},
290
+ "value_drift": {"type": "number"},
291
+ "fresh_loser_redo_prob": {"type": "number"},
292
+ "seed": {"type": "integer"},
293
+ "policy_mode": {"type": "string"},
294
+ "global_materiality": {"type": "number"},
295
+ },
296
+ "required": ["n_levels", "fields_per_level", "deps_per_field",
297
+ "frac_zero_tol", "zero_tol", "gen_tol",
298
+ "tol_safety", "route_threshold", "value_drift",
299
+ "fresh_loser_redo_prob", "seed"],
300
+ },
301
+ ),
302
+ types.Tool(
303
+ name="read_state",
304
+ description="Read current value+rev of fields and log them as the "
305
+ "caller's read-set (the premise snapshot for a later write).",
306
+ inputSchema={
307
+ "type": "object",
308
+ "properties": {
309
+ "fields": {"type": "array", "items": {"type": "string"}},
310
+ "agent_id": {"type": "string"},
311
+ "write_field": {"type": "string"},
312
+ },
313
+ "required": ["fields", "agent_id", "write_field"],
314
+ },
315
+ ),
316
+ types.Tool(
317
+ name="propose_update",
318
+ description="Propose a write. When the batch for `field` reaches "
319
+ "`expected_writers`, runs the hybrid resolve_field router.",
320
+ inputSchema={
321
+ "type": "object",
322
+ "properties": {
323
+ "field": {"type": "string"},
324
+ "proposed_value": {},
325
+ "confidence": {"type": "number"},
326
+ "authority_tier": {"type": "integer"},
327
+ "tolerance": {"type": "number"},
328
+ "agent_id": {"type": "string"},
329
+ "expected_writers": {"type": "integer"},
330
+ },
331
+ "required": ["field", "proposed_value", "confidence",
332
+ "authority_tier", "tolerance", "agent_id",
333
+ "expected_writers"],
334
+ },
335
+ ),
336
+ types.Tool(
337
+ name="churn",
338
+ description="Bump a source field's rev+value (simulates upstream churn).",
339
+ inputSchema={
340
+ "type": "object",
341
+ "properties": {"field": {"type": "string"}},
342
+ "required": ["field"],
343
+ },
344
+ ),
345
+ types.Tool(
346
+ name="get_field",
347
+ description="Inspect a field's current state.",
348
+ inputSchema={
349
+ "type": "object",
350
+ "properties": {"field": {"type": "string"}},
351
+ "required": ["field"],
352
+ },
353
+ ),
354
+ ]
355
+
356
+
357
+ @server.call_tool()
358
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
359
+ if name == "configure":
360
+ r = await configure_impl(**arguments)
361
+ elif name == "read_state":
362
+ r = await read_state_impl(**arguments)
363
+ elif name == "propose_update":
364
+ r = await propose_update_impl(**arguments)
365
+ elif name == "churn":
366
+ r = await churn_impl(**arguments)
367
+ elif name == "get_field":
368
+ r = await get_field_impl(**arguments)
369
+ else:
370
+ raise ValueError(f"unknown tool: {name}")
371
+ return [types.TextContent(type="text", text=json.dumps(r, default=str))]
372
+
373
+
374
+ async def main() -> None:
375
+ async with stdio_server() as (read, write):
376
+ await server.run(read, write, server.create_initialization_options())
377
+
378
+
379
+ def cli() -> None:
380
+ """Synchronous entry point for the ``cascade-mcp`` console script."""
381
+ import asyncio
382
+ asyncio.run(main())
383
+
384
+
385
+ if __name__ == "__main__":
386
+ cli()
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: cascade-mcp
3
+ Version: 0.1.0
4
+ Summary: Cascade-resolution routing for concurrent multi-agent writes, exposed as an MCP server.
5
+ Project-URL: Homepage, https://github.com/clemente-turrubiates/cascade-mcp
6
+ Project-URL: Repository, https://github.com/clemente-turrubiates/cascade-mcp
7
+ Author: Pedro Clemente-Turrubiates
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Pedro Clemente-Turrubiates
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: agents,conflict-resolution,mcp,model-context-protocol,occ,routing
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Topic :: Software Development :: Libraries
35
+ Requires-Python: >=3.10
36
+ Requires-Dist: mcp>=1.0
37
+ Description-Content-Type: text/markdown
38
+
39
+ # cascade-mcp
40
+
41
+ [![PyPI](https://img.shields.io/pypi/v/cascade-mcp.svg)](https://pypi.org/project/cascade-mcp/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/cascade-mcp.svg)](https://pypi.org/project/cascade-mcp/)
43
+ [![CI](https://github.com/clemente-turrubiates/cascade-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/clemente-turrubiates/cascade-mcp/actions/workflows/ci.yml)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
45
+
46
+ Cascade-resolution routing for concurrent multi-agent writes — a resolution
47
+ router that decides, per conflict, whether a write **wins**, **forks** to a
48
+ human, or must be **recomputed**, plus an MCP server that exposes the router as
49
+ tools and a stress-test suite that proves the behavior can't be cherry-picked.
50
+
51
+ ## What's here
52
+
53
+ The core question: when many agents write to the same field over a dependency
54
+ DAG, how do you resolve conflicts without either silently committing wrong
55
+ values (pure cascade) or overpaying in wasted re-runs (pure OCC)? The **hybrid**
56
+ policy routes zero-tolerance fields to OCC and tolerant fields to a
57
+ provenance-weighted cascade. Every conflict lands in one of a few arms:
58
+
59
+ - **WINNER** — a live (non-stale) write wins on authority → confidence. No
60
+ re-run, no human. This is the win over OCC.
61
+ - **FORK** — two+ fresh writes tie; defer to a human/high-tier agent instead of
62
+ silently dropping one.
63
+ - **RECOMPUTE** — every competing write is premise-stale; there's no correct
64
+ value to pick, so re-run. Here you're no better than OCC.
65
+
66
+ ## Layout
67
+
68
+ ```
69
+ cascade/ importable package
70
+ cascade_routing.py core resolution router (OCC vs cascade vs hybrid)
71
+ server.py MCP stdio server wrapping the router as tools
72
+ cascade_sim.py standalone go/no-go regime simulator
73
+ scripts/ data-generation / audit utilities
74
+ gen_agent_logs.py emit agent_logs.csv across the regime × policy grid
75
+ audit_cherrypick.py adversarial read of agent_logs.csv
76
+ validate_logs.py quick sanity checks on a generated CSV
77
+ tests/ verification suite
78
+ test_agent_logs.py 43-check self-consistency + usability suite over the CSV
79
+ test_mcp_wrapper.py routes the regime grid through the MCP wrapper and
80
+ re-runs the suite to prove the wrapper preserves behavior
81
+ ```
82
+
83
+ Large simulation outputs (`agent_logs.csv`, `agent_logs_mcp.csv`, ~900 MB each)
84
+ are regenerable and are gitignored.
85
+
86
+ ## Requirements
87
+
88
+ - Python ≥ 3.10 (developed on 3.13)
89
+ - [`mcp`](https://pypi.org/project/mcp/) — installed automatically as a dependency
90
+
91
+ ## Install & attach to an MCP client
92
+
93
+ Once published to PyPI, no clone or virtualenv is needed — [`uvx`](https://docs.astral.sh/uv/)
94
+ runs the server in an ephemeral environment:
95
+
96
+ ```
97
+ uvx cascade-mcp
98
+ ```
99
+
100
+ To attach the router to **Claude Desktop** or **Cursor**, add this to your
101
+ `claude_desktop_config.json`:
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "cascade": {
107
+ "command": "uvx",
108
+ "args": ["cascade-mcp"]
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ The MCP server exposes five tools: `configure`, `read_state`, `propose_update`,
115
+ `churn`, `get_field`.
116
+
117
+ ## Usage (from source)
118
+
119
+ Clone the repo and run everything **from the repo root**.
120
+
121
+ Run the MCP server (stdio):
122
+
123
+ ```
124
+ python -m cascade.server
125
+ ```
126
+
127
+ Run the standalone simulator:
128
+
129
+ ```
130
+ python -m cascade.cascade_sim
131
+ ```
132
+
133
+ Generate the stress-test CSV (writes UTF-8 — pipe via a POSIX shell, **not**
134
+ PowerShell `>`, which re-encodes to UTF-16 and corrupts the file):
135
+
136
+ ```
137
+ python scripts/gen_agent_logs.py > agent_logs.csv
138
+ ```
139
+
140
+ Verify the generated CSV:
141
+
142
+ ```
143
+ python -m tests.test_agent_logs # 43-check suite
144
+ python scripts/audit_cherrypick.py # adversarial cross-checks
145
+ ```
146
+
147
+ Verify the MCP wrapper preserves the router's behavior end-to-end (wire-protocol
148
+ smoke test → regime grid through the wrapper → re-run the suite):
149
+
150
+ ```
151
+ python -m tests.test_mcp_wrapper
152
+ ```
@@ -0,0 +1,9 @@
1
+ cascade/__init__.py,sha256=g3tK6_JLleAXkdJ8CZAOzJeY6MqaeguI7fL3nO6oBHU,284
2
+ cascade/cascade_routing.py,sha256=BgP6NV4YA19-u1ky5Q55Y1vLKu1srNO54_Eewj3d2MM,8357
3
+ cascade/cascade_sim.py,sha256=C3ReNNQJ7RBaqnHN0jLjrWkd3ARznvPLJHfZZom4kcE,13647
4
+ cascade/server.py,sha256=o5XXmxrBnDiDrMnJ8Qby_xE4C3mUyWcc1VPqoy9DDgY,16056
5
+ cascade_mcp-0.1.0.dist-info/METADATA,sha256=AxSnfVFlUy4DDqeHJjtbpPPt6-371yuVNkQilK0g91I,5909
6
+ cascade_mcp-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ cascade_mcp-0.1.0.dist-info/entry_points.txt,sha256=JjoVpF8tLyfqtSkcvCLCAES5RLeXd3l00fkkv3KQlLI,51
8
+ cascade_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=BT6a36j8u_AQmHmhEU7Md6yreS5_E6MqZf5digarpFY,1083
9
+ cascade_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cascade-mcp = cascade.server:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Clemente-Turrubiates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.