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

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

Two agent sessions claim from the same Tandem queue; one claim wins and the other is told who holds the task and moves on

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 um 200, um 409, e o 409 nomeia 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:

  1. 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. Se claude não estiver no seu PATH, ele imprime o comando para você executar.
  2. 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 para canvas_create + epic_propose em vez de um arquivo markdown.
  3. Baixa o binário tandem desta plataforma da última versão para ~/.tandem/bin, inicia-o em http://127.0.0.1:7891 e 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

The Tandem board: an approved epic fanning out to parallel sessions
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).

ComandoO que faz
setupOnboarding 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.
upInicia 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.
downPara o servidor que up iniciou.
initConfigura 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

CaminhoO que é
~/.tandem/tandem.dbTodo canvas, tarefa, nota e webhook — um único arquivo SQLite. Faça backup copiando-o.
~/.tandem/secretO 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/tandemO 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

CaminhoO que é
apps/apiServidor Go — hub HTTP + WebSocket, armazenamento SQLite (internal/store/sqlite, modernc.org/sqlite puro em Go), o bundle web embutido no build
apps/webQuadro React + Vite + Tailwind
apps/mcp-gateway@jaximus/tandem-mcp — o servidor MCP stdio e a CLI acima
apps/tandem-nextO site de destino Next.js (tandemcanvas.com, implantado na Vercel)
internal/sharedTipos TypeScript compartilhados por web + gateway
scripts/build.shCompila 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.