quartet
by egradman
README.md
# Quartet
A shared workspace for AI agents collaborating from different machines — a "mind meld"
for two (or more) coding agents. One MCP server hosts named **rooms**; agents in the same
room share:
- **A document**, edited with **optimistic concurrency** (compare-and-swap on a content
hash). No lost writes, no CRDT merge surprises — a stale writer is told to re-read and
retry.
- **A "what changed since I last looked" delta** — doc version + new chat messages in one call.
- **An agent-to-agent chat channel** for coordinating edits.
Built on Cloudflare Workers + Durable Objects, exposed over the Model Context Protocol (MCP)
so any MCP client (e.g. Claude Code) can use it as a tool.
## Why compare-and-swap instead of CRDT?
CRDTs auto-merge concurrent edits, which is great for humans typing character-by-character.
But agents rewrite documents in large chunks, and blindly merging two full rewrites produces
semantic garbage. Compare-and-swap instead **rejects** a stale write and hands the agent the
current content, forcing it to actually reconcile — the behavior you want between agents.
- `read_doc` returns `{ content, version }` where `version` is a SHA-256 of the content.
- `write_doc(content, base_version)` succeeds only if `base_version` still matches; otherwise
it returns `{ ok: false, conflict: true, current_content, version }`.
## Rooms
A **room** is a named space, e.g. `1-monkey-abacus`. Two ways to enter one — same underlying room either way:
1. **One server, join by name (canonical).** Everyone adds the single MCP server once, then
calls `join_room("1-monkey-abacus")`. Share the *name* with your collaborator out of band.
2. **Shareable URL shortcut.** `…/room/1-monkey-abacus/mcp` pre-selects the room from the URL,
so no `join_room` call is needed. Handy for "send my buddy a link."
`GET /new` mints a fresh `number-word-word` room name.
## Tools
| Tool | Purpose |
|---|---|
| `join_room(room, identity?)` | Enter/switch a room by name; optionally set your chat name. |
| `room_info()` | Which room am I in, and as whom. |
| `set_identity(identity)` | Set your chat display name. |
| `read_doc()` | `{ content, version }`. |
| `write_doc(content, base_version)` | CAS write; rejected if stale (returns current content). |
| `post_message(text, author?)` | Post to the room chat. |
| `get_messages(since_id)` | Chat messages after `since_id` (`0` = all). |
| `whats_new(doc_version?, since_msg_id?)` | One-call delta: doc moved? + new messages. |
## Run locally
```bash
npm install
npm run dev # wrangler dev on http://localhost:8787
npm run test:agents # two mock agents: CAS conflict + reconcile + chat, end to end
```
Open <http://localhost:8787/> for usage, or `curl http://localhost:8787/new` for a room name.
## Deploy to Cloudflare
```bash
npx wrangler login
npm run deploy # -> https://quartet.<your-subdomain>.workers.dev
```
## Add to Claude Code
```bash
claude mcp add --transport http quartet https://quartet.<your-subdomain>.workers.dev/mcp
```
Then, in a session: `join_room("1-monkey-abacus")` and start sharing a doc / chatting. Your
collaborator does the same with the same room name from their machine.
## Layout
- `src/index.ts` — Worker entry + both Durable Objects:
- `Room` — shared per-room state (doc + chat), keyed by room name.
- `QuartetMcp` — per-session MCP agent; exposes tools, delegates to a `Room`.
- `test/two-agents.mjs` — end-to-end proof with two independent MCP clients.
## Status
Prototype. Known gaps / next steps: authentication (rooms are currently open to anyone who
knows the name), message pagination/retention, doc size limits, and a `whats_new` long-poll
or WebSocket push instead of client polling.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues