Na tej stronie
21 września 2026 roku, o 00:33 UTC, Tobi Lütke napisał: „MCP kontra CLI dla LLM to zła dyskusja. Ludzie spierają się na złej warstwie”. Potem, 23 października, wskazał nowe repozytorium o nazwie dquickts. To fork QuickJS, który potrafi wstrzymać program w środku pętli for, zapisać całą jego stertę do bajtów i wznowić go w innym procesie. „Świetne środowisko do code mode”, napisał.
Ma rację co do warstwy. Chcę się z nim najpierw zgodzić, jego własnymi słowami, a potem zawęzić tezę, aż powie coś, z czego synchronizacja może skorzystać.
Warstwa, o którą mu chodzi
Oto argument w takiej formie, w jakiej go przedstawił, bo to jedyny uczciwy sposób cytowania argumentu, który zamierza się nagiąć:
Oba działają niesamowicie dobrze, o ile uruchamia się je w środowisku podobnym do REPL, które potrafi przechowywać stan. Zabawne w CLI jest to, że narzędzia CLI są zwykle używane przez BASH, który akurat jest REPL z trwałym stanem (systemem plików), więc CLI działa dużo lepiej niż MCP. Ale to nawet w przybliżeniu nie jest wewnętrzna cecha MCP. Oznacza to tylko, że potrzebujemy lepszych środowisk uruchomieniowych.
To prawda i wyjaśnia obserwację, którą wiele osób poczyniło, ale nie wyjaśniło. Agent, który dostaje CLI, zapisuje plik, odczytuje go trzy kroki później, przeszukuje go grepem, przekazuje dalej potokiem. Agent, który dostaje gołe MCP, otrzymuje jeden wynik na wywołanie i całą resztę musi nosić w swoim oknie kontekstu. CLI nie wygrało protokołem. Wygrało, bo bash przyszedł z miejscem, w którym można coś odłożyć.
Jego zakład wynika z tego wprost. Ktoś zbuduje osadzalny silnik w stylu SQLite, który zamieni bash, TypeScript lub wywołania narzędzi w jeden plan, sprawdzony przed uruchomieniem i zbudowany do trwałego wykonywania. dquickts to krok w tę stronę. Jego README opisuje REPL, „którego funkcje i zmienne przetrwają restarty”. Opisuje też trwałe wywołanie hosta: „skrypt wywołuje funkcję hosta, której odpowiedź przychodzi kilka godzin później. Proces zapisuje skrypt i kończy działanie”.
Do tego miejsca pełna zgoda. Teraz zawężenie.
Jaki stan, do jakiego zadania
„Przechowywać stan” kryje w sobie pytanie: czyj stan i w jakiej formie? Dla agenta programistycznego stanem jest repozytorium i powłoka, a REPL, który przetrwa restarty, to dobra odpowiedź. Dla agenta, który buduje synchronizację między dwoma systemami SaaS, stan potrzebny mu między krokami jest już określony przez samo zadanie. Niosą go cztery rekordy i żaden z nich nie jest zmienną w skrypcie.
Kursor. Synchronizacja czyta źródło w oknach i musi wiedzieć, gdzie skończyło się ostatnie okno. W silniku synchronizacji Tajo to zapisany kursor dla każdej synchronizacji i zakresu, zapisywany po każdej partii bez nieudanych rekordów, a następne uruchomienie zaczyna od niego. Zaplanowane uruchomienie zapisuje swój punkt startowy, zanim zrobi cokolwiek innego, więc pierwsze uruchomienie, które się nie powiedzie, pozwala następnemu czytać od tego samego punktu. Zasada, która za tym stoi, czyli wysyłaj deltę, nigdy tabelę, jest opisana w objaśnieniu reverse ETL. Tam zostawiam kursor.
Oczekująca propozycja. W narzędziach MCP konektorów Tajo zapis to dwa wywołania. brevo_propose sprawdza dane wejściowe względem zadeklarowanej akcji i zapisuje propozycję. Nic nie zostaje wysłane. Wynik zawiera identyfikator propozycji oraz, gdy decyzję musi podjąć człowiek, stronę, na której ją zatwierdza. brevo_apply wykonuje po kolei zatwierdzone zapisy danego połączenia i zatrzymuje się na pierwszym, który wciąż czeka na człowieka. Rekord pomiędzy nimi jest stanem. Przechowuje dane wejściowe i ich skrót (digest), więc zatwierdzonego zapisu nie da się zmienić między decyzją a apply. Nie przechowuje wartości z odpowiedzi dostawcy, bo rekord zatwierdzeń typu append-only nie może stać się magazynem danych osobowych.
Dzierżawa. Uruchomienie synchronizacji trzyma jedną wyłączną dzierżawę (lease) przez cały swój czas życia, z tokenem właściciela, limitem czasu wynoszącym domyślnie piętnaście minut i odnawianiem. Nieaktualny właściciel nie może zwolnić nowszej dzierżawy. Jeśli magazyn dzierżaw jest nieosiągalny, uruchomienie odmawia startu, zamiast zgadywać. To, który worker trzyma uruchomienie, jest faktem zapisanym w dzierżawie, a nie w pamięci jakiegokolwiek procesu.
Plan. syncs.dryRun czyta następne okno od kursora, ustala dla każdego rekordu wstawienie, aktualizację, usunięcie lub brak zmian i zapisuje dokładnie jeden wiersz: plan, przechowywany przez godzinę. Nie przesuwa kursora i nie zapisuje niczego do żadnego z systemów. syncs.apply zapisuje ten plan jeden raz. Odmawia, jeśli kursor się przesunął albo jeśli w międzyczasie zmieniło się więcej niż tolerowana część rekordów. Powtórzenie zwraca pierwszy wynik, zamiast zapisywać ponownie.
To jest stan między krokami: cztery rekordy z identyfikatorami, czytelne dla każdego agenta, który ma prawo je widzieć.
Dlaczego strona MCP nie przechowuje niczego w sesji
Gdy stan jest już w rekordach, sesja nie ma czego trzymać. Dlatego endpointy MCP w Tajo żadnej nie trzymają. Każde żądanie dostaje nowy serwer i transport bez identyfikatora sesji. Odczyty to jedno żądanie na wywołanie, z ograniczonymi wynikami, aby jedna odpowiedź nie zalała tury. Zapisy to najpierw propozycja, potem apply, a tym, co przenosisz dalej, jest identyfikator propozycji.
Protokół poszedł w tę samą stronę. Rewizja 2026-07-28 specyfikacji MCP usunęła sesje na poziomie protokołu oraz nagłówek Mcp-Session-Id z transportu Streamable HTTP. Serwer, który potrzebuje ciągłości, tworzy teraz własny uchwyt i otrzymuje go z powrotem jako zwykły argument. Tym właśnie jest identyfikator propozycji. Czym jest serwer MCP omawia tę rewizję i to, czym wywołanie narzędzia różni się od trwałej synchronizacji; ten tekst dotyczy tylko tego, gdzie trafia stan.
Konsekwencja jest tą, która liczy się w pracy integracyjnej. Załóżmy, że agent w Claude Code proponuje zapis, a jego sesja się kończy. Człowiek zatwierdza go godzinę później. Agent w innym kliencie, albo ten sam jutro, wywołuje apply z identyfikatorem i zapis trafia na miejsce, jeden raz. Agent, który uległ awarii, traci swoje okno kontekstu i nic więcej. Nic nie zostało wstrzymane, więc nic nie trzeba było wznawiać.
Czego nie mamy i ile to kosztuje
Uczciwy zarzut pada ze strony REPL i brzmi tak: „Dobrze, Wasze rekordy przetrwają. Ale każde wywołanie ponownie czyta to, co poprzednie wywołanie już miało. Skrypt pobrałby kontakty raz, złączyłby je, przefiltrował i zwrócił liczbę. Wasz agent wykonuje pięć wywołań i płaci za każde”. Ten zarzut ma rację co do mechanizmu, a my nie zmierzyliśmy ceny. Ewaluacja, która liczyłaby wywołania na zadanie z przenoszonym stanem i bez niego, nie została zbudowana. Dopóki nie powstanie, twierdzenie, że call mode kosztuje więcej tokenów, pozostaje bez odpowiedzi.
Trzy kolejne rzeczy trzeba powiedzieć otwarcie, bo czytelnik, który dotrze do końca tego artykułu, nie powinien ich sobie wyobrażać:
- Dziś nie działa żaden kod napisany przez klientów. Odczyty konektorów to jedno żądanie na wywołanie, bez sandboksa na ścieżce. W naszym stosie istnieje code runner i jest to narzędzie wewnętrzne. Na produkcji jest wyłączony, chyba że ustawiono dwie osobne flagi, i odpowiada tylko administratorom. Każde wywołanie dostaje nowy kontekst, powiązania pozwalające wyłącznie na odczyt i dwusekundowy budżet na kod synchroniczny. Jego własny nagłówek mówi wielkimi literami, że jest granicą poprawności, a nie bezpieczeństwa.
- Brak REPL. Nic, co agent obliczy w jednym wywołaniu, nie jest dostępne w następnym, chyba że zostało zapisane jako rekord z identyfikatorem.
- Brak trwałej sesji agenta. Klient przechowuje swoją rozmowę. Tajo przechowuje propozycje, plany, kursory i dzierżawy. Trzeciej rzeczy nie ma.
Do czego służy trwały silnik
Nic z tego nie jest argumentem przeciwko dquickts. Jego README wymienia, do czego służą snapshoty, i ta lista jest dobra. Uśpij bezczynny skrypt i zwolnij jego pamięć. Uruchamiaj długie obliczenia w kawałkach na tym workerze, który akurat jest wolny. Startuj każdą instancję na ciepło ze snapshotu, zamiast powtarzać konfigurację. Zaparkuj skrypt na wywołaniu hosta, którego odpowiedź przyjdzie później. Dla przestrzeni roboczej agenta programistycznego albo notatnika, który ma przetrwać restart, to właściwe narzędzie.
Synchronizacja i tak nie powinna od niego zależeć, z trzech powodów, które podaje mi samo README. Po pierwsze: „Plik sesji ładuje się tylko do tej kompilacji qjs, która go zapisała”. Sterta zapisana przez jedną wersję silnika jest po aktualizacji martwym plikiem; kursor jest ciągiem znaków. Po drugie: „Snapshoty są zaufanymi danymi wejściowymi, tak jak kod bajtowy”, a „złośliwe nie są wykrywane”. Zapisana sterta to coś, co trzeba chronić, a nie coś, co można przejrzeć; propozycja to wiersz, który człowiek czyta przed zatwierdzeniem. Po trzecie, snapshot zawiera wszystko, co skrypt przeczytał, w tym rekordy klientów. Rekordy opisane wyżej są zbudowane odwrotnie: plan przechowuje skróty, nie wartości, a rekord propozycji nigdy nie przechowuje odpowiedzi dostawcy.
Jedna z zasad, według których budujemy, mówi, że deterministyczne wygrywa ze sprytnym. Sterta, która wznawia działanie w środku pętli, jest sprytna. Kursor, propozycja, dzierżawa i plan są nudne, dają się sprawdzić i są takie same na każdym silniku, w każdym kliencie, po każdej awarii.
Lütke zadał właściwe pytanie. Gdzie mieszka stan? W przypadku synchronizacji odpowiedź nigdy nie miała leżeć w procesie. Zawsze miała leżeć w rekordach, bo właśnie tam człowiek, który zatwierdza zapis, musi móc go przeczytać.
Najczęściej Zadawane Pytania
Co jest lepsze dla agentów AI: MCP czy CLI?
Samo w sobie ani jedno, ani drugie. Teza Tobiego Lütkego z 21 września 2026 roku się broni: oba działają, gdy tylko wywołania przechodzą przez coś, co przechowuje stan między krokami. CLI wydaje się dziś lepsze, bo bash przechowuje stan w systemie plików. Różnica nie leży w protokole, tylko w środowisku uruchomieniowym (harness).
Jakiego stanu potrzebuje agent między krokami synchronizacji?
Własnych rekordów integracji: kursora, który mówi, gdzie zatrzymało się ostatnie uruchomienie, propozycji czekającej na zatwierdzenie przez człowieka, dzierżawy (lease), która mówi, który worker trzyma uruchomienie, oraz planu z dry runu, z którym musi się zgadzać apply. Nic z tego nie jest zmienną w skrypcie. To wiersze z identyfikatorami.
Dlaczego serwer MCP w Tajo nie przechowuje niczego w sesji?
Każde żądanie dostaje nowy serwer i transport bez identyfikatora sesji. Wszystko, czego potrzebuje następne wywołanie, jest rekordem: identyfikatorem propozycji, identyfikatorem planu, kursorem. Dzięki temu drugi agent, w innym kliencie, może kontynuować tam, gdzie zatrzymał się pierwszy, a agent, który uległ awarii, traci tylko własne okno kontekstu.
Czy Tajo uruchamia kod napisany przez agentów?
Nie dla klientów. Odczyty przez narzędzia konektorów to jedno żądanie na wywołanie, bez sandboksa. Istniejący code runner jest narzędziem wewnętrznym ukrytym za dwiema flagami, dostępnym tylko dla administratorów, z powiązaniami tylko do odczytu i nowym kontekstem przy każdym wywołaniu. Sam o sobie mówi, że nie jest granicą bezpieczeństwa. Nie ma REPL ani trwałej sesji.
Do czego przydaje się trwały silnik JavaScript, taki jak dquickts?
Jego README wymienia zastosowania: uśpienie bezczynnego skryptu, podzielenie długich obliczeń między workery, ciepły start ze snapshotu i zaparkowanie skryptu na wywołaniu hosta, którego odpowiedź przychodzi kilka godzin później. Wszystko to prawdziwe. Synchronizacja i tak nie powinna od niego zależeć: snapshot ładuje się tylko do tej kompilacji silnika, która go zapisała, jest nieprzejrzysty dla recenzenta i zawiera wszystkie dane klientów, które skrypt odczytał.