On This Page
On 21 September 2026, at 00:33 UTC, Tobi Lütke posted this: “MCP vs CLI for LLMs is the wrong discussion. People are debating at the wrong layer.” Then, on 23 October, he pointed at a new repository called dquickts. It is a fork of QuickJS that can pause a program in the middle of a for loop, write its whole heap to bytes, and resume it in another process. “Excellent code mode env,” he wrote.
He is right about the layer. I want to agree with him first, in his own words, and then narrow the claim until it says something a sync can use.
The layer he means
Here is the argument as he made it, which is the only fair way to quote an argument you intend to bend:
Both work incredibly well as long as they are run through a repl like environment that can persist state. The funny thing about CLI is that CLI tools are usually accessed through BASH which happens to be a repl with persistent state (the file system), therefore cli works much better than mcp. But this isn’t even close to being an intrinsic property of MCP. Just means that we need better harnesses.
That is correct, and it explains an observation a lot of people have made without explaining it. An agent given a CLI writes a file, reads it back three steps later, greps it, pipes it. An agent given a bare MCP server gets one result per call and has to carry everything else in its context window. The CLI did not win on protocol. It won because bash came with a place to put things.
His bet follows from that. Someone builds an embeddable, SQLite-style engine that turns bash, TypeScript or tool calls into one plan, checked before it runs and built for durable execution. dquickts is a step toward it. Its README describes a REPL “whose functions and variables survive restarts”. It also describes a durable host call: “a script calls a host function whose answer arrives hours later. The process saves the script and exits.”
So far, full agreement. Now the narrowing.
Which state, for which job
“Persist state” hides a question: whose state, and in what form? For a coding agent, the state is the repository and the shell, and a REPL that survives restarts is a good answer. For an agent building a sync between two SaaS systems, the state it needs between steps is already defined by the job. Four records carry it, and none of them is a variable in a script.
The cursor. A sync reads a source in windows and has to know where the last window ended. In Tajo’s sync engine that is a stored cursor per sync and scope, written after each batch with zero failed records, and the next run picks up from it. A scheduled run stores its starting point before it does anything else, so a first run that fails leaves the next one reading from the same point. The principle behind this, send the delta and never the table, is in the reverse ETL explainer. I will leave the cursor there.
The pending proposal. On Tajo’s connector MCP tools, a write is two calls. brevo_propose validates the input against the declared action and records a proposal. Nothing is sent. The result carries a proposal id and, when a person has to decide, the page where they approve it. brevo_apply runs the approved writes of that connection in order and stops at the first one still waiting on a person. The record in between is the state. It holds the input and a digest of it, so an approved write cannot be edited between the decision and the apply. It does not hold the provider’s response values, because an append-only approval record must not turn into a store of personal data.
The lease. A run of a sync holds one exclusive lease for its whole lifetime, with an owner token, a time limit of fifteen minutes by default, and renewal. A stale owner cannot release a newer lease. If the lease store is unreachable, the run refuses to start rather than guessing. Which worker holds the run is a fact in the lease, not in any process’s memory.
The plan. syncs.dryRun reads the next window from the cursor, works out insert, update, delete or unchanged per record, and writes exactly one row: the plan, kept for an hour. It moves no cursor and writes nothing to either system. syncs.apply writes that plan once. It refuses if the cursor has moved, or if more than a tolerated share of the records changed in the meantime. A repeat returns the first result instead of writing again.
That is the state between steps: four records with ids, readable by any agent allowed to see them.
Why the MCP side keeps nothing in a session
Once the state is in the records, a session has nothing left to hold. So Tajo’s MCP endpoints do not hold one. Each request gets a fresh server and a transport with no session id. Reads are one request per call, with results capped so one answer cannot flood a turn. Writes are propose, then apply, and the id of the proposal is what you carry.
The protocol moved the same way. The 2026-07-28 revision of the MCP specification removed protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport. A server that needs continuity now mints its own handle and gets it back as an ordinary argument. That is what a proposal id is. What is an MCP server covers that revision and how a tool call differs from a durable sync; this piece is only about where the state goes.
The consequence is the one that matters for integration work. Say an agent in Claude Code proposes a write and its session ends. The person approves it an hour later. An agent in a different client, or the same one tomorrow, calls apply with the id and the write lands, once. A crashed agent loses its context window and nothing else. Nothing was suspended, so nothing had to be resumed.
What we do not have, and what it costs
The honest objection comes from the REPL side and goes like this: “Fine, your records survive. But every call re-reads what the last call already had. A script would fetch the contacts once, join them, filter them and return a number. Your agent makes five calls and pays for each.” That objection is right about the mechanism, and we have not measured the price. The evaluation that would count calls per task with and without carried state is not built. Until it is, the claim that call mode costs more tokens stands unanswered.
Three more things have to be stated plainly, because a reader who finishes this article should not imagine them:
- No customer-written code runs today. The connector reads are one request per call, with no sandbox in the path. A code runner exists in our stack, and it is an internal tool. In production it is off unless two separate flags are set, and it answers to administrators only. Each call gets a fresh context, bindings that allow reads only, and a two-second budget for synchronous code. Its own header says in capitals that it is a correctness boundary, not a security one.
- No REPL. Nothing an agent computes in one call is available to the next, unless it was written as a record with an id.
- No persistent agent session. The client keeps its conversation. Tajo keeps the proposals, plans, cursors and leases. There is no third thing.
What a durable engine is for
None of this is an argument against dquickts. Its README lists what snapshots are for, and the list is good. Hibernate an idle script and free its memory. Run a long computation in slices on whichever worker is free. Warm-start every instance from a snapshot instead of repeating the setup. Park a script on a host call whose answer arrives later. For a coding agent’s workspace, or a notebook that should survive a restart, that is the right tool.
A sync should still not depend on one, for three reasons the README itself gives me. First: “A session file only loads into the qjs build that wrote it.” A heap written by one engine version is a dead file after an upgrade; a cursor is a string. Second: “Snapshots are trusted input, like bytecode,” and “malicious ones are not detected.” A stored heap is something you must protect, not something you can review; a proposal is a row a person reads before approving it. Third, a snapshot holds whatever the script had read, customer records included. The records above are built the other way round: the plan stores digests, not values, and the proposal record never stores the provider’s response.
One of the rules we build by is that deterministic beats clever. A heap that resumes mid-loop is clever. A cursor, a proposal, a lease and a plan are dull, inspectable, and the same on every engine, in every client, after every crash.
Lütke asked the right question. Where does the state live? For a sync, the answer was never going to be in the process. It was always going to be in the records, because that is where the person who approves the write has to be able to read it.
Frequently Asked Questions
Is MCP or a CLI better for AI agents?
Neither, by itself. Tobi Lütke's point from 21 September 2026 holds: both work once the calls run through something that keeps state between steps. A CLI feels better today because bash keeps state in the file system. The protocol is not the difference; the harness is.
What state does an agent need between the steps of a sync?
The integration's own records: a cursor that says where the last run stopped, a proposal waiting for a person's approval, a lease that says which worker holds the run, and a dry-run plan the apply must match. None of that is a variable in a script. It is rows with ids.
Why does Tajo's MCP server keep nothing in a session?
Every request gets a fresh server and a transport with no session id. Whatever the next call needs is a record: a proposal id, a plan id, a cursor. So a second agent, in a different client, can pick up where the first one stopped, and a crashed one loses nothing but its own context window.
Does Tajo run agent-written code?
Not for customers. Reads on the connector tools are one request per call, with no sandbox. The code runner that exists is an internal tool behind two flags, reachable by administrators only, with read-only bindings and a fresh context per call. It says of itself that it is not a security boundary. There is no REPL and no persistent session.
What is a durable JavaScript engine like dquickts good for?
Its README names the uses: hibernate an idle script, slice a long computation across workers, warm-start from a snapshot, and park a script on a host call whose answer arrives hours later. All real. A sync should still not depend on one: a snapshot loads only into the engine build that wrote it, is opaque to a reviewer, and holds whatever customer data the script read.