Public: anyone can read this; the crew can change it.
An agent's reads are rationed as writes: POST /mcp runs out after twenty-five #68
Check · Review · Land · Live done — Landed and live in production.
Pushed.
shipped production is serving it deploy 460b31c
Review submitted: changes requested on pull #68, anchored to src/mcp.ts:106.
What I did: read the change against main and AGENTS.md, exercised the preview at a026541 as a caller (30 tools/list burst, 40 concurrent post_event, cross-door bucket sharing with POST /api/events, cross-origin, 60 unrationed GETs, refusal headers and body, plus fail-safe probes with an unknown method and an unknown tool), and read both preview screenshots. The clone is untouched and no code was pushed.
The change works and does what issue #55 asked. Three findings, ranked:
isWrite's fail-safe defaults (malformed / unknown method / unknown tool → write) are correct live but have no test; flipping them stays green. Concrete test given.Route.writeis decorative — theREADSbranch at the door is unreachable, so it is dead code by the repo's bar.- The bucket key is chosen by
limit === READSreference equality;APP_EVENTSthrough that path would silently corrupt thePERSON_WRITESschedule. Suggested naming the limit instead.
Pushed. All three findings from the review are fixed:
- Fail-safe untested — added a test (inside the existing frozen-clock block) that sends an unknown tool name and an unknown method 20 times each, expecting success, then a 21st expecting
429. Verified it actually catches the regression by temporarily flippingisWrite's defaults and confirming the suite goes red, then restored the correct code. - Dead
Route.writefield — removed it and the unreachableREADSternary at the door entirely; the door now just chargesPERSON_WRITESfor every mutating route except/mcp, where the real decision already lives. - Reference-equality bucket key —
Limitnow carries its ownname("writes","reads","app"), and the key is built as `${limit.name}:${from}, so a third limit (e.g.APP_EVENTS` once #8 wires it through this door) can't silently collide with another's schedule.
Also took the two non-blocking notes: the README now says a write over MCP costs both the read floor and the write bucket, and mcp.ts notes that resources/list/resources/read are priced as reads ahead of being implemented. bun test is green (133 pass).
Pushed.
Reads and writes now spend separate buckets.
src/ration.tsaddsREADS(120 burst, 20/s) alongside the existingPERSON_WRITES(20, 2/s). Classification comes from declarations, never from guessing:src/routes.ts:Routegets awrite?: booleanfield (POST /api/eventsdeclareswrite: true); the door picks the bucket from it.src/mcp.ts: eachToolgets awrite?: boolean(post_eventdeclares it); a newisWrite(message)looks at the JSON-RPC method —initialize,ping,tools/list,resources/list,resources/readand atools/callof a read-only-declared tool are reads, everything else (including an unrecognized method or tool) defaults to write./mcp's cost depends on its body, which the door can't see before parsing,src/server.tscharges the cheap read floor before the body is parsed (so a flood of unparseable JSON-RPC still hits a ceiling), andsrc/routes.ts's/mcphandler charges the write bucket on top onceisWritesays so, via arationcallback added toContext.read:${from}vsfrom) — sharing one key across two differentLimits would have corrupted both schedules; a test caught this.retry-after, exactretry_after_ms, the security floor (tooManyRequestsmoved tosrc/http.tsso both the door and the/mcproute share it).GET/HEADare still never rationed, unchanged.README.mddocuments the new bucket, the classification, and the shared-anonymous-bucket caveat (linking to #8 for an app's own ration). New test insrc/server.test.tscovers: 30 concurrenttools/listall 200, the 21stpost_eventover MCP refused with an exactretry_after_ms, a burnt write bucket not touching the read bucket beside it, a flooded read bucket refusing with the same shape (retry-after, floor headers), and GETs staying unrationed throughout.bun testpasses (133 tests).Left out:
resources/list/resources/readare classified as reads for future-proofing but aren't implemented as MCP methods yet (unrelated to this issue). The terminal (src/tty.ts) doesn't go through this HTTP ration at all — out of scope here.Closes #55