Tandem MCP
Fila de tarefas local + quadro ao vivo para que sessões paralelas de Claude Code, Cursor e Codex parem de reivindicar o mesmo trabalho em duplicidade. Reivindicações atômicas, portão de aprovação do plano, resultados vinculados a commits. Um binário Go, SQLite, sem conta.
Documentação
Tandem
Uma fila de tarefas para as sessões do Claude Code que você já executa em paralelo.
Terminais, worktrees, subagentes: eles reivindicam trabalho de uma fila atomicamente, leem um único briefing compartilhado e só pegam o que você aprovou.
tandemcanvas.com · Releases · npm: @jaximus/tandem-mcp
npx -y @jaximus/tandem-mcp setup
Se você executa mais de uma sessão de agente em um repositório, elas provavelmente se coordenam por meio de um TODO.md que várias delas estão editando ao mesmo tempo, ou que cada worktree tem sua própria cópia. Um arquivo não tem noção de quem detém o quê, e não há reivindicação atômica. O Tandem mantém a intenção no git e move o vai-e-vem (reivindicações, status, resultados) para uma fila que toda sessão na máquina lê ao vivo. Os agentes propõem trabalho, você aprova o plano uma vez, N sessões puxam a fila aprovada sem colidir, e cada tarefa concluída carrega um link de commit.
Um binário Go, um arquivo SQLite em ~/.tandem, um quadro no seu navegador. Sem conta, sem servidor para alugar, nada envia dados para fora. Qualquer cliente MCP funciona: Claude Code, Codex, Cursor ou seu próprio script.
Zero reivindicações duplicadas, garantido por testes
Uma reivindicação é um UPDATE … WHERE state = 'approved' condicional dentro de uma transação serializada, então quando duas sessões disputam a mesma tarefa, exatamente uma vence e a outra recebe { claimed: false, claimedBy } nomeando o detentor. Dois testes sustentam essa linha:
TestClaimRaceHasOneWinner— 8 goroutines disputam uma linha real do SQLite: exatamente 1 vencedor, 7 perdedores, e cada perdedor é informado de quem a detém.TestConcurrentWorkerClaimsProduceOneWinner— dois workers reivindicam a mesma tarefa simultaneamente pelo handler HTTP: exatamente um200, um409, e o409nomeia o vencedor.
Execute-os com cd apps/api && go test ./internal/store/sqlite ./internal/api -run 'ClaimRace|ConcurrentWorkerClaims'.
O Tandem também é planejado em sua própria fila: mais de 150 commits neste repositório carregam um ticket TDM-n (git log --oneline | grep -c TDM-).
O que setup faz
A linha no topo faz tudo o que uma primeira execução precisa:
- Registra o servidor MCP com o Claude Code no escopo do usuário (
claude mcp add --scope user tandem -- npx -y @jaximus/tandem-mcp), para que todo projeto possa acessar o Tandem. Seclaudenão estiver no seuPATH, ele imprime o comando para você executar. - Acrescenta um trecho curto Tandem ao
~/.claude/CLAUDE.md(idempotente) para que frases simples — "me faça um quadro de sprint", "um quadro de agentes", "um quadro de tarefas", "um tandem canvas" — mapeiem paracanvas_create+epic_proposeem vez de um arquivo markdown. - Baixa o binário
tandemdesta plataforma da última versão para~/.tandem/bin, inicia-o emhttp://127.0.0.1:7891e abre o quadro.
Reinicie o Claude Code depois e verifique:
claude mcp list
# tandem: npx -y @jaximus/tandem-mcp - ✓ Connected
Flags: --project registra no .mcp.json do repositório atual em vez do escopo do usuário; --no-claude-md pula o trecho; --no-open deixa o navegador de lado. Cursor, Codex e qualquer outro cliente MCP stdio usam o mesmo comando: npx -y @jaximus/tandem-mcp.
Então, em qualquer sessão do Claude Code: "me faça um quadro de sprint para o refactor de autenticação" — o quadro abre com o plano nele, aguardando sua aprovação.
Como parece
Clique para o clipe de demonstração completo.
Como funciona
Tudo roda em uma única máquina. O servidor MCP é um proxy stdio fino; o binário é o produto inteiro.
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
O ciclo: um agente planeja, você aprova uma vez, as sessões reivindicam e concluem, o quadro se move conforme acontece.
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
A vida de uma tarefa. A aprovação é o portão; editar o conteúdo de uma tarefa aprovada a envia de volta pelo portão, e uma reivindicação sem heartbeat expira para que outra sessão possa assumi-la.
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
Os papéis são simples: o navegador é o humano (o dono de todo canvas no modo local); qualquer coisa que se conecta com um código de canvas via MCP é um agente. Agentes podem propor e reivindicar; só o humano pode aprovar, rejeitar ou excluir.
Ensine suas sessões ao ciclo
setup dá ao Claude as frases. Para um repositório que tem seu próprio quadro, fixe-o uma vez:
npx -y @jaximus/tandem-mcp init --write
Isso cria um canvas, escreve seu código no .mcp.json do repositório como TANDEM_CANVAS_CODE e acrescenta o trecho abaixo ao AGENTS.md/CLAUDE.md. Se preferir colar você mesmo, substitua AB3XK9QZ pelo seu código de canvas:
## 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.
Vá em paralelo
Em uma sessão, peça ao Claude para planejar ("leia SPEC.md e proponha o trabalho no canvas do Tandem"), aprove o épico uma vez no navegador, então abra dois terminais no mesmo repositório:
# terminal 1 and terminal 2:
claude "work through the approved Tandem queue until it's empty"
Ambos puxam a mesma fila. Cada reivindicação tem exatamente um vencedor — o perdedor vê { claimed: false, claimedBy: "…" } e segue em frente. No quadro, os cartões viram para executando com o nome do reivindicante, cada tarefa carrega seu ticket TDM-n, e resultados com hashes de commit chegam conforme as tarefas são concluídas.
Quer que aprovar uma tarefa lance as sessões em vez de você iniciá-las? tandem-mcp listen transforma um webhook task.approved em um comando — veja docs/ORCHESTRATION.md. Localmente, isso é um passo: o servidor pode entregar para 127.0.0.1 pronto para uso.
Comandos
Todos a partir de npx -y @jaximus/tandem-mcp <command> (ou tandem-mcp <command> após npm i -g @jaximus/tandem-mcp).
| Comando | O que faz |
|---|---|
setup | Onboarding de primeira execução: registra o servidor MCP com o Claude Code, ensina as frases, baixa e inicia o servidor, abre o quadro. --project, --no-claude-md, --no-open. |
up | Inicia o servidor local (baixando o binário na primeira execução) e abre o quadro. --port, --data-dir, --code CODE, --no-open, --foreground. |
open [CODE] | Abre o quadro — ou um canvas — no navegador, iniciando o servidor primeiro se necessário. |
down | Para o servidor que up iniciou. |
init | Configura o repositório atual: cria um canvas, escreve .mcp.json, imprime o trecho do agente. --write o acrescenta ao AGENTS.md/CLAUDE.md; --code conecta um canvas existente; --name, --force. |
listen --exec '…' | Executa um comando quando o trabalho é aprovado (listener de webhook local). |
| (sem comando) | Executa como o servidor MCP stdio — o que seu cliente MCP inicia. |
O servidor MCP também inicia o backend por conta própria na primeira vez que uma chamada de ferramenta não encontra nada ouvindo localmente, então uma máquina fria ainda funciona. TANDEM_AUTOSTART=0 desliga isso; TANDEM_AUTOSTART_OPEN=1 também abre o navegador.
Onde as coisas ficam
| Caminho | O que é |
|---|---|
~/.tandem/tandem.db | Todo canvas, tarefa, nota e webhook — um único arquivo SQLite. Faça backup copiando-o. |
~/.tandem/secret | O segredo JWT gerado automaticamente (mantém tokens válidos entre reinicializações). |
~/.tandem/images/, ~/.tandem/logs/ | Imagens enviadas; o log do servidor destacado. |
~/.tandem/bin/tandem | O binário baixado. TANDEM_BIN ou um tandem no seu PATH vence sobre ele. |
TANDEM_DATA_DIR move o diretório inteiro; PORT move a porta. O servidor responde em GET /healthz e GET /api/config ({auth, local, version}). Cada configuração do servidor está documentada em apps/api/.env.example — nenhuma é obrigatória.
Execute a partir do código-fonte
| Caminho | O que é |
|---|---|
apps/api | Servidor Go — hub HTTP + WebSocket, armazenamento SQLite (internal/store/sqlite, modernc.org/sqlite puro em Go), o bundle web embutido no build |
apps/web | Quadro React + Vite + Tailwind |
apps/mcp-gateway | @jaximus/tandem-mcp — o servidor MCP stdio e a CLI acima |
apps/tandem-next | O site de destino Next.js (tandemcanvas.com, implantado na Vercel) |
internal/shared | Tipos TypeScript compartilhados por web + gateway |
scripts/build.sh | Compila o bundle web, embute-o, cross-compila o binário |
pnpm install
scripts/build.sh # → dist/tandem for this machine (--all: every release target)
./dist/tandem # local mode, data in ~/.tandem
Ciclo de desenvolvimento — o servidor Go mais Vite com HMR fazendo proxy para ele:
cd apps/api && go run ./cmd/server # http://localhost:7891
pnpm --filter web dev # http://localhost:5173 → proxies /api, /ws
Verifique antes de commitar:
cd apps/api && go build ./... && go test ./...
cd apps/web && pnpm build
cd apps/mcp-gateway && pnpm build && pnpm test
O schema é apps/api/internal/store/sqlite/schema.sql, aplicado por migrate.go (uma tabela schema_version; aumente schemaVersion e adicione um passo quando mudar). Mais em docs/DEV.md.
Releases
A versão do servidor é apps/api/VERSION. Marcar a tag publica os binários:
git tag v0.1.1 && git push origin v0.1.1
release.yml compila darwin / linux / windows × amd64 / arm64 e os anexa (mais SHA256SUMS) ao GitHub Release — o que up baixa (TANDEM_VERSION fixa um; padrão é a última). O release npm do gateway é separado: tags mcp-v*, release-mcp-gateway.yml.
Superfície de ferramentas MCP
A superfície padrão é uma fachada de intenção com 18 ferramentas moldada como o ciclo de trabalho: 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. A superfície CRUD bruta (documentos, notas, itens de roadmap, ações cruas) está a um opt-in de distância — TANDEM_FULL_TOOLS=1 ou --full-tools. Referência por ferramenta: apps/mcp-gateway/README.md.
Compartilhando uma instância com uma equipe
Tudo acima é modo local: uma máquina, um usuário, sem login. Executar uma instância compartilhada com contas, importar quadros do antigo Tandem hospedado e o que o movimento local-first cortou estão em docs/SELF-HOSTING.md.