---
title: "MCP hay CLI là câu hỏi sai. Hãy hỏi trạng thái nằm ở đâu"
description: "Tobi Lütke cho rằng cuộc tranh luận MCP hay CLI đang diễn ra sai tầng: cả hai đều hoạt động khi trạng thái được giữ lại giữa các bước. Chúng tôi đồng ý. Với một luồng đồng bộ, trạng thái đó là các bản ghi của chính tích hợp, không phải heap của một REPL, và đó là lý do các công cụ MCP của Tajo không giữ gì trong phiên."
url: https://tajo.io/vi/blog/mcp-or-cli-is-the-wrong-question/
markdown: https://tajo.io/vi/blog/mcp-or-cli-is-the-wrong-question.md
lang: vi-VN
author: "Set Noa"
author_url: https://tajo.io/vi/set-noa/
published: 2026-11-04
updated: 2026-11-05
alternates: {en-US: /blog/mcp-or-cli-is-the-wrong-question.md, bg-BG: /bg/blog/mcp-or-cli-is-the-wrong-question.md, cs-CZ: /cz/blog/mcp-or-cli-is-the-wrong-question.md, de-DE: /de/blog/mcp-or-cli-is-the-wrong-question.md, da-DK: /dk/blog/mcp-or-cli-is-the-wrong-question.md, es-ES: /es/blog/mcp-or-cli-is-the-wrong-question.md, fr-FR: /fr/blog/mcp-or-cli-is-the-wrong-question.md, el-GR: /gr/blog/mcp-or-cli-is-the-wrong-question.md, hi-IN: /hi/blog/mcp-or-cli-is-the-wrong-question.md, id-ID: /in/blog/mcp-or-cli-is-the-wrong-question.md, hr-HR: /hr/blog/mcp-or-cli-is-the-wrong-question.md, hu-HU: /hu/blog/mcp-or-cli-is-the-wrong-question.md, it-IT: /it/blog/mcp-or-cli-is-the-wrong-question.md, ja-JP: /jp/blog/mcp-or-cli-is-the-wrong-question.md, ko-KR: /kr/blog/mcp-or-cli-is-the-wrong-question.md, ar-SA: /ar/blog/mcp-or-cli-is-the-wrong-question.md, nl-NL: /nl/blog/mcp-or-cli-is-the-wrong-question.md, no-NO: /no/blog/mcp-or-cli-is-the-wrong-question.md, pl-PL: /pl/blog/mcp-or-cli-is-the-wrong-question.md, pt-BR: /pt/blog/mcp-or-cli-is-the-wrong-question.md, ro-RO: /ro/blog/mcp-or-cli-is-the-wrong-question.md, ru-RU: /ru/blog/mcp-or-cli-is-the-wrong-question.md, sv-SE: /se/blog/mcp-or-cli-is-the-wrong-question.md, zh-CN: /zh-cn/blog/mcp-or-cli-is-the-wrong-question.md, sl-SI: /sl/blog/mcp-or-cli-is-the-wrong-question.md, sr-Latn-RS: /sr/blog/mcp-or-cli-is-the-wrong-question.md, th-TH: /th/blog/mcp-or-cli-is-the-wrong-question.md, tr-TR: /tr/blog/mcp-or-cli-is-the-wrong-question.md, ur-PK: /ur/blog/mcp-or-cli-is-the-wrong-question.md, sk-SK: /sk/blog/mcp-or-cli-is-the-wrong-question.md}
---

[Tác nhân AI](https://tajo.io/vi/blog/?category=ai-agents)

# MCP hay CLI là câu hỏi sai. Hãy hỏi trạng thái nằm ở đâu

Tobi Lütke cho rằng cuộc tranh luận MCP hay CLI đang diễn ra sai tầng: cả hai đều hoạt động khi trạng thái được giữ lại giữa các bước. Chúng tôi đồng ý. Với một luồng đồng bộ, trạng thái đó là các bản ghi của chính tích hợp, không phải heap của một REPL, và đó là lý do các công cụ MCP của Tajo không giữ gì trong phiên.

so sánh MCP và CLI

Tác nhân AI nên dùng MCP hay CLI?

Lütke đúng khi nói MCP hay CLI là tranh luận sai tầng: cả hai đều hoạt động khi trạng thái được giữ lại giữa các bước, và bash thắng chỉ vì hệ thống tệp chính là trạng thái đó. Nhưng với một luồng đồng bộ, trạng thái không phải là heap của một REPL. Đó là các bản ghi của chính tích hợp: một con trỏ, một đề xuất đang chờ, một hợp đồng thuê, một kế hoạch. Đó là lý do các công cụ MCP của Tajo không giữ gì trong phiên: bất kỳ tác nhân nào, từ bất kỳ client nào, đều tiếp tục từ các bản ghi. Hiện Tajo không chạy code của khách hàng và không có REPL, và chúng tôi nói rõ điều đó bên dưới.

[Tìm hiểu thêm](https://tajo.io/vi/blog/mcp-or-cli-is-the-wrong-question/#t%E1%BA%A7ng-m%C3%A0-%C3%B4ng-n%C3%B3i-t%E1%BB%9Bi)

Ngày 21/09/2026, lúc 00:33 UTC, Tobi Lütke đăng dòng này: “MCP hay CLI cho LLM là một cuộc thảo luận sai. Mọi người đang tranh luận ở sai tầng.” Rồi [ngày 23/10](https://x.com/tobi/status/2113596638899974567), ông chỉ tới một kho mã mới tên là [dquickts](https://github.com/stencil-hq/dquickts). Đó là một bản fork của QuickJS có thể tạm dừng một chương trình giữa vòng lặp `for`, ghi toàn bộ heap của nó thành byte, rồi chạy tiếp trong một tiến trình khác. “Môi trường code mode xuất sắc,” ông viết.

Ông đúng về tầng. Tôi muốn đồng ý với ông trước, bằng chính lời của ông, rồi thu hẹp nhận định cho đến khi nó nói được điều gì đó mà một luồng đồng bộ dùng được.

## Tầng mà ông nói tới

Đây là lập luận đúng như ông đưa ra, cách công bằng duy nhất để trích dẫn một lập luận mà bạn định bẻ lại:

> Cả hai đều hoạt động cực kỳ tốt, miễn là chúng được chạy qua một môi trường kiểu REPL có thể giữ trạng thái. Điều buồn cười về CLI là các công cụ CLI thường được truy cập qua BASH, mà BASH tình cờ lại là một REPL có trạng thái lâu dài (hệ thống tệp), vì thế CLI hoạt động tốt hơn MCP nhiều. Nhưng đây hoàn toàn không phải là một thuộc tính nội tại của MCP. Nó chỉ có nghĩa là chúng ta cần những bộ khung chạy tốt hơn.

Điều đó đúng, và nó giải thích một quan sát mà nhiều người đã thấy nhưng chưa giải thích được. Một tác nhân được trao CLI sẽ ghi một tệp, đọc lại nó ba bước sau, grep nó, chuyển nó qua pipe. Một tác nhân được trao một máy chủ MCP trần chỉ nhận một kết quả cho mỗi lần gọi và phải mang mọi thứ khác trong cửa sổ ngữ cảnh của mình. CLI không thắng nhờ giao thức. Nó thắng vì bash đi kèm một chỗ để đặt đồ.

Canh bạc của ông xuất phát từ đó. Ai đó sẽ xây một engine nhúng được, theo kiểu SQLite, biến bash, TypeScript hoặc các lần gọi công cụ thành một kế hoạch duy nhất, được kiểm tra trước khi chạy và được thiết kế cho thực thi bền vững. dquickts là một bước tiến về phía đó. [README của nó](https://github.com/stencil-hq/dquickts) mô tả một REPL “có các hàm và biến tồn tại qua các lần khởi động lại”. Nó cũng mô tả một lần gọi host bền vững: “một script gọi một hàm host mà câu trả lời đến sau nhiều giờ. Tiến trình lưu script lại rồi thoát.”

Đến đây, tôi hoàn toàn đồng ý. Giờ là phần thu hẹp.

## Trạng thái nào, cho công việc nào

“Giữ trạng thái” che giấu một câu hỏi: trạng thái của ai, và ở dạng nào? Với một tác nhân lập trình, trạng thái là kho mã và shell, và một REPL tồn tại qua các lần khởi động lại là một câu trả lời tốt. Với một tác nhân đang xây dựng luồng đồng bộ giữa hai hệ thống SaaS, trạng thái nó cần giữa các bước đã được chính công việc định nghĩa sẵn. Bốn bản ghi mang trạng thái đó, và không bản ghi nào là biến trong một đoạn script.

**Con trỏ.** Một luồng đồng bộ đọc nguồn theo từng cửa sổ và phải biết cửa sổ trước kết thúc ở đâu. Trong engine đồng bộ của Tajo, đó là một [con trỏ được lưu](https://tajo.io/vi/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#cursor) cho mỗi luồng đồng bộ và mỗi phạm vi, được ghi sau mỗi lô không có bản ghi nào thất bại, và lần chạy kế tiếp tiếp tục từ đó. Một lần chạy theo lịch sẽ lưu điểm bắt đầu của nó trước khi làm bất cứ việc gì khác, nên nếu lần chạy đầu tiên thất bại thì lần chạy sau vẫn đọc từ cùng một điểm. Nguyên tắc đằng sau điều này, gửi phần thay đổi và không bao giờ gửi cả bảng, có trong [bài giải thích về reverse ETL](https://tajo.io/vi/blog/what-is-reverse-etl/). Tôi xin để chuyện con trỏ ở đó.

**Đề xuất đang chờ.** Trên các công cụ MCP connector của Tajo, một thao tác ghi gồm hai lần gọi. `brevo_propose` kiểm tra đầu vào theo hành động đã khai báo và ghi lại một đề xuất. Không có gì được gửi đi. Kết quả chứa một ID đề xuất và, khi cần một người quyết định, trang để họ phê duyệt nó. `brevo_apply` chạy các thao tác ghi đã được phê duyệt của kết nối đó theo thứ tự và dừng ở thao tác đầu tiên vẫn đang chờ một người. Bản ghi nằm giữa hai bước đó chính là trạng thái. Nó giữ đầu vào và một bản tóm lược (digest) của đầu vào, nên một thao tác ghi đã được phê duyệt không thể bị sửa giữa lúc quyết định và lúc apply. Nó không giữ các giá trị phản hồi của nhà cung cấp, vì một bản ghi phê duyệt chỉ cho phép thêm vào không được biến thành kho lưu dữ liệu cá nhân.

**Hợp đồng thuê (lease).** Một lần chạy đồng bộ giữ một [hợp đồng thuê độc quyền](https://tajo.io/vi/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#lease) trong suốt vòng đời của nó, với một token chủ sở hữu, giới hạn thời gian mặc định là mười lăm phút, và cơ chế gia hạn. Một chủ sở hữu cũ không thể giải phóng một hợp đồng thuê mới hơn. Nếu không truy cập được kho hợp đồng thuê, lần chạy sẽ từ chối khởi động thay vì đoán. Worker nào đang giữ lần chạy là một sự thật nằm trong hợp đồng thuê, không nằm trong bộ nhớ của bất kỳ tiến trình nào.

**Kế hoạch.** `syncs.dryRun` đọc cửa sổ tiếp theo từ con trỏ, xác định cho từng bản ghi là thêm, cập nhật, xóa hay không đổi, rồi ghi đúng một hàng: kế hoạch, được giữ trong một giờ. Nó không di chuyển con trỏ và không ghi gì vào hệ thống nào. `syncs.apply` ghi kế hoạch đó một lần. Nó từ chối nếu con trỏ đã di chuyển, hoặc nếu số bản ghi thay đổi trong khoảng thời gian đó vượt quá tỷ lệ cho phép. Gọi lại sẽ trả về kết quả đầu tiên thay vì ghi thêm lần nữa.

Đó là trạng thái giữa các bước: bốn bản ghi có ID, đọc được bởi bất kỳ tác nhân nào được phép xem chúng.

## Vì sao phía MCP không giữ gì trong phiên

Khi trạng thái đã nằm trong các bản ghi, phiên không còn gì để giữ. Vì vậy các endpoint MCP của Tajo không giữ phiên. Mỗi yêu cầu nhận một máy chủ mới và một lớp truyền tải không có ID phiên. Thao tác đọc là một yêu cầu cho mỗi lần gọi, với kết quả bị giới hạn để một câu trả lời không thể làm ngập một lượt. Thao tác ghi là đề xuất rồi apply, và thứ bạn mang theo là ID của đề xuất.

Giao thức cũng đi theo cùng hướng. [Bản sửa đổi 2026-07-28 của đặc tả MCP](https://modelcontextprotocol.io/specification/2026-07-28/changelog) đã loại bỏ phiên ở cấp giao thức và header `Mcp-Session-Id` khỏi lớp truyền tải Streamable HTTP. Một máy chủ cần tính liên tục giờ tự tạo handle của riêng mình và nhận lại nó như một đối số thông thường. Một ID đề xuất chính là như vậy. Bài [Máy chủ MCP là gì](https://tajo.io/vi/blog/what-is-an-mcp-server/) trình bày bản sửa đổi đó và sự khác nhau giữa một lần gọi công cụ với một luồng đồng bộ bền vững; bài này chỉ nói về việc trạng thái được đặt ở đâu.

Hệ quả quan trọng đối với công việc tích hợp là thế này. Giả sử một tác nhân trong Claude Code đề xuất một thao tác ghi và phiên của nó kết thúc. Một giờ sau, một người phê duyệt nó. Một tác nhân trong một client khác, hoặc chính tác nhân đó vào ngày hôm sau, gọi apply với ID đó và thao tác ghi được thực hiện, một lần. Một tác nhân bị sập mất cửa sổ ngữ cảnh của nó và không mất gì khác. Không có gì bị tạm dừng, nên không có gì phải tiếp tục lại.

## Những gì chúng tôi không có, và cái giá của nó

Phản bác thẳng thắn đến từ phía REPL và đại ý thế này: “Được thôi, các bản ghi của bạn tồn tại. Nhưng mỗi lần gọi lại đọc lại thứ mà lần gọi trước đã có. Một đoạn script sẽ lấy danh bạ một lần, nối chúng, lọc chúng và trả về một con số. Tác nhân của bạn gọi năm lần và trả giá cho mỗi lần.” Phản bác đó đúng về cơ chế, và chúng tôi chưa đo cái giá đó. Bài đánh giá đếm số lần gọi cho mỗi tác vụ khi có và không có trạng thái mang theo vẫn chưa được xây dựng. Cho đến khi có nó, nhận định rằng chế độ gọi tốn nhiều token hơn vẫn chưa được trả lời.

Còn ba điều nữa phải được nói rõ, vì người đọc xong bài này không nên tưởng tượng rằng chúng tồn tại:

- **Hiện không có code nào do khách hàng viết được chạy.** Các thao tác đọc qua connector là một yêu cầu cho mỗi lần gọi, không có sandbox trên đường đi. Trong hệ thống của chúng tôi có một trình chạy code, và đó là một công cụ nội bộ. Trên môi trường production, nó bị tắt trừ khi hai cờ riêng biệt được bật, và nó chỉ phản hồi quản trị viên. Mỗi lần gọi nhận một ngữ cảnh mới, các liên kết chỉ cho phép đọc, và ngân sách hai giây cho code đồng bộ. Chính phần header của nó ghi bằng chữ in hoa rằng nó là ranh giới về tính đúng đắn, không phải ranh giới bảo mật.
- **Không có REPL.** Bất cứ thứ gì một tác nhân tính toán trong một lần gọi sẽ không có sẵn cho lần gọi tiếp theo, trừ khi nó được ghi thành một bản ghi có ID.
- **Không có phiên tác nhân lâu dài.** Client giữ cuộc hội thoại của nó. Tajo giữ các đề xuất, kế hoạch, con trỏ và hợp đồng thuê. Không có thứ thứ ba.

## Một engine bền vững dùng để làm gì

Không điều nào ở trên là lập luận chống lại dquickts. README của nó liệt kê snapshot dùng để làm gì, và danh sách đó rất tốt. Cho một script đang rảnh ngủ đông và giải phóng bộ nhớ. Chạy một phép tính dài thành từng lát trên bất kỳ worker nào đang rảnh. Khởi động nóng mọi instance từ một snapshot thay vì lặp lại bước thiết lập. Để một script chờ ở một lần gọi host mà câu trả lời đến sau. Với không gian làm việc của một tác nhân lập trình, hoặc một notebook cần tồn tại qua lần khởi động lại, đó là công cụ phù hợp.

Dù vậy, một luồng đồng bộ vẫn không nên phụ thuộc vào nó, vì ba lý do mà chính README cho tôi biết. Thứ nhất: “Một tệp phiên chỉ nạp được vào bản build qjs đã ghi ra nó.” Một heap được ghi bởi một phiên bản engine sẽ là tệp chết sau khi nâng cấp; một con trỏ chỉ là một chuỗi. Thứ hai: “Snapshot là đầu vào được tin cậy, giống như bytecode,” và “snapshot độc hại sẽ không bị phát hiện.” Một heap được lưu là thứ bạn phải bảo vệ, không phải thứ bạn có thể duyệt; một đề xuất là một hàng mà một người đọc trước khi phê duyệt. Thứ ba, một snapshot chứa mọi thứ script đã đọc, kể cả các bản ghi khách hàng. Các bản ghi ở trên được xây theo hướng ngược lại: kế hoạch lưu bản tóm lược chứ không lưu giá trị, và bản ghi đề xuất không bao giờ lưu phản hồi của nhà cung cấp.

Một trong những quy tắc chúng tôi dùng khi xây dựng là tính tất định thắng sự khéo léo. Một heap tiếp tục chạy giữa vòng lặp là khéo léo. Một con trỏ, một đề xuất, một hợp đồng thuê và một kế hoạch thì nhàm chán, kiểm tra được, và giống hệt nhau trên mọi engine, trong mọi client, sau mọi lần sập.

Lütke đã đặt đúng câu hỏi. Trạng thái nằm ở đâu? Với một luồng đồng bộ, câu trả lời chưa bao giờ có thể là trong tiến trình. Nó luôn phải nằm trong các bản ghi, vì đó là nơi người phê duyệt thao tác ghi cần đọc được nó.

## Câu hỏi Thường gặp

**MCP hay CLI tốt hơn cho tác nhân AI?**

Tự thân thì không cái nào cả. Nhận định của Tobi Lütke ngày 21/09/2026 vẫn đúng: cả hai đều hoạt động khi các lần gọi chạy qua một thứ giữ được trạng thái giữa các bước. CLI hiện có cảm giác tốt hơn vì bash giữ trạng thái trong hệ thống tệp. Khác biệt không nằm ở giao thức mà ở bộ khung chạy (harness).

**Tác nhân cần trạng thái gì giữa các bước của một luồng đồng bộ?**

Các bản ghi của chính tích hợp: một con trỏ cho biết lần chạy trước dừng ở đâu, một đề xuất đang chờ một người phê duyệt, một hợp đồng thuê (lease) cho biết worker nào đang giữ lần chạy, và một kế hoạch chạy thử mà bước apply phải khớp. Không cái nào là biến trong một đoạn script. Chúng là các hàng có ID.

**Vì sao máy chủ MCP của Tajo không giữ gì trong phiên?**

Mỗi yêu cầu nhận một máy chủ mới và một lớp truyền tải không có ID phiên. Mọi thứ lần gọi tiếp theo cần đều là bản ghi: ID đề xuất, ID kế hoạch, con trỏ. Vì vậy một tác nhân thứ hai, ở một client khác, có thể tiếp tục từ chỗ tác nhân thứ nhất dừng lại, và một tác nhân bị sập không mất gì ngoài cửa sổ ngữ cảnh của chính nó.

**Tajo có chạy code do tác nhân viết không?**

Không chạy cho khách hàng. Các thao tác đọc trên công cụ connector là một yêu cầu cho mỗi lần gọi, không có sandbox. Trình chạy code hiện có là một công cụ nội bộ nằm sau hai cờ, chỉ quản trị viên truy cập được, với các liên kết chỉ đọc và một ngữ cảnh mới cho mỗi lần gọi. Chính nó tự ghi rằng nó không phải là ranh giới bảo mật. Không có REPL và không có phiên lâu dài.

**Một engine JavaScript bền vững như dquickts phù hợp để làm gì?**

README của nó nêu các công dụng: cho một script đang rảnh ngủ đông, chia một phép tính dài ra nhiều worker, khởi động nóng từ một snapshot, và để một script chờ ở một lần gọi host mà câu trả lời đến sau nhiều giờ. Tất cả đều là công dụng thật. Dù vậy, một luồng đồng bộ vẫn không nên phụ thuộc vào nó: snapshot chỉ nạp được vào đúng bản build engine đã ghi ra nó, người duyệt không nhìn được bên trong, và nó chứa mọi dữ liệu khách hàng mà script đã đọc.

## Bài viết liên quan

### [Đáp án đúng, nhưng con đường sai](https://tajo.io/vi/blog/the-answer-is-right-the-path-is-wrong/)

8 thg 10, 2026 · 5 phút đọc

3 phần

### [Nên ghi log những gì cho mỗi lần gọi công cụ MCP](https://tajo.io/vi/blog/what-to-log-for-every-mcp-tool-call/)

6 thg 11, 2026 · 11 phút đọc

6 phần

### [Máy chủ MCP là gì? MCP và MCP UI hoạt động thế nào năm 2026](https://tajo.io/vi/blog/what-is-an-mcp-server/)

3 thg 10, 2026 · 20 phút đọc

Bước 1 → Bước 6
