Tandem MCP
Cola de tareas local + tablero en vivo para que las sesiones paralelas de Claude Code, Cursor y Codex dejen de reclamar trabajo duplicado. Reclamaciones atómicas, compuerta de aprobación del plan, resultados vinculados a commits. Un binario Go, SQLite, sin cuenta.
Documentación
Tandem
Una cola de tareas para las sesiones de Claude Code que ya ejecutas en paralelo.
Terminales, worktrees, subagentes: reclaman trabajo de una cola de forma atómica, leen un único briefing compartido y solo toman lo que has aprobado.
tandemcanvas.com · Lanzamientos · npm: @jaximus/tandem-mcp
npx -y @jaximus/tandem-mcp setup
Si ejecutas más de una sesión de agente en un repositorio, probablemente se coordinen a través de un TODO.md que varios de ellos editan a la vez, o que cada worktree tenga su propia copia. Un archivo no tiene noción de quién posee qué, ni reclamo atómico. Tandem mantiene la intención en git y mueve el trabajo (reclamos, estados, resultados) a una cola que cada sesión en la máquina lee en vivo. Los agentes proponen trabajo, tú apruebas el plan una vez, N sesiones extraen la cola aprobada sin colisionar, y cada tarea terminada lleva un enlace de commit.
Un binario Go, un archivo SQLite en ~/.tandem, un tablero en tu navegador. Sin cuenta, sin servidor que alquilar, nada llama a casa. Cualquier cliente MCP funciona: Claude Code, Codex, Cursor, o tu propio script.
Cero dobles reclamos, garantizado en pruebas
Un reclamo es un UPDATE … WHERE state = 'approved' condicional dentro de una transacción serializada, así que cuando dos sesiones van por la misma tarea, exactamente una gana y la otra recibe { claimed: false, claimedBy } nombrando al titular. Dos pruebas mantienen esa línea:
TestClaimRaceHasOneWinner— 8 goroutines compiten por una fila SQLite real: exactamente 1 ganador, 7 perdedores, y cada perdedor sabe quién la tiene.TestConcurrentWorkerClaimsProduceOneWinner— dos trabajadores reclaman la misma tarea concurrentemente a través del manejador HTTP: exactamente un200, un409, y el409nombra al ganador.
Ejecútalas con cd apps/api && go test ./internal/store/sqlite ./internal/api -run 'ClaimRace|ConcurrentWorkerClaims'.
Tandem también está planificado en su propia cola: más de 150 commits en este repositorio llevan un ticket TDM-n (git log --oneline | grep -c TDM-).
Qué hace setup
La línea del principio hace todo lo necesario para una primera ejecución:
- Registra el servidor MCP con Claude Code a nivel de usuario (
claude mcp add --scope user tandem -- npx -y @jaximus/tandem-mcp), para que cada proyecto pueda acceder a Tandem. Siclaudeno está en tuPATH, imprime el comando para que lo ejecutes. - Añade un fragmento corto de Tandem a
~/.claude/CLAUDE.md(idempotente) para que frases simples — "hazme un tablero sprint", "un tablero de agentes", "un tablero de tareas", "un lienzo tandem" — se asignen acanvas_create+epic_proposeen lugar de un archivo markdown. - Descarga el binario
tandemde esta plataforma desde el último lanzamiento en~/.tandem/bin, lo inicia enhttp://127.0.0.1:7891, y abre el tablero.
Reinicia Claude Code después y verifica:
claude mcp list
# tandem: npx -y @jaximus/tandem-mcp - ✓ Connected
Banderas: --project registra en el .mcp.json del repositorio actual en lugar del ámbito de usuario; --no-claude-md omite el fragmento; --no-open deja el navegador solo. Cursor, Codex y cualquier otro cliente MCP stdio lanzan el mismo comando: npx -y @jaximus/tandem-mcp.
Luego, en cualquier sesión de Claude Code: "hazme un tablero sprint para el refactor de autenticación" — el tablero se abre con el plan, esperando tu aprobación.
Cómo se ve
Haz clic para ver el clip de demostración completo.
Cómo funciona
Todo se ejecuta en una máquina. El servidor MCP es un proxy stdio ligero; el binario es el producto completo.
flowchart LR
CC[Claude Code / Cursor /<br/>any MCP client]
MCP["tandem-mcp<br/>(stdio MCP server)"]
T["tandem<br/>127.0.0.1:7891<br/>HTTP + WebSocket"]
DB[("~/.tandem/tandem.db<br/>SQLite")]
B[Board in your browser]
CC -- tool calls --> MCP -- HTTP --> T
T --- DB
B <-- live WebSocket --> T
El bucle: un agente planifica, tú apruebas una vez, las sesiones reclaman y completan, el tablero se mueve mientras ocurre.
sequenceDiagram
actor H as You
participant A as Agent session
participant T as Tandem
participant W as N worker sessions
H->>A: "plan the auth refactor"
A->>T: epic_propose (epic + tasks)
T-->>H: board shows the plan as proposed
H->>T: approve once (web UI)
W->>T: queue_next
W->>T: task_claim (atomic — one winner)
W->>T: task_progress (heartbeat)
W->>T: task_complete + commit link
T-->>H: board updates live
La vida de una tarea. La aprobación es la puerta; editar el contenido de una tarea aprobada la envía de vuelta por la puerta, y un reclamo sin heartbeat expira para que otra sesión pueda tomarlo.
stateDiagram-v2
[*] --> proposed
proposed --> approved: human approves
proposed --> rejected: human rejects
approved --> executing: task_claim
executing --> done: task_complete
executing --> failed: task_complete (failed)
executing --> executing: claim expires (~15 min, no heartbeat) — another session may take it over
approved --> proposed: title/body edited (re-enters the gate)
done --> approved: reviewer requests changes
Los roles son simples: el navegador es el humano (el propietario de cada lienzo en modo local); cualquier cosa que se conecte con un código de lienzo a través de MCP es un agente. Los agentes pueden proponer y reclamar; solo el humano puede aprobar, rechazar o eliminar.
Enseña a tus sesiones el bucle
setup le da a Claude las frases. Para un repositorio que tiene su propio tablero, fíjalo una vez:
npx -y @jaximus/tandem-mcp init --write
Eso crea un lienzo, escribe su código en el .mcp.json del repositorio como TANDEM_CANVAS_CODE, y añade el fragmento de abajo a AGENTS.md/CLAUDE.md. Si prefieres pegarlo tú mismo, reemplaza AB3XK9QZ con tu código de lienzo:
## Tandem task queue
The shared work queue for this repo is Tandem canvas `AB3XK9QZ`.
Every session:
1. `canvas_connect` with code `AB3XK9QZ` — once per session. Pass `role`
("executor" if you'll work tasks yourself, "planner" if you'll dispatch
them to subagents) and a `name`. Keep the `session` handle it returns and
pass it as `session` on every later Tandem call.
2. `queue_next` — the approved, ready-to-work queue. That is the entry point
for work; don't go looking for it by reading the canvas. (`context_get`
once if you need to orient on what this canvas is.)
3. Pick one task, `task_get` for its hydrated context (linked notes, roadmap
items, its epic), then `task_claim` to claim it. The claim is atomic: if
it returns `{ claimed: false, claimedBy }`, another session won — do NOT
work on that task; go back to `queue_next` and take the next one.
4. Do the work. Every commit message for the task starts with its ticket
ID, e.g. `TDM-7: add rate limiter`. On long work, `task_progress` with
one line per meaningful step — it doubles as a heartbeat, so a task that
runs past ~15 minutes doesn't become reclaimable underneath you.
5. `task_complete` with a `result` saying what was done and where — always
include the commit hash(es) — plus `links` to the commit or PR.
Planning from a spec: when asked to decompose SPEC.md (or any spec file)
into work, use `epic_propose` — one epic per spec section, with that
section's tasks passed in the SAME call via `tasks`, so the human approves
once instead of task by task. In the epic body, record the spec file path,
the section heading, and the current commit SHA of the spec file. One human
approval of the epic approves its tasks (canvas approval policy `epic`, the
default); add more tasks to an existing epic later with `task_propose` and
that `epicId`. Before working a claimed task that belongs to an epic, diff
the spec section against the SHA recorded in the epic body: if the section
changed since the epic was planned, do not proceed — flag the task back
with `task_complete` using `status: "failed"` and an `error` noting the spec
drift, so the plan gets redone against the current spec.
If a task needs to deviate from the approved plan, don't silently do it —
propose the deviation with `task_propose` and `requiresApproval: true` so a
human gates it.
Dispatching subagents instead of working tasks yourself? Connect with
`role: "planner"` and paste each ready task's `handoff` block from
`queue_next` into one subagent per task — you dispatch, they claim. Never
claim a task you won't personally do, and never hand a subagent your
`session` handle; the canvas code is what travels.
Ve en paralelo
En una sesión, pide a Claude que planifique ("lee SPEC.md y propón el trabajo en el lienzo de Tandem"), aprueba el épico una vez en el navegador, luego abre dos terminales en el mismo repositorio:
# terminal 1 and terminal 2:
claude "work through the approved Tandem queue until it's empty"
Ambos extraen la misma cola. Cada reclamo tiene exactamente un ganador — el perdedor ve { claimed: false, claimedBy: "…" } y sigue adelante. En el tablero, las tarjetas cambian a ejecutando con el nombre del reclamante, cada tarea lleva su ticket TDM-n, y los resultados con hashes de commit aterrizan cuando las tareas se completan.
¿Quieres que aprobar una tarea lance las sesiones en lugar de iniciarlas tú? tandem-mcp listen convierte un webhook task.approved en un comando — ver docs/ORCHESTRATION.md. Localmente eso es un paso: el servidor puede entregar a 127.0.0.1 de fábrica.
Comandos
Todo desde npx -y @jaximus/tandem-mcp <command> (o tandem-mcp <command> después de npm i -g @jaximus/tandem-mcp).
| Comando | Qué hace |
|---|---|
setup | Incorporación de primera ejecución: registra el servidor MCP con Claude Code, enséñale las frases, descarga e inicia el servidor, abre el tablero. --project, --no-claude-md, --no-open. |
up | Inicia el servidor local (descargando el binario en la primera ejecución) y abre el tablero. --port, --data-dir, --code CODE, --no-open, --foreground. |
open [CODE] | Abre el tablero — o un lienzo — en el navegador, iniciando el servidor primero si es necesario. |
down | Detiene el servidor que up inició. |
init | Configura el repositorio actual: crea un lienzo, escribe .mcp.json, imprime el fragmento de agente. --write lo añade a AGENTS.md/CLAUDE.md; --code conecta un lienzo existente; --name, --force. |
listen --exec '…' | Ejecuta un comando cuando el trabajo es aprobado (listener de webhook local). |
| (sin comando) | Ejecuta como el servidor MCP stdio — lo que tu cliente MCP lanza. |
El servidor MCP también inicia el backend por sí mismo la primera vez que una llamada de herramienta no encuentra nada escuchando localmente, así que una máquina fría aún funciona. TANDEM_AUTOSTART=0 desactiva eso; TANDEM_AUTOSTART_OPEN=1 abre el navegador también.
Dónde viven las cosas
| Ruta | Qué es |
|---|---|
~/.tandem/tandem.db | Cada lienzo, tarea, nota y webhook — un archivo SQLite. Haz una copia de seguridad copiándolo. |
~/.tandem/secret | El secreto JWT autogenerado (mantiene los tokens válidos entre reinicios). |
~/.tandem/images/, ~/.tandem/logs/ | Imágenes subidas; el registro del servidor separado. |
~/.tandem/bin/tandem | El binario descargado. TANDEM_BIN o un tandem en tu PATH tiene prioridad sobre él. |
TANDEM_DATA_DIR mueve todo el directorio; PORT mueve el puerto. El servidor responde a GET /healthz y GET /api/config ({auth, local, version}). Cada configuración del servidor está documentada en apps/api/.env.example — ninguna es obligatoria.
Ejecutar desde el código fuente
| Ruta | Qué es |
|---|---|
apps/api | Servidor Go — hub HTTP + WebSocket, almacén SQLite (internal/store/sqlite, modernc.org/sqlite puro en Go), el paquete web incrustado en tiempo de compilación |
apps/web | Tablero React + Vite + Tailwind |
apps/mcp-gateway | @jaximus/tandem-mcp — el servidor MCP stdio y el CLI anterior |
apps/tandem-next | El sitio de aterrizaje Next.js (tandemcanvas.com, desplegado en Vercel) |
internal/shared | Tipos TypeScript compartidos por web + gateway |
scripts/build.sh | Compila el paquete web, lo incrusta, compila el binario de forma cruzada |
pnpm install
scripts/build.sh # → dist/tandem for this machine (--all: every release target)
./dist/tandem # local mode, data in ~/.tandem
Bucle de desarrollo — el servidor Go más Vite con HMR proxy hacia él:
cd apps/api && go run ./cmd/server # http://localhost:7891
pnpm --filter web dev # http://localhost:5173 → proxies /api, /ws
Verifica antes de hacer commit:
cd apps/api && go build ./... && go test ./...
cd apps/web && pnpm build
cd apps/mcp-gateway && pnpm build && pnpm test
El esquema es apps/api/internal/store/sqlite/schema.sql, aplicado por migrate.go (una tabla schema_version; incrementa schemaVersion y añade un paso cuando cambie). Más en docs/DEV.md.
Lanzamientos
La versión del servidor es apps/api/VERSION. Etiquetarla publica los binarios:
git tag v0.1.1 && git push origin v0.1.1
release.yml compila darwin / linux / windows × amd64 / arm64 y los adjunta (más SHA256SUMS) al lanzamiento de GitHub — lo que up descarga (TANDEM_VERSION fija uno; por defecto el último). El lanzamiento npm del gateway es separado: mcp-v* etiquetas, release-mcp-gateway.yml.
Superficie de herramientas MCP
La superficie predeterminada es una fachada de intención de 18 herramientas con forma de bucle de trabajo: canvas_connect, canvas_create, agent_register, context_get, queue_next, queue_wait, task_find, task_get, task_claim, task_progress, task_complete, task_propose, task_amend, task_review, epic_propose, doc_write, doc_read, board_status. La superficie CRUD cruda (documentos, notas, elementos de hoja de ruta, acciones crudas) está a una opción de distancia — TANDEM_FULL_TOOLS=1 o --full-tools. Referencia por herramienta: apps/mcp-gateway/README.md.
Compartir una instancia con un equipo
Todo lo anterior es modo local: una máquina, un usuario, sin inicio de sesión. Ejecutar una instancia compartida con cuentas, importar tableros del antiguo Tandem alojado, y lo que el movimiento local-first eliminó están en docs/SELF-HOSTING.md.