이 페이지에서
2026년 9월 21일 UTC 00시 33분, Tobi Lütke는 이렇게 게시했습니다. “LLM을 위한 MCP 대 CLI는 잘못된 논의다. 사람들은 잘못된 계층에서 논쟁하고 있다.” 그리고 10월 23일, 그는 새로 나온 dquickts라는 저장소를 가리켰습니다. 이것은 QuickJS의 포크로, for 루프 중간에 프로그램을 멈추고, 힙 전체를 바이트로 기록한 뒤, 다른 프로세스에서 다시 실행할 수 있습니다. 그는 “훌륭한 코드 모드 환경”이라고 썼습니다.
계층에 대해서는 그가 옳습니다. 먼저 그의 말로 그에게 동의하고, 그다음 동기화에 쓸모 있는 무언가를 말할 때까지 주장을 좁혀 보겠습니다.
그가 말하는 계층
그가 펼친 그대로의 논지를 인용합니다. 비틀려는 논지를 인용할 때 공정한 방법은 이것뿐입니다.
둘 다 상태를 유지할 수 있는 REPL 같은 환경을 통해 실행되는 한 놀라울 만큼 잘 작동한다. CLI의 재미있는 점은 CLI 도구가 보통 BASH를 통해 사용되는데, BASH가 마침 영구 상태(파일 시스템)를 가진 REPL이라는 것이다. 그래서 CLI가 MCP보다 훨씬 잘 작동한다. 하지만 이것은 MCP의 본질적인 속성과는 거리가 멀다. 그저 더 나은 하네스가 필요하다는 뜻일 뿐이다.
이 말은 옳고, 많은 사람이 설명 없이 관찰해 온 현상을 설명해 줍니다. CLI를 받은 에이전트는 파일을 쓰고, 세 단계 뒤에 다시 읽고, grep하고, 파이프로 연결합니다. 맨 MCP 서버를 받은 에이전트는 호출당 결과 하나를 받고 나머지는 모두 컨텍스트 창에 담아 다녀야 합니다. CLI는 프로토콜로 이긴 것이 아닙니다. bash에 물건을 둘 자리가 딸려 있었기 때문에 이긴 것입니다.
그의 예측은 여기서 나옵니다. 누군가 SQLite처럼 임베드할 수 있는 엔진을 만들어 bash, TypeScript 또는 도구 호출을 하나의 계획으로 바꾸고, 실행 전에 검사하며, 지속 실행을 위해 설계한다는 것입니다. dquickts는 그 방향으로 가는 한 걸음입니다. 그 README는 “함수와 변수가 재시작 후에도 살아남는” REPL을 설명합니다. 지속형 호스트 호출도 설명합니다. “스크립트가 몇 시간 뒤에 응답이 오는 호스트 함수를 호출한다. 프로세스는 스크립트를 저장하고 종료한다.”
여기까지는 전적으로 동의합니다. 이제 범위를 좁히겠습니다.
어떤 상태가, 어떤 작업을 위해
“상태를 유지한다”는 말에는 질문이 숨어 있습니다. 누구의 상태를, 어떤 형태로 유지하느냐는 것입니다. 코딩 에이전트에게 상태는 저장소와 셸이고, 재시작 후에도 살아남는 REPL은 좋은 답입니다. 두 SaaS 시스템 사이의 동기화를 구축하는 에이전트에게 단계 사이에 필요한 상태는 작업 자체가 이미 정의해 두었습니다. 네 개의 레코드가 그 상태를 담고 있으며, 그중 어느 것도 스크립트의 변수가 아닙니다.
커서. 동기화는 소스를 윈도 단위로 읽으며, 마지막 윈도가 어디서 끝났는지 알아야 합니다. Tajo의 동기화 엔진에서 이것은 동기화와 범위마다 저장되는 커서로, 실패한 레코드가 없는 배치마다 기록되며 다음 실행은 그 지점에서 이어집니다. 예약 실행은 다른 무엇보다 먼저 시작 지점을 저장하므로, 첫 실행이 실패해도 다음 실행은 같은 지점에서 읽습니다. 그 바탕에 있는 원칙, 즉 테이블이 아니라 변경분만 보낸다는 원칙은 리버스 ETL 설명에 있습니다. 커서 이야기는 거기에 맡기겠습니다.
대기 중인 제안. Tajo의 커넥터 MCP 도구에서 쓰기는 두 번의 호출입니다. brevo_propose는 선언된 액션에 비추어 입력을 검증하고 제안을 기록합니다. 아무것도 전송되지 않습니다. 결과에는 제안 ID가 담기고, 사람이 결정해야 할 때는 그것을 승인하는 페이지도 담깁니다. brevo_apply는 해당 연결의 승인된 쓰기를 순서대로 실행하고, 아직 사람을 기다리는 첫 번째 쓰기에서 멈춥니다. 그 사이에 있는 레코드가 상태입니다. 이 레코드는 입력과 그 다이제스트를 담고 있어서, 승인된 쓰기를 결정과 적용 사이에 수정할 수 없습니다. 공급자의 응답 값은 담지 않습니다. 추가 전용 승인 레코드가 개인 데이터 저장소가 되어서는 안 되기 때문입니다.
리스. 동기화의 한 실행은 전체 수명 동안 하나의 배타적 리스를 보유합니다. 리스에는 소유자 토큰, 기본 15분의 시간 제한, 갱신이 있습니다. 오래된 소유자는 더 새로운 리스를 해제할 수 없습니다. 리스 저장소에 연결할 수 없으면 실행은 추측하지 않고 시작을 거부합니다. 어느 워커가 실행을 맡고 있는지는 어떤 프로세스의 메모리가 아니라 리스 안에 있는 사실입니다.
계획. syncs.dryRun은 커서에서 다음 윈도를 읽고, 레코드마다 삽입, 업데이트, 삭제, 변경 없음을 판단한 뒤 정확히 한 행을 기록합니다. 그것이 계획이며 한 시간 동안 보관됩니다. 커서를 옮기지 않고 어느 시스템에도 아무것도 쓰지 않습니다. syncs.apply는 그 계획을 한 번 기록합니다. 커서가 움직였거나, 그사이 허용 비율을 넘는 레코드가 바뀌었으면 거부합니다. 반복 호출하면 다시 쓰는 대신 첫 번째 결과를 돌려줍니다.
이것이 단계 사이의 상태입니다. ID를 가진 네 개의 레코드로, 볼 권한이 있는 어떤 에이전트든 읽을 수 있습니다.
MCP 쪽이 세션에 아무것도 보관하지 않는 이유
상태가 레코드 안에 있으면 세션이 붙들고 있을 것이 남지 않습니다. 그래서 Tajo의 MCP 엔드포인트는 세션을 유지하지 않습니다. 요청마다 새 서버와 세션 ID가 없는 전송 계층이 주어집니다. 읽기는 호출당 요청 하나이며, 하나의 답이 한 턴을 가득 채우지 않도록 결과에 상한이 있습니다. 쓰기는 제안한 다음 적용하며, 들고 다니는 것은 제안의 ID입니다.
프로토콜도 같은 방향으로 움직였습니다. MCP 사양의 2026-07-28 개정은 Streamable HTTP 전송에서 프로토콜 수준의 세션과 Mcp-Session-Id 헤더를 제거했습니다. 연속성이 필요한 서버는 이제 자체 핸들을 발급하고 그것을 일반 인수로 돌려받습니다. 제안 ID가 바로 그것입니다. MCP 서버란 무엇인가에서 그 개정과 도구 호출이 지속적인 동기화와 어떻게 다른지를 다룹니다. 이 글은 상태가 어디로 가는지만 다룹니다.
연동 작업에서 중요한 결과는 이것입니다. Claude Code의 에이전트가 쓰기를 제안하고 그 세션이 끝났다고 합시다. 한 시간 뒤 사람이 그것을 승인합니다. 다른 클라이언트의 에이전트, 혹은 다음 날의 같은 에이전트가 ID로 apply를 호출하면 쓰기는 한 번 반영됩니다. 충돌한 에이전트는 컨텍스트 창을 잃을 뿐 그 밖에는 아무것도 잃지 않습니다. 아무것도 중단되지 않았으므로 아무것도 재개할 필요가 없었습니다.
우리에게 없는 것, 그리고 그 대가
정당한 반론은 REPL 쪽에서 나오며 이렇습니다. “좋습니다, 레코드는 살아남습니다. 하지만 호출할 때마다 이전 호출이 이미 가지고 있던 것을 다시 읽습니다. 스크립트라면 연락처를 한 번 가져와 결합하고 걸러서 숫자 하나를 돌려줄 것입니다. 여러분의 에이전트는 다섯 번 호출하고 매번 비용을 치릅니다.” 이 반론은 메커니즘에 관해서는 옳으며, 우리는 그 비용을 측정하지 않았습니다. 상태를 들고 다닐 때와 그렇지 않을 때 작업당 호출 수를 세는 평가는 아직 만들어지지 않았습니다. 그것이 만들어지기 전까지, 호출 모드가 토큰을 더 쓴다는 주장은 반박되지 않은 채로 남습니다.
세 가지를 더 분명히 밝혀야 합니다. 이 글을 다 읽은 독자가 그것들이 있다고 상상하지 않도록 하기 위해서입니다.
- 현재 고객이 작성한 코드는 실행되지 않습니다. 커넥터 읽기는 호출당 요청 하나이며, 그 경로에 샌드박스는 없습니다. 우리 스택에 코드 러너가 있지만 내부 도구입니다. 프로덕션에서는 두 개의 별도 플래그가 설정되지 않으면 꺼져 있고, 관리자에게만 응답합니다. 호출마다 새 컨텍스트, 읽기만 허용하는 바인딩, 동기 코드에 대한 2초 예산이 주어집니다. 러너의 헤더 자체가 이것은 정확성의 경계이지 보안의 경계가 아니라고 대문자로 밝힙니다.
- REPL이 없습니다. 에이전트가 한 호출에서 계산한 것은 ID를 가진 레코드로 기록되지 않는 한 다음 호출에서 쓸 수 없습니다.
- 영구 에이전트 세션이 없습니다. 클라이언트는 대화를 보관합니다. Tajo는 제안, 계획, 커서, 리스를 보관합니다. 세 번째 것은 없습니다.
지속형 엔진은 무엇을 위한 것인가
이 중 어느 것도 dquickts에 반대하는 주장이 아닙니다. README는 스냅숏의 용도를 나열하며, 그 목록은 훌륭합니다. 유휴 스크립트를 최대 절전 상태로 두고 메모리를 해제한다. 긴 계산을 비어 있는 워커에서 조각으로 나눠 실행한다. 설정을 반복하는 대신 모든 인스턴스를 스냅숏에서 웜 스타트한다. 나중에 응답이 오는 호스트 호출에 스크립트를 대기시킨다. 코딩 에이전트의 작업 공간이나 재시작 후에도 살아남아야 하는 노트북에는 이것이 알맞은 도구입니다.
그래도 동기화는 이것에 의존해서는 안 되며, 그 이유 세 가지는 README가 직접 알려 줍니다. 첫째, “세션 파일은 그것을 쓴 qjs 빌드에만 로드된다.” 한 엔진 버전이 쓴 힙은 업그레이드 후에는 죽은 파일이 되지만, 커서는 문자열입니다. 둘째, “스냅숏은 바이트코드처럼 신뢰된 입력이다”, 그리고 “악의적인 스냅숏은 탐지되지 않는다.” 저장된 힙은 보호해야 하는 대상이지 검토할 수 있는 대상이 아닙니다. 제안은 사람이 승인하기 전에 읽는 행입니다. 셋째, 스냅숏은 고객 레코드를 포함해 스크립트가 읽은 모든 것을 담고 있습니다. 위의 레코드는 그 반대로 만들어졌습니다. 계획은 값이 아니라 다이제스트를 저장하고, 제안 레코드는 공급자의 응답을 절대 저장하지 않습니다.
우리가 따르는 원칙 중 하나는 결정적인 것이 영리한 것보다 낫다는 것입니다. 루프 중간에서 재개되는 힙은 영리합니다. 커서, 제안, 리스, 계획은 따분하고, 검사할 수 있으며, 어떤 엔진에서든, 어떤 클라이언트에서든, 어떤 충돌 후에든 똑같습니다.
Lütke는 올바른 질문을 던졌습니다. 상태는 어디에 있는가? 동기화에서 그 답은 결코 프로세스 안이 될 수 없었습니다. 답은 언제나 레코드 안이어야 했습니다. 쓰기를 승인하는 사람이 그것을 읽을 수 있어야 하는 곳이 바로 거기이기 때문입니다.
자주 묻는 질문
AI 에이전트에는 MCP와 CLI 중 무엇이 더 낫습니까?
그 자체로는 어느 쪽도 아닙니다. 2026년 9월 21일 Tobi Lütke의 지적은 유효합니다. 단계 사이에 상태를 유지하는 무언가를 통해 호출이 실행되면 둘 다 잘 작동합니다. 오늘날 CLI가 더 낫게 느껴지는 것은 bash가 파일 시스템에 상태를 보관하기 때문입니다. 차이는 프로토콜이 아니라 하네스에 있습니다.
에이전트는 동기화의 단계 사이에 어떤 상태가 필요합니까?
연동 자체의 레코드입니다. 마지막 실행이 어디서 멈췄는지 알려 주는 커서, 사람의 승인을 기다리는 제안, 어느 워커가 실행을 맡고 있는지 알려 주는 리스, 그리고 apply가 일치해야 하는 드라이 런 계획입니다. 그중 어느 것도 스크립트의 변수가 아닙니다. ID를 가진 행입니다.
Tajo의 MCP 서버는 왜 세션에 아무것도 보관하지 않습니까?
요청마다 새 서버와 세션 ID가 없는 전송 계층이 주어집니다. 다음 호출에 필요한 것은 모두 레코드입니다. 제안 ID, 계획 ID, 커서입니다. 그래서 다른 클라이언트에 있는 두 번째 에이전트가 첫 번째 에이전트가 멈춘 곳에서 이어서 작업할 수 있고, 충돌한 에이전트는 자신의 컨텍스트 창 외에는 아무것도 잃지 않습니다.
Tajo는 에이전트가 작성한 코드를 실행합니까?
고객을 위해서는 실행하지 않습니다. 커넥터 도구의 읽기는 호출당 요청 하나이며 샌드박스가 없습니다. 존재하는 코드 러너는 두 개의 플래그 뒤에 있는 내부 도구로, 관리자만 접근할 수 있고, 읽기 전용 바인딩을 가지며, 호출마다 새 컨텍스트를 사용합니다. 이 러너는 스스로 보안 경계가 아니라고 밝힙니다. REPL도, 영구 세션도 없습니다.
dquickts 같은 지속형 JavaScript 엔진은 어디에 유용합니까?
README가 용도를 밝힙니다. 유휴 스크립트를 최대 절전 상태로 두기, 긴 계산을 여러 워커에 나눠 실행하기, 스냅숏에서 웜 스타트하기, 몇 시간 뒤에 응답이 오는 호스트 호출에 스크립트를 대기시키기입니다. 모두 실제 용도입니다. 그래도 동기화는 이것에 의존해서는 안 됩니다. 스냅숏은 그것을 쓴 엔진 빌드에만 로드되고, 검토자에게는 불투명하며, 스크립트가 읽은 고객 데이터를 그대로 담고 있기 때문입니다.