Auf dieser Seite
Am 21. September 2026 um 00:33 UTC postete Tobi Lütke Folgendes: „MCP gegen CLI für LLMs ist die falsche Diskussion. Die Leute debattieren auf der falschen Ebene.“ Am 23. Oktober verwies er dann auf ein neues Repository namens dquickts. Es ist ein Fork von QuickJS, der ein Programm mitten in einer for-Schleife anhalten, seinen gesamten Heap in Bytes schreiben und es in einem anderen Prozess fortsetzen kann. „Exzellente Code-Mode-Umgebung“, schrieb er.
Mit der Ebene hat er recht. Ich will ihm zuerst zustimmen, in seinen eigenen Worten, und die Aussage dann so weit eingrenzen, bis sie etwas sagt, das ein Sync gebrauchen kann.
Die Ebene, die er meint
Hier ist das Argument so, wie er es formuliert hat. Nur so lässt sich ein Argument fair zitieren, das man umbiegen will:
Beides funktioniert unglaublich gut, solange es durch eine REPL-artige Umgebung läuft, die Zustand persistieren kann. Das Witzige an der CLI ist, dass CLI-Tools meist über BASH angesprochen werden, und das ist zufällig eine REPL mit persistentem Zustand (dem Dateisystem), deshalb funktioniert CLI viel besser als MCP. Aber das ist nicht einmal annähernd eine inhärente Eigenschaft von MCP. Es heißt nur, dass wir bessere Harnesses brauchen.
Das stimmt, und es erklärt eine Beobachtung, die viele gemacht haben, ohne sie zu erklären. Ein Agent mit einer CLI schreibt eine Datei, liest sie drei Schritte später wieder, durchsucht sie mit grep, leitet sie per Pipe weiter. Ein Agent mit einem nackten MCP-Server bekommt ein Ergebnis pro Aufruf und muss alles andere in seinem Kontextfenster mittragen. Die CLI hat nicht wegen des Protokolls gewonnen. Sie hat gewonnen, weil bash einen Ort mitbrachte, an dem man Dinge ablegen kann.
Seine Wette folgt daraus. Jemand baut eine einbettbare Engine im Stil von SQLite, die bash, TypeScript oder Tool-Aufrufe in einen einzigen Plan verwandelt, der vor der Ausführung geprüft wird und für dauerhafte Ausführung gebaut ist. dquickts ist ein Schritt dorthin. Sein README beschreibt eine REPL, „deren Funktionen und Variablen Neustarts überleben“. Es beschreibt außerdem einen dauerhaften Host-Aufruf: „Ein Skript ruft eine Host-Funktion auf, deren Antwort Stunden später kommt. Der Prozess speichert das Skript und beendet sich.“
Bis hierhin volle Zustimmung. Jetzt die Eingrenzung.
Welcher Zustand, für welche Aufgabe
„Zustand persistieren“ verbirgt eine Frage: wessen Zustand, und in welcher Form? Für einen Coding-Agenten ist der Zustand das Repository und die Shell, und eine REPL, die Neustarts überlebt, ist eine gute Antwort. Für einen Agenten, der einen Sync zwischen zwei SaaS-Systemen baut, ist der Zustand, den er zwischen den Schritten braucht, durch die Aufgabe bereits festgelegt. Vier Datensätze tragen ihn, und keiner davon ist eine Variable in einem Skript.
Der Cursor. Ein Sync liest eine Quelle in Fenstern und muss wissen, wo das letzte Fenster geendet hat. In Tajos Sync-Engine ist das ein gespeicherter Cursor pro Sync und Bereich, der nach jedem Batch ohne fehlgeschlagene Datensätze geschrieben wird, und der nächste Lauf setzt dort an. Ein geplanter Lauf speichert seinen Startpunkt, bevor er irgendetwas anderes tut, sodass ein fehlgeschlagener erster Lauf den nächsten an derselben Stelle lesen lässt. Das Prinzip dahinter, das Delta senden und nie die Tabelle, steht im Reverse-ETL-Erklärstück. Beim Cursor belasse ich es dabei.
Der offene Vorschlag. Bei Tajos Connector-MCP-Tools besteht ein Schreibvorgang aus zwei Aufrufen. brevo_propose validiert die Eingabe gegen die deklarierte Aktion und speichert einen Vorschlag. Nichts wird gesendet. Das Ergebnis enthält eine Vorschlags-ID und, wenn eine Person entscheiden muss, die Seite, auf der sie ihn freigibt. brevo_apply führt die freigegebenen Schreibvorgänge dieser Verbindung der Reihe nach aus und stoppt beim ersten, der noch auf eine Person wartet. Der Datensatz dazwischen ist der Zustand. Er enthält die Eingabe und einen Digest davon, sodass ein freigegebener Schreibvorgang zwischen Entscheidung und Apply nicht verändert werden kann. Er enthält nicht die Antwortwerte des Anbieters, denn ein Freigabe-Datensatz, an den nur angehängt wird, darf nicht zu einem Speicher personenbezogener Daten werden.
Der Lease. Ein Lauf eines Syncs hält während seiner gesamten Lebensdauer einen exklusiven Lease, mit einem Owner-Token, einem Zeitlimit von standardmäßig fünfzehn Minuten und Verlängerung. Ein veralteter Owner kann keinen neueren Lease freigeben. Ist der Lease-Speicher nicht erreichbar, verweigert der Lauf den Start, statt zu raten. Welcher Worker den Lauf hält, ist ein Fakt im Lease, nicht im Speicher irgendeines Prozesses.
Der Plan. syncs.dryRun liest das nächste Fenster ab dem Cursor, bestimmt pro Datensatz Einfügen, Aktualisieren, Löschen oder Unverändert und schreibt genau eine Zeile: den Plan, der eine Stunde aufbewahrt wird. Er bewegt keinen Cursor und schreibt nichts in eines der beiden Systeme. syncs.apply schreibt diesen Plan einmal. Es verweigert, wenn sich der Cursor bewegt hat oder wenn sich in der Zwischenzeit mehr als ein tolerierter Anteil der Datensätze geändert hat. Eine Wiederholung gibt das erste Ergebnis zurück, statt erneut zu schreiben.
Das ist der Zustand zwischen den Schritten: vier Datensätze mit IDs, lesbar für jeden Agenten, der sie sehen darf.
Warum die MCP-Seite nichts in einer Session festhält
Sobald der Zustand in den Datensätzen liegt, bleibt für eine Session nichts mehr zu halten. Also halten Tajos MCP-Endpunkte keine. Jede Anfrage bekommt einen frischen Server und einen Transport ohne Session-ID. Lesezugriffe sind eine Anfrage pro Aufruf, mit begrenzten Ergebnissen, damit eine Antwort keinen Turn überfluten kann. Schreibvorgänge sind erst Propose, dann Apply, und die ID des Vorschlags ist das, was du weitergibst.
Das Protokoll hat sich in dieselbe Richtung bewegt. Die Revision 2026-07-28 der MCP-Spezifikation hat Sessions auf Protokollebene und den Header Mcp-Session-Id aus dem Streamable-HTTP-Transport entfernt. Ein Server, der Kontinuität braucht, erzeugt jetzt sein eigenes Handle und bekommt es als gewöhnliches Argument zurück. Genau das ist eine Vorschlags-ID. Was ist ein MCP-Server behandelt diese Revision und den Unterschied zwischen einem Tool-Aufruf und einem dauerhaften Sync; in diesem Text geht es nur darum, wo der Zustand hingehört.
Die Folge ist die, auf die es bei Integrationsarbeit ankommt. Angenommen, ein Agent in Claude Code schlägt einen Schreibvorgang vor, und seine Session endet. Die Person gibt ihn eine Stunde später frei. Ein Agent in einem anderen Client, oder derselbe morgen, ruft Apply mit der ID auf, und der Schreibvorgang landet, einmal. Ein abgestürzter Agent verliert sein Kontextfenster und sonst nichts. Nichts wurde angehalten, also musste nichts fortgesetzt werden.
Was wir nicht haben, und was es kostet
Der ehrliche Einwand kommt von der REPL-Seite und lautet so: „Gut, deine Datensätze überleben. Aber jeder Aufruf liest erneut, was der letzte Aufruf schon hatte. Ein Skript würde die Kontakte einmal abrufen, sie joinen, filtern und eine Zahl zurückgeben. Dein Agent macht fünf Aufrufe und zahlt für jeden.“ Dieser Einwand trifft den Mechanismus, und den Preis haben wir nicht gemessen. Die Auswertung, die Aufrufe pro Aufgabe mit und ohne mitgeführten Zustand zählen würde, ist nicht gebaut. Bis sie es ist, bleibt die Behauptung, dass der Aufrufmodus mehr Tokens kostet, unbeantwortet.
Drei weitere Dinge müssen klar gesagt werden, denn wer diesen Artikel zu Ende liest, soll sie sich nicht einbilden:
- Heute läuft kein von Kund:innen geschriebener Code. Die Connector-Lesezugriffe sind eine Anfrage pro Aufruf, ohne Sandbox im Pfad. In unserem Stack existiert ein Code-Runner, und er ist ein internes Tool. In Produktion ist er aus, solange nicht zwei getrennte Flags gesetzt sind, und er antwortet nur Administrator:innen. Jeder Aufruf bekommt einen frischen Kontext, Bindings, die nur Lesezugriffe erlauben, und ein Budget von zwei Sekunden für synchronen Code. Sein eigener Header sagt in Großbuchstaben, dass er eine Korrektheitsgrenze ist, keine Sicherheitsgrenze.
- Keine REPL. Nichts, was ein Agent in einem Aufruf berechnet, steht dem nächsten zur Verfügung, es sei denn, es wurde als Datensatz mit ID geschrieben.
- Keine persistente Agenten-Session. Der Client behält seine Konversation. Tajo behält die Vorschläge, Pläne, Cursor und Leases. Ein drittes Ding gibt es nicht.
Wofür eine dauerhafte Engine da ist
Nichts davon ist ein Argument gegen dquickts. Sein README listet auf, wofür Snapshots da sind, und die Liste ist gut. Ein untätiges Skript in den Ruhezustand versetzen und seinen Speicher freigeben. Eine lange Berechnung in Abschnitten auf dem Worker ausführen, der gerade frei ist. Jede Instanz per Warmstart aus einem Snapshot starten, statt die Einrichtung zu wiederholen. Ein Skript bei einem Host-Aufruf parken, dessen Antwort später kommt. Für den Arbeitsbereich eines Coding-Agenten oder ein Notebook, das einen Neustart überleben soll, ist das das richtige Werkzeug.
Ein Sync sollte trotzdem nicht davon abhängen, aus drei Gründen, die mir das README selbst liefert. Erstens: „Eine Session-Datei lädt nur in den qjs-Build, der sie geschrieben hat.“ Ein Heap, den eine Engine-Version geschrieben hat, ist nach einem Upgrade eine tote Datei; ein Cursor ist ein String. Zweitens: „Snapshots sind vertrauenswürdige Eingaben, wie Bytecode“, und „bösartige werden nicht erkannt“. Einen gespeicherten Heap musst du schützen, prüfen kannst du ihn nicht; ein Vorschlag ist eine Zeile, die eine Person liest, bevor sie ihn freigibt. Drittens enthält ein Snapshot alles, was das Skript gelesen hatte, Kundendatensätze eingeschlossen. Die Datensätze oben sind umgekehrt gebaut: Der Plan speichert Digests, keine Werte, und der Vorschlags-Datensatz speichert nie die Antwort des Anbieters.
Eine der Regeln, nach denen wir bauen, lautet: Deterministisch schlägt clever. Ein Heap, der mitten in einer Schleife weiterläuft, ist clever. Ein Cursor, ein Vorschlag, ein Lease und ein Plan sind langweilig, nachprüfbar und gleich auf jeder Engine, in jedem Client, nach jedem Absturz.
Lütke hat die richtige Frage gestellt. Wo liegt der Zustand? Bei einem Sync lag die Antwort nie im Prozess. Sie lag immer in den Datensätzen, denn dort muss die Person, die den Schreibvorgang freigibt, ihn lesen können.
Häufig gestellte Fragen
Ist MCP oder eine CLI besser für KI-Agenten?
Für sich genommen keins von beiden. Tobi Lütkes Punkt vom 21. September 2026 gilt: Beides funktioniert, sobald die Aufrufe durch etwas laufen, das den Zustand zwischen den Schritten hält. Eine CLI fühlt sich heute besser an, weil bash den Zustand im Dateisystem hält. Der Unterschied liegt nicht im Protokoll, sondern im Harness.
Welchen Zustand braucht ein Agent zwischen den Schritten eines Syncs?
Die eigenen Datensätze der Integration: einen Cursor, der sagt, wo der letzte Lauf aufgehört hat, einen Vorschlag, der auf die Freigabe durch eine Person wartet, einen Lease, der sagt, welcher Worker den Lauf hält, und einen Dry-Run-Plan, dem das Apply entsprechen muss. Nichts davon ist eine Variable in einem Skript. Es sind Zeilen mit IDs.
Warum hält Tajos MCP-Server nichts in einer Session fest?
Jede Anfrage bekommt einen frischen Server und einen Transport ohne Session-ID. Was der nächste Aufruf braucht, ist ein Datensatz: eine Vorschlags-ID, eine Plan-ID, ein Cursor. So kann ein zweiter Agent in einem anderen Client dort weitermachen, wo der erste aufgehört hat, und ein abgestürzter Agent verliert nur sein eigenes Kontextfenster.
Führt Tajo von Agenten geschriebenen Code aus?
Nicht für Kund:innen. Lesezugriffe über die Connector-Tools sind eine Anfrage pro Aufruf, ohne Sandbox. Der vorhandene Code-Runner ist ein internes Tool hinter zwei Flags, nur für Administrator:innen erreichbar, mit reinen Lese-Bindings und einem frischen Kontext pro Aufruf. Er sagt über sich selbst, dass er keine Sicherheitsgrenze ist. Es gibt keine REPL und keine persistente Session.
Wofür ist eine dauerhafte JavaScript-Engine wie dquickts gut?
Ihr README nennt die Einsatzzwecke: ein untätiges Skript in den Ruhezustand versetzen, eine lange Berechnung in Abschnitten auf Worker verteilen, per Warmstart aus einem Snapshot starten und ein Skript bei einem Host-Aufruf parken, dessen Antwort Stunden später kommt. Alles real. Ein Sync sollte trotzdem nicht davon abhängen: Ein Snapshot lädt nur in den Engine-Build, der ihn geschrieben hat, ist für Prüfende undurchsichtig und enthält alle Kundendaten, die das Skript gelesen hat.