På denne side
Den 21. september 2026, kl. 00.33 UTC, skrev Tobi Lütke dette: »MCP vs CLI for LLM’er er den forkerte diskussion. Folk debatterer på det forkerte lag.« Den 23. oktober pegede han så på et nyt repository ved navn dquickts. Det er en fork af QuickJS, der kan sætte et program på pause midt i en for-løkke, skrive hele dets heap til bytes og genoptage det i en anden proces. »Fremragende code mode-miljø,« skrev han.
Han har ret med hensyn til laget. Jeg vil først give ham ret, med hans egne ord, og derefter indsnævre påstanden, til den siger noget, en synkronisering kan bruge.
Det lag, han mener
Her er argumentet, som han fremførte det, for det er den eneste fair måde at citere et argument, man har tænkt sig at bøje:
Begge virker utroligt godt, så længe de køres gennem et REPL-lignende miljø, der kan bevare tilstand. Det sjove ved CLI er, at CLI-værktøjer som regel tilgås gennem BASH, som tilfældigvis er en REPL med vedvarende tilstand (filsystemet), og derfor virker CLI meget bedre end MCP. Men det er ikke engang tæt på at være en iboende egenskab ved MCP. Det betyder bare, at vi har brug for bedre harnesses.
Det er korrekt, og det forklarer en observation, som mange har gjort uden at forklare den. En agent, der får en CLI, skriver en fil, læser den igen tre trin senere, grepper i den, piper den. En agent, der får en bar MCP-server, får ét resultat pr. kald og må bære alt andet i sit kontekstvindue. CLI’en vandt ikke på protokol. Den vandt, fordi bash kom med et sted at lægge ting.
Hans væddemål følger af det. Nogen bygger en indlejrbar motor i SQLite-stil, der gør bash, TypeScript eller tool calls til én plan, tjekket før den kører og bygget til holdbar eksekvering. dquickts er et skridt i den retning. Dens README beskriver en REPL, »hvis funktioner og variabler overlever genstarter«. Den beskriver også et holdbart host call: »et script kalder en værtsfunktion, hvis svar kommer timer senere. Processen gemmer scriptet og afslutter.«
Indtil videre fuld enighed. Nu indsnævringen.
Hvilken tilstand, til hvilket job
»Bevar tilstand« skjuler et spørgsmål: hvis tilstand, og i hvilken form? For en kodeagent er tilstanden repositoryet og shellen, og en REPL, der overlever genstarter, er et godt svar. For en agent, der bygger en synkronisering mellem to SaaS-systemer, er den tilstand, den har brug for mellem trinene, allerede defineret af jobbet. Fire poster bærer den, og ingen af dem er en variabel i et script.
Cursoren. En synkronisering læser en kilde i vinduer og skal vide, hvor det sidste vindue sluttede. I Tajos synkroniseringsmotor er det en gemt cursor pr. synkronisering og omfang, skrevet efter hver batch uden fejlede poster, og den næste kørsel fortsætter derfra. En planlagt kørsel gemmer sit startpunkt, før den gør noget andet, så en første kørsel, der fejler, lader den næste læse fra samme punkt. Princippet bag, send deltaet og aldrig tabellen, står i forklaringen om reverse ETL. Jeg lader cursoren blive der.
Det ventende forslag. På Tajos connector-MCP-værktøjer er en skrivning to kald. brevo_propose validerer inputtet mod den erklærede handling og registrerer et forslag. Intet bliver sendt. Resultatet indeholder et forslags-id og, når en person skal beslutte, den side, hvor vedkommende godkender det. brevo_apply kører de godkendte skrivninger for den forbindelse i rækkefølge og stopper ved den første, der stadig venter på en person. Posten imellem er tilstanden. Den indeholder inputtet og et digest af det, så en godkendt skrivning ikke kan ændres mellem beslutningen og apply. Den indeholder ikke udbyderens svarværdier, fordi en append-only godkendelsespost ikke må blive et lager af persondata.
Leasen. En kørsel af en synkronisering holder én eksklusiv lease i hele sin levetid, med et ejertoken, en tidsgrænse på som standard femten minutter og fornyelse. En forældet ejer kan ikke frigive en nyere lease. Hvis lease-lageret ikke kan nås, nægter kørslen at starte i stedet for at gætte. Hvilken worker der har kørslen, er et faktum i leasen, ikke i nogen proces’ hukommelse.
Planen. syncs.dryRun læser det næste vindue fra cursoren, afgør indsæt, opdater, slet eller uændret pr. post og skriver præcis én række: planen, som gemmes i en time. Den flytter ingen cursor og skriver intet til nogen af systemerne. syncs.apply skriver planen én gang. Den nægter, hvis cursoren har flyttet sig, eller hvis mere end en tolereret andel af posterne er ændret i mellemtiden. En gentagelse returnerer det første resultat i stedet for at skrive igen.
Det er tilstanden mellem trinene: fire poster med id’er, læsbare for enhver agent, der har lov til at se dem.
Hvorfor MCP-siden intet gemmer i en session
Når tilstanden ligger i posterne, har en session intet tilbage at holde på. Så Tajos MCP-endpoints holder ikke på nogen. Hver forespørgsel får en ny server og en transport uden sessions-id. Læsninger er én forespørgsel pr. kald, med begrænsede resultater, så ét svar ikke kan oversvømme en tur. Skrivninger er først forslag, så apply, og forslagets id er det, du bærer med dig.
Protokollen bevægede sig samme vej. Revision 2026-07-28 af MCP-specifikationen fjernede sessioner på protokolniveau og headeren Mcp-Session-Id fra Streamable HTTP-transporten. En server, der har brug for kontinuitet, laver nu sit eget handle og får det tilbage som et almindeligt argument. Det er præcis, hvad et forslags-id er. Hvad er en MCP-server dækker den revision, og hvordan et tool call adskiller sig fra en holdbar synkronisering; denne artikel handler kun om, hvor tilstanden havner.
Konsekvensen er den, der betyder noget for integrationsarbejde. Lad os sige, at en agent i Claude Code foreslår en skrivning, og at dens session slutter. Personen godkender den en time senere. En agent i en anden klient, eller den samme i morgen, kalder apply med id’et, og skrivningen lander, én gang. En agent, der går ned, mister sit kontekstvindue og intet andet. Intet blev sat på pause, så intet skulle genoptages.
Hvad vi ikke har, og hvad det koster
Den ærlige indvending kommer fra REPL-siden og lyder sådan: »Fint, jeres poster overlever. Men hvert kald genlæser det, som det sidste kald allerede havde. Et script ville hente kontakterne én gang, joine dem, filtrere dem og returnere et tal. Jeres agent laver fem kald og betaler for hvert.« Den indvending har ret med hensyn til mekanismen, og vi har ikke målt prisen. Den evaluering, der ville tælle kald pr. opgave med og uden medbragt tilstand, er ikke bygget. Indtil den er, står påstanden om, at call mode koster flere tokens, ubesvaret.
Tre ting mere skal siges ligeud, fordi en læser, der læser denne artikel færdig, ikke skal forestille sig dem:
- Ingen kode skrevet af kunder kører i dag. Connector-læsningerne er én forespørgsel pr. kald, uden sandkasse i stien. Der findes en code runner i vores stack, og det er et internt værktøj. I produktion er den slået fra, medmindre to separate flag er sat, og den svarer kun administratorer. Hvert kald får en ny kontekst, bindinger, der kun tillader læsning, og et budget på to sekunder til synkron kode. Dens egen header siger med versaler, at den er en korrekthedsgrænse, ikke en sikkerhedsgrænse.
- Ingen REPL. Intet, en agent beregner i ét kald, er tilgængeligt for det næste, medmindre det er skrevet som en post med et id.
- Ingen vedvarende agentsession. Klienten gemmer sin samtale. Tajo gemmer forslagene, planerne, cursorerne og leaserne. Der er ikke en tredje ting.
Hvad en holdbar motor er til
Intet af dette er et argument mod dquickts. Dens README opremser, hvad snapshots er til, og listen er god. Lad et inaktivt script gå i dvale og frigør dets hukommelse. Kør en lang beregning i bidder på den worker, der er ledig. Varmstart hver instans fra et snapshot i stedet for at gentage opsætningen. Parkér et script på et host call, hvis svar kommer senere. Til en kodeagents arbejdsområde, eller en notebook, der skal overleve en genstart, er det det rigtige værktøj.
En synkronisering bør alligevel ikke afhænge af en, af tre grunde, som README’en selv giver mig. For det første: »En sessionsfil kan kun indlæses i den qjs-build, der skrev den.« En heap skrevet af én motorversion er en død fil efter en opgradering; en cursor er en streng. For det andet: »Snapshots er betroet input, ligesom bytecode,« og »ondsindede opdages ikke.« En gemt heap er noget, du skal beskytte, ikke noget, du kan gennemgå; et forslag er en række, som en person læser, før vedkommende godkender det. For det tredje indeholder et snapshot alt, hvad scriptet havde læst, inklusive kundeposter. Posterne ovenfor er bygget omvendt: planen gemmer digests, ikke værdier, og forslagsposten gemmer aldrig udbyderens svar.
En af de regler, vi bygger efter, er, at deterministisk slår smart. En heap, der genoptages midt i en løkke, er smart. En cursor, et forslag, en lease og en plan er kedelige, kan inspiceres og er de samme på enhver motor, i enhver klient, efter ethvert nedbrud.
Lütke stillede det rigtige spørgsmål. Hvor bor tilstanden? For en synkronisering skulle svaret aldrig ligge i processen. Det skulle altid ligge i posterne, fordi det er der, personen, der godkender skrivningen, skal kunne læse den.
Ofte Stillede Spørgsmål
Er MCP eller en CLI bedst til AI-agenter?
Ingen af dem i sig selv. Tobi Lütkes pointe fra 21. september 2026 holder: begge virker, så snart kaldene kører gennem noget, der bevarer tilstand mellem trinene. En CLI føles bedre i dag, fordi bash gemmer tilstand i filsystemet. Forskellen ligger ikke i protokollen, men i harnessen.
Hvilken tilstand har en agent brug for mellem trinene i en synkronisering?
Integrationens egne poster: en cursor, der siger, hvor sidste kørsel stoppede, et forslag, der venter på en persons godkendelse, en lease, der siger, hvilken worker der har kørslen, og en dry-run-plan, som apply skal stemme overens med. Intet af det er en variabel i et script. Det er rækker med id'er.
Hvorfor gemmer Tajos MCP-server intet i en session?
Hver forespørgsel får en ny server og en transport uden sessions-id. Alt, hvad det næste kald har brug for, er en post: et forslags-id, et plan-id, en cursor. Så kan en anden agent, i en anden klient, fortsætte, hvor den første stoppede, og en agent, der går ned, mister intet andet end sit eget kontekstvindue.
Kører Tajo kode skrevet af agenter?
Ikke for kunder. Læsninger på connector-værktøjerne er én forespørgsel pr. kald, uden sandkasse. Den code runner, der findes, er et internt værktøj bag to flag, som kun administratorer kan nå, med skrivebeskyttede bindinger og en ny kontekst pr. kald. Den siger om sig selv, at den ikke er en sikkerhedsgrænse. Der er ingen REPL og ingen vedvarende session.
Hvad er en holdbar JavaScript-motor som dquickts god til?
Dens README nævner anvendelserne: lade et inaktivt script gå i dvale, dele en lang beregning op på tværs af workers, varmstarte fra et snapshot og parkere et script på et host call, hvis svar kommer timer senere. Alt sammen reelt. En synkronisering bør alligevel ikke afhænge af en: et snapshot kan kun indlæses i den motorbuild, der skrev det, er uigennemskueligt for en reviewer og indeholder alle de kundedata, scriptet har læst.