En esta página
El 21 de septiembre de 2026, a las 00:33 UTC, Tobi Lütke publicó esto: «MCP contra CLI para los LLM es la discusión equivocada. La gente debate en la capa equivocada». Después, el 23 de octubre, señaló un repositorio nuevo llamado dquickts. Es un fork de QuickJS que puede pausar un programa en mitad de un bucle for, escribir todo su heap en bytes y reanudarlo en otro proceso. «Excelente entorno para code mode», escribió.
Tiene razón sobre la capa. Quiero darle la razón primero, con sus propias palabras, y luego acotar la afirmación hasta que diga algo que una sincronización pueda usar.
La capa a la que se refiere
Este es el argumento tal como lo planteó, que es la única forma justa de citar un argumento que piensas reorientar:
Ambos funcionan increíblemente bien siempre que se ejecuten a través de un entorno tipo REPL que pueda persistir el estado. Lo curioso de la CLI es que a las herramientas CLI se suele acceder a través de BASH, que resulta ser un REPL con estado persistente (el sistema de archivos), y por eso la CLI funciona mucho mejor que MCP. Pero esto ni de lejos es una propiedad intrínseca de MCP. Solo significa que necesitamos mejores harnesses.
Eso es correcto, y explica una observación que mucha gente ha hecho sin explicarla. Un agente con una CLI escribe un archivo, lo vuelve a leer tres pasos después, le pasa grep, lo encadena con un pipe. Un agente con un servidor MCP pelado recibe un resultado por llamada y tiene que cargar todo lo demás en su ventana de contexto. La CLI no ganó por el protocolo. Ganó porque bash venía con un lugar donde guardar cosas.
Su apuesta se deriva de ahí. Alguien construye un motor embebible, al estilo de SQLite, que convierte bash, TypeScript o llamadas a herramientas en un solo plan, comprobado antes de ejecutarse y diseñado para la ejecución duradera. dquickts es un paso en esa dirección. Su README describe un REPL «cuyas funciones y variables sobreviven a los reinicios». También describe una llamada al host duradera: «un script llama a una función del host cuya respuesta llega horas después. El proceso guarda el script y termina».
Hasta aquí, acuerdo total. Ahora, el acotamiento.
Qué estado, para qué trabajo
“Persistir el estado” esconde una pregunta: ¿el estado de quién, y en qué forma? Para un agente de programación, el estado es el repositorio y el shell, y un REPL que sobrevive a los reinicios es una buena respuesta. Para un agente que construye una sincronización entre dos sistemas SaaS, el estado que necesita entre pasos ya está definido por el trabajo. Cuatro registros lo llevan, y ninguno es una variable en un script.
El cursor. Una sincronización lee una fuente por ventanas y tiene que saber dónde terminó la última ventana. En el motor de sincronización de Tajo es un cursor almacenado por sincronización y ámbito, escrito después de cada lote sin ningún registro fallido, y la siguiente ejecución continúa desde él. Una ejecución programada guarda su punto de partida antes de hacer cualquier otra cosa, de modo que si una primera ejecución falla, la siguiente lee desde el mismo punto. El principio detrás de esto, enviar el delta y nunca la tabla, está en el artículo explicativo sobre reverse ETL. Ahí dejo el cursor.
La propuesta pendiente. En las herramientas MCP de conectores de Tajo, una escritura son dos llamadas. brevo_propose valida la entrada contra la acción declarada y registra una propuesta. No se envía nada. El resultado incluye un id de propuesta y, cuando una persona tiene que decidir, la página donde la aprueba. brevo_apply ejecuta en orden las escrituras aprobadas de esa conexión y se detiene en la primera que todavía espera a una persona. El registro intermedio es el estado. Contiene la entrada y un digest de ella, para que una escritura aprobada no se pueda editar entre la decisión y la aplicación. No contiene los valores de respuesta del proveedor, porque un registro de aprobaciones de solo anexado no debe convertirse en un almacén de datos personales.
El lease. Una ejecución de una sincronización mantiene un lease exclusivo durante toda su vida, con un token de propietario, un límite de tiempo de quince minutos por defecto y renovación. Un propietario caducado no puede liberar un lease más reciente. Si el almacén de leases no está disponible, la ejecución se niega a arrancar en lugar de adivinar. Qué worker tiene la ejecución es un hecho en el lease, no en la memoria de ningún proceso.
El plan. syncs.dryRun lee la siguiente ventana desde el cursor, determina por registro si toca insertar, actualizar, borrar o dejar sin cambios, y escribe exactamente una fila: el plan, que se conserva una hora. No mueve ningún cursor y no escribe nada en ninguno de los dos sistemas. syncs.apply escribe ese plan una vez. Se niega si el cursor se ha movido, o si entretanto ha cambiado más de una proporción tolerada de los registros. Una repetición devuelve el primer resultado en lugar de escribir de nuevo.
Ese es el estado entre pasos: cuatro registros con ids, legibles por cualquier agente con permiso para verlos.
Por qué el lado MCP no guarda nada en una sesión
Una vez que el estado está en los registros, a una sesión no le queda nada que guardar. Así que los endpoints MCP de Tajo no mantienen ninguna. Cada solicitud recibe un servidor nuevo y un transporte sin id de sesión. Las lecturas son una solicitud por llamada, con resultados limitados para que una respuesta no pueda inundar un turno. Las escrituras son primero propose y luego apply, y lo que llevas contigo es el id de la propuesta.
El protocolo se movió en la misma dirección. La revisión 2026-07-28 de la especificación MCP eliminó las sesiones a nivel de protocolo y la cabecera Mcp-Session-Id del transporte Streamable HTTP. Un servidor que necesita continuidad ahora genera su propio identificador y lo recibe de vuelta como un argumento normal. Eso es lo que es un id de propuesta. Qué es un servidor MCP cubre esa revisión y en qué se diferencia una llamada a una herramienta de una sincronización duradera; este texto trata solo de adónde va el estado.
La consecuencia es la que importa para el trabajo de integración. Supón que un agente en Claude Code propone una escritura y su sesión termina. La persona la aprueba una hora después. Un agente en otro cliente, o el mismo mañana, llama a apply con el id y la escritura se aplica, una vez. Un agente que falla pierde su ventana de contexto y nada más. No se suspendió nada, así que no hubo nada que reanudar.
Lo que no tenemos, y lo que cuesta
La objeción honesta viene del lado del REPL y dice así: “Vale, tus registros sobreviven. Pero cada llamada vuelve a leer lo que la anterior ya tenía. Un script obtendría los contactos una vez, los uniría, los filtraría y devolvería un número. Tu agente hace cinco llamadas y paga por cada una”. Esa objeción acierta en el mecanismo, y no hemos medido el precio. La evaluación que contaría las llamadas por tarea con y sin estado arrastrado no está construida. Hasta que lo esté, la afirmación de que el modo por llamadas cuesta más tokens sigue sin respuesta.
Hay tres cosas más que decir claramente, porque quien termine este artículo no debería imaginárselas:
- Hoy no se ejecuta código escrito por clientes. Las lecturas de conectores son una solicitud por llamada, sin sandbox en el camino. En nuestro stack existe un ejecutor de código, y es una herramienta interna. En producción está desactivado salvo que se activen dos flags distintos, y solo responde a administradores. Cada llamada recibe un contexto nuevo, bindings que solo permiten lecturas y un presupuesto de dos segundos para código síncrono. Su propia cabecera dice en mayúsculas que es una frontera de corrección, no de seguridad.
- Sin REPL. Nada de lo que un agente calcula en una llamada está disponible para la siguiente, salvo que se haya escrito como registro con un id.
- Sin sesión de agente persistente. El cliente conserva su conversación. Tajo conserva las propuestas, los planes, los cursores y los leases. No hay una tercera cosa.
Para qué sirve un motor duradero
Nada de esto es un argumento contra dquickts. Su README enumera para qué sirven los snapshots, y la lista es buena. Hibernar un script inactivo y liberar su memoria. Ejecutar un cálculo largo por partes en el worker que esté libre. Arrancar cada instancia en caliente desde un snapshot en lugar de repetir la configuración. Aparcar un script en una llamada al host cuya respuesta llega más tarde. Para el espacio de trabajo de un agente de programación, o un notebook que debe sobrevivir a un reinicio, es la herramienta adecuada.
Aun así, una sincronización no debería depender de uno, por tres razones que me da el propio README. Primera: «Un archivo de sesión solo se carga en la build de qjs que lo escribió». Un heap escrito por una versión del motor es un archivo muerto tras una actualización; un cursor es una cadena de texto. Segunda: «Los snapshots son entrada de confianza, como el bytecode», y «los maliciosos no se detectan». Un heap almacenado es algo que tienes que proteger, no algo que puedas revisar; una propuesta es una fila que una persona lee antes de aprobarla. Tercera, un snapshot contiene todo lo que el script había leído, incluidos registros de clientes. Los registros de arriba están construidos al revés: el plan almacena digests, no valores, y el registro de la propuesta nunca almacena la respuesta del proveedor.
Una de las reglas con las que construimos es que lo determinista vence a lo ingenioso. Un heap que se reanuda en mitad de un bucle es ingenioso. Un cursor, una propuesta, un lease y un plan son aburridos, inspeccionables e iguales en cada motor, en cada cliente, después de cada fallo.
Lütke hizo la pregunta correcta. ¿Dónde vive el estado? En una sincronización, la respuesta nunca iba a estar en el proceso. Siempre iba a estar en los registros, porque ahí es donde la persona que aprueba la escritura tiene que poder leerla.
Preguntas frecuentes
¿Qué es mejor para los agentes de IA, MCP o una CLI?
Ninguno, por sí solo. El argumento de Tobi Lütke del 21 de septiembre de 2026 se sostiene: ambos funcionan en cuanto las llamadas pasan por algo que conserva el estado entre pasos. Hoy una CLI parece mejor porque bash guarda el estado en el sistema de archivos. La diferencia no está en el protocolo, sino en el harness.
¿Qué estado necesita un agente entre los pasos de una sincronización?
Los registros propios de la integración: un cursor que indica dónde se detuvo la última ejecución, una propuesta que espera la aprobación de una persona, un lease que indica qué worker tiene la ejecución y un plan de simulación con el que debe coincidir la aplicación. Nada de eso es una variable en un script. Son filas con ids.
¿Por qué el servidor MCP de Tajo no guarda nada en una sesión?
Cada solicitud recibe un servidor nuevo y un transporte sin id de sesión. Lo que necesite la siguiente llamada es un registro: un id de propuesta, un id de plan, un cursor. Así, un segundo agente, en otro cliente, puede retomar donde se detuvo el primero, y uno que falla no pierde nada más que su propia ventana de contexto.
¿Ejecuta Tajo código escrito por agentes?
No para los clientes. Las lecturas en las herramientas de conectores son una solicitud por llamada, sin sandbox. El ejecutor de código que existe es una herramienta interna detrás de dos flags, accesible solo para administradores, con bindings de solo lectura y un contexto nuevo en cada llamada. Dice de sí mismo que no es una frontera de seguridad. No hay REPL ni sesión persistente.
¿Para qué sirve un motor de JavaScript duradero como dquickts?
Su README nombra los usos: hibernar un script inactivo, repartir un cálculo largo en partes entre workers, arrancar en caliente desde un snapshot y aparcar un script en una llamada al host cuya respuesta llega horas después. Todo real. Aun así, una sincronización no debería depender de uno: un snapshot solo se carga en la build del motor que lo escribió, es opaco para quien lo revisa y contiene todos los datos de clientes que el script leyó.