Nesta página
Em 21 de setembro de 2026, às 00:33 UTC, Tobi Lütke publicou isto: “MCP versus CLI para LLMs é a discussão errada. As pessoas estão debatendo na camada errada.” Depois, em 23 de outubro, ele apontou para um repositório novo chamado dquickts. É um fork do QuickJS que consegue pausar um programa no meio de um loop for, gravar todo o seu heap em bytes e retomá-lo em outro processo. “Excelente ambiente de code mode”, escreveu ele.
Ele está certo sobre a camada. Quero concordar com ele primeiro, nas palavras dele, e depois estreitar a afirmação até que ela diga algo que uma sincronização possa usar.
A camada a que ele se refere
Aqui está o argumento como ele o fez, que é a única forma justa de citar um argumento que você pretende entortar:
Os dois funcionam incrivelmente bem desde que rodem em um ambiente parecido com um REPL que consiga persistir estado. O engraçado da CLI é que as ferramentas de CLI costumam ser acessadas pelo BASH, que por acaso é um REPL com estado persistente (o sistema de arquivos), por isso a CLI funciona muito melhor que o MCP. Mas isso nem de longe é uma propriedade intrínseca do MCP. Só significa que precisamos de harnesses melhores.
Isso está correto, e explica uma observação que muita gente fez sem explicar. Um agente com uma CLI grava um arquivo, lê de novo três etapas depois, passa grep nele, encadeia com um pipe. Um agente com um servidor MCP puro recebe um resultado por chamada e precisa carregar todo o resto na janela de contexto. A CLI não venceu pelo protocolo. Venceu porque o bash vinha com um lugar para guardar as coisas.
A aposta dele decorre disso. Alguém constrói um motor embarcável, no estilo do SQLite, que transforma bash, TypeScript ou chamadas de ferramentas em um único plano, verificado antes de rodar e feito para execução durável. O dquickts é um passo nessa direção. O README dele descreve um REPL “cujas funções e variáveis sobrevivem a reinicializações”. Ele também descreve uma chamada durável ao host: “um script chama uma função do host cuja resposta chega horas depois. O processo salva o script e encerra.”
Até aqui, concordância total. Agora, o estreitamento.
Qual estado, para qual trabalho
“Persistir estado” esconde uma pergunta: o estado de quem, e em que forma? Para um agente de programação, o estado é o repositório e o shell, e um REPL que sobrevive a reinicializações é uma boa resposta. Para um agente que constrói uma sincronização entre dois sistemas SaaS, o estado de que ele precisa entre as etapas já está definido pelo trabalho. Quatro registros o carregam, e nenhum deles é uma variável em um script.
O cursor. Uma sincronização lê uma fonte em janelas e precisa saber onde a última janela terminou. No motor de sincronização da Tajo, isso é um cursor armazenado por sincronização e por escopo, gravado após cada lote sem registros com falha, e a próxima execução continua a partir dele. Uma execução agendada armazena seu ponto de partida antes de fazer qualquer outra coisa, de modo que uma primeira execução que falha deixa a próxima lendo do mesmo ponto. O princípio por trás disso, enviar o delta e nunca a tabela, está no explicativo sobre reverse ETL. Vou deixar o cursor por lá.
A proposta pendente. Nas ferramentas MCP de conectores da Tajo, uma escrita são duas chamadas. brevo_propose valida a entrada em relação à ação declarada e registra uma proposta. Nada é enviado. O resultado traz um id de proposta e, quando uma pessoa precisa decidir, a página onde ela aprova. brevo_apply executa em ordem as escritas aprovadas daquela conexão e para na primeira que ainda aguarda uma pessoa. O registro no meio é o estado. Ele guarda a entrada e um digest dela, para que uma escrita aprovada não possa ser editada entre a decisão e o apply. Ele não guarda os valores de resposta do provedor, porque um registro de aprovações somente de inclusão não pode virar um depósito de dados pessoais.
O lease. Uma execução de uma sincronização mantém um lease exclusivo durante toda a sua vida, com um token de proprietário, um limite de tempo de quinze minutos por padrão e renovação. Um proprietário expirado não consegue liberar um lease mais novo. Se o armazenamento de leases estiver inacessível, a execução se recusa a começar em vez de adivinhar. Qual worker detém a execução é um fato no lease, não na memória de algum processo.
O plano. syncs.dryRun lê a próxima janela a partir do cursor, determina para cada registro se é inserção, atualização, exclusão ou sem alteração, e grava exatamente uma linha: o plano, mantido por uma hora. Ele não move nenhum cursor e não grava nada em nenhum dos dois sistemas. syncs.apply grava esse plano uma vez. Ele se recusa se o cursor tiver se movido, ou se mais do que uma parcela tolerada dos registros tiver mudado nesse meio-tempo. Uma repetição retorna o primeiro resultado em vez de gravar de novo.
Esse é o estado entre as etapas: quatro registros com ids, legíveis por qualquer agente autorizado a vê-los.
Por que o lado MCP não guarda nada em uma sessão
Quando o estado está nos registros, não sobra nada para uma sessão guardar. Então os endpoints MCP da Tajo não guardam nenhuma. Cada requisição recebe um servidor novo e um transporte sem id de sessão. Leituras são uma requisição por chamada, com resultados limitados para que uma resposta não possa inundar um turno. Escritas são primeiro propose, depois apply, e o id da proposta é o que você carrega.
O protocolo seguiu o mesmo caminho. A revisão 2026-07-28 da especificação MCP removeu as sessões no nível do protocolo e o cabeçalho Mcp-Session-Id do transporte Streamable HTTP. Um servidor que precisa de continuidade agora cria o próprio identificador e o recebe de volta como um argumento comum. É exatamente isso que um id de proposta é. O que é um servidor MCP cobre essa revisão e a diferença entre uma chamada de ferramenta e uma sincronização durável; este texto trata apenas de para onde vai o estado.
A consequência é a que importa para o trabalho de integração. Digamos que um agente no Claude Code proponha uma escrita e a sessão dele termine. A pessoa aprova uma hora depois. Um agente em outro cliente, ou o mesmo amanhã, chama apply com o id e a escrita acontece, uma vez. Um agente que trava perde a janela de contexto e nada mais. Nada foi suspenso, então nada precisou ser retomado.
O que não temos, e quanto isso custa
A objeção honesta vem do lado do REPL e é assim: “Tudo bem, seus registros sobrevivem. Mas cada chamada relê o que a chamada anterior já tinha. Um script buscaria os contatos uma vez, faria o join, filtraria e retornaria um número. Seu agente faz cinco chamadas e paga por cada uma.” Essa objeção está certa sobre o mecanismo, e nós não medimos o preço. A avaliação que contaria as chamadas por tarefa com e sem estado carregado não foi construída. Até que seja, a afirmação de que o modo por chamadas custa mais tokens continua sem resposta.
Mais três coisas precisam ser ditas com clareza, porque quem termina este artigo não deve imaginá-las:
- Nenhum código escrito por clientes roda hoje. As leituras de conectores são uma requisição por chamada, sem sandbox no caminho. Existe um executor de código na nossa stack, e ele é uma ferramenta interna. Em produção, ele fica desligado a menos que duas flags separadas estejam ativadas, e responde apenas a administradores. Cada chamada recebe um contexto novo, bindings que só permitem leituras e um orçamento de dois segundos para código síncrono. O próprio cabeçalho dele diz em maiúsculas que ele é uma fronteira de correção, não de segurança.
- Sem REPL. Nada do que um agente calcula em uma chamada fica disponível para a próxima, a menos que tenha sido gravado como um registro com id.
- Sem sessão de agente persistente. O cliente mantém a conversa dele. A Tajo mantém as propostas, os planos, os cursores e os leases. Não existe uma terceira coisa.
Para que serve um motor durável
Nada disso é um argumento contra o dquickts. O README dele lista para que servem os snapshots, e a lista é boa. Hibernar um script ocioso e liberar a memória dele. Rodar um cálculo longo em fatias no worker que estiver livre. Fazer partida a quente de cada instância a partir de um snapshot em vez de repetir a configuração. Estacionar um script em uma chamada ao host cuja resposta chega mais tarde. Para o espaço de trabalho de um agente de programação, ou um notebook que deve sobreviver a uma reinicialização, essa é a ferramenta certa.
Mesmo assim, uma sincronização não deveria depender de um, por três motivos que o próprio README me dá. Primeiro: “Um arquivo de sessão só carrega no build do qjs que o gravou.” Um heap gravado por uma versão do motor é um arquivo morto depois de uma atualização; um cursor é uma string. Segundo: “Snapshots são entrada confiável, como bytecode”, e “os maliciosos não são detectados”. Um heap armazenado é algo que você precisa proteger, não algo que você consegue revisar; uma proposta é uma linha que uma pessoa lê antes de aprová-la. Terceiro, um snapshot guarda tudo o que o script tinha lido, inclusive registros de clientes. Os registros acima são construídos ao contrário: o plano armazena digests, não valores, e o registro da proposta nunca armazena a resposta do provedor.
Uma das regras com que construímos é que o determinístico vence o engenhoso. Um heap que retoma no meio de um loop é engenhoso. Um cursor, uma proposta, um lease e um plano são entediantes, inspecionáveis e iguais em todo motor, em todo cliente, depois de toda falha.
Lütke fez a pergunta certa. Onde o estado vive? Em uma sincronização, a resposta nunca ia estar no processo. Sempre ia estar nos registros, porque é lá que a pessoa que aprova a escrita precisa conseguir lê-la.
Perguntas frequentes
MCP ou CLI: qual é melhor para agentes de IA?
Nenhum dos dois, por si só. O ponto de Tobi Lütke de 21 de setembro de 2026 se sustenta: os dois funcionam assim que as chamadas passam por algo que mantém o estado entre as etapas. Uma CLI parece melhor hoje porque o bash mantém o estado no sistema de arquivos. A diferença não está no protocolo, e sim no harness.
De que estado um agente precisa entre as etapas de uma sincronização?
Dos registros da própria integração: um cursor que diz onde a última execução parou, uma proposta aguardando a aprovação de uma pessoa, um lease que diz qual worker detém a execução e um plano de dry run ao qual o apply precisa corresponder. Nada disso é uma variável em um script. São linhas com ids.
Por que o servidor MCP da Tajo não guarda nada em uma sessão?
Cada requisição recebe um servidor novo e um transporte sem id de sessão. Tudo de que a próxima chamada precisa é um registro: um id de proposta, um id de plano, um cursor. Assim, um segundo agente, em outro cliente, pode continuar de onde o primeiro parou, e um agente que trava não perde nada além da própria janela de contexto.
A Tajo executa código escrito por agentes?
Não para clientes. As leituras nas ferramentas de conectores são uma requisição por chamada, sem sandbox. O executor de código que existe é uma ferramenta interna atrás de duas flags, acessível apenas a administradores, com bindings somente leitura e um contexto novo a cada chamada. Ele diz de si mesmo que não é uma fronteira de segurança. Não há REPL nem sessão persistente.
Para que serve um motor JavaScript durável como o dquickts?
O README dele lista os usos: hibernar um script ocioso, dividir um cálculo longo entre workers, fazer partida a quente a partir de um snapshot e estacionar um script em uma chamada ao host cuja resposta chega horas depois. Tudo real. Mesmo assim, uma sincronização não deveria depender de um: um snapshot só carrega no build do motor que o escreveu, é opaco para quem revisa e contém todos os dados de clientes que o script leu.