Xây Dựng Công Khai: Kiến Trúc Dự Án Rust Đằng Sau Một MCP Server

06 tháng 4, 2026·11 phút đọc

Tác giả chia sẻ hành trình xây dựng Drengr, một máy chủ MCP giúp các tác nhân AI có "mắt và tay" trên thiết bị di động. Bài viết đi sâu vào kiến trúc phần mềm dựa trên Rust, những bài học về lập trình độc lập và nghịch lý khi sử dụng chính AI để phát triển công cụ hỗ trợ AI.

Xây Dựng Công Khai: Kiến Trúc Dự Án Rust Đằng Sau Một MCP Server

Tôi là người tạo ra Drengr, một máy chủ MCP (Model Context Protocol) trao cho các tác nhân AI khả năng "nhìn" và "tương tác" với thiết bị di động. Tôi bắt đầu blog này để chia sẻ về mặt kỹ thuật đằng sau dự án. Không giả vờ là một quan sát viên trung lập viết một bài suy ngẫm — tôi đã xây dựng nó, và tôi ở đây để nói về nó.

Tôi là một lập trình viên độc đang xây dựng một dự án Rust, và tôi muốn nói với bạn về điều đó thực sự trông như thế nào. Không phải phiên bản hoàn hảo "ra mắt trên Product Hunt và nhận 500 sao", mà là phiên bản thực tế — những quyết định kiến trúc được đưa vào lúc nửa đêm, những lỗi mất cả ngày để sửa, và sự mỉa mai kỳ lạ khi sử dụng AI để xây dựng công cụ cho AI.

Drengr bắt đầu như một câu hỏi nghiên cứu: Tôi có thể đưa một chiếc điện thoại cho tác nhân AI không? Không vốn đầu tư mạo hiểm, không đội nhóm, không áp lực thời gian. Chỉ là tò mò và một vấn đề cảm thấy đủ quan trọng để dành nhiều tháng để giải quyết. Xây dựng công khai nghĩa là chia sẻ hành trình một cách trung thực, bao gồm cả những phần không trông ấn tượng.

Tại sao tôi lại tự xây dựng điều này

Câu trả lời trung thực là dự án này bắt đầu trước khi tôi biết đó là một dự án. Sau mười năm viết ứng dụng Android và chứng kiến mọi bộ kiểm thử UI tôi chạm vào mục nát nhanh hơn khả năng bảo trì của chúng, tôi bắt đầu thử nghiệm xem liệu AI có thể làm tốt hơn. Tôi đã hack nhanh một đoạn script Python chụp màn hình và gửi nó tới API LLM cùng hướng dẫn hành động. Nó hoạt động rất tệ, nhưng nó hoạt động. Đoạn script đó trở thành bản mẫu, bản mẫu trở thành kiến trúc, và kiến trúc đòi hỏi Rust.

Không có lúc nào tôi ngồi xuống và nói "tôi sẽ xây dựng một sản phẩm". Tôi cứ giải quyết vấn đề tiếp theo. Vấn đề tiếp theo cứ thế thú vị. Drengr là công cụ thực sự đầu tiên của tôi — thứ đầu tiên tôi xây dựng không phải là một script nội bộ hay một thử nghiệm cuối tuần. Sáu tháng sau, tôi có khoảng 6.300 dòng code Rust, một máy chủ MCP đang hoạt động, và khởi đầu của một thứ tôi nghĩ có thể quan trọng.

Phát triển độc lập có những sự đánh đổi thực sự. Tôi không có ai để review code của mình. Tôi không có ai để thách thức các quyết định kiến trúc của mình. Khi tôi mắc lỗi, không ai bắt được nó cho đến khi người dùng báo cáo lỗi. Đổi lại là tốc độ — tôi có thể refactor toàn bộ lớp transport vào một ngày thứ Bảy mà không cần họp hành.

Kiến trúc

Kiến trúc của Drengr được xây dựng xung quanh một sự trừu tượng hóa cốt lõi: lớp vận chuyển (transport layer).

Transport Trait

Một trait duy nhất của Rust định nghĩa ý nghĩa của việc "nói chuyện với một thiết bị":

trait Transport {
    fn capture_screen(&self) -> Result<Screenshot>;
    fn get_ui_tree(&self) -> Result<Vec<UiElement>>;
    fn execute_action(&self, action: Action) -> Result<()>;
    fn query_state(&self, query: Query) -> Result<StateResponse>;
}

Hiện có ba triển khai: ADB cho thiết bị và trình giả lập Android, simctl cho trình giả lập iOS, và Appium cho các trang thiết bị đám mây. Mỗi cái sử dụng một giao thức hoàn toàn khác nhau. ADB sử dụng các lệnh shell và giao thức nhị phân. Simctl sử dụng công cụ dòng lệnh của Apple. Appium sử dụng HTTP/WebDriver.

Phần còn lại của codebase không biết và không quan tâm đến cái nào đang hoạt động. Bộ xử lý MCP, vòng lặp OODA, hệ thống chú thích màn hình — tất cả đều hoạt động thông qua trait đó. Thêm một nền tảng mới nghĩa là triển khai bốn phương thức.

Bộ xử lý MCP

Máy chủ MCP đọc JSON-RPC từ stdin và viết phản hồi ra stdout. Điều này nghe có vẻ đơn giản cho đến khi bạn nhận ra rằng các tương tác thiết bị cũng ghi ra stdout (ví dụ: các lệnh ADB tạo ra đầu ra). Một trong những quyết định kiến trúc sớm nhất của tôi là chuyển hướng I/O của quy trình con để tránh làm ô nhiễm kênh MCP.

Bộ xử lý định tuyến các cuộc gọi công cụ đến một trong ba đường dẫn: drengr_look kích hoạt chụp màn hình và trích xuất cây UI, drengr_do gửi một hành động đến lớp vận chuyển, và drengr_query đọc trạng thái mà không có tác dụng phụ.

Chú thích màn hình

Khi tác nhân gọi drengr_look, nó không chỉ nhận được một ảnh chụp màn hình thô. Drengr trích xuất thứ bậc UI, xác định các phần tử tương tác, gán một con số cho mỗi phần tử, và trả về cả thông tin đã chú thích và siêu dữ liệu của phần tử. Sau đó tác nhân có thể nói "chạm vào phần tử 7" thay vì "chạm vào tọa độ (342, 891)".

Hệ thống chú thích này quan trọng hơn nó có vẻ. Nó bắc cầu khoảng cách giữa cách AI nhìn nhận màn hình (như một trường thị giác) và cách thiết bị chấp nhận đầu vào (như các lệnh có cấu trúc). Nếu không có nó, mọi tương tác đều yêu cầu tác nhân ước lượng tọa độ pixel từ việc kiểm tra thị giác, điều này không đáng tin cậy.

Những bài học từ 6.300 dòng Rust

Trình biên dịch là người review code khắt khe nhất của bạn. Tôi đã đếm không xuể số lần borrow checker từ chối code mà tôi tin là đúng, chỉ để nhận ra khi suy ngẫm lại rằng nó đang bắt một vấn đề thực sự. Không phải lúc nào cũng là lỗi — đôi khi là vấn đề thiết kế. "Bạn không thể giữ một tham chiếu mutable đến transport trong khi lặp qua kết quả cây UI của nó" là cách nói của trình biên dịch rằng "dòng dữ liệu của bạn đang rối tung".

Nếu nó biên dịch được, nó có thể sẽ hoạt động. Câu sáo rỗng này có giới hạn — lỗi logic vẫn tồn tại, kiểm thử tích hợp vẫn quan trọng — nhưng mật độ lỗi runtime trên mỗi dòng code thấp hơn bất kỳ ngôn ngữ nào tôi từng trải nghiệm. Khi tôi gặp lỗi, nó gần như luôn nằm trong logic của tôi, không phải trong quản lý bộ nhớ, không phải trong xử lý lỗi, và không phải trong mô hình đồng thời.

Ngữ nghĩa sở hữu (ownership semantics) ép buộc kiến trúc tốt hơn. Trong Python hay JavaScript, tôi có thể tự do truyền kết nối transport đi khắp nơi, có lẽ lưu tham chiếu ở ba chỗ khác nhau. Rust buộc tôi phải suy nghĩ về ai sở hữu kết nối và ai mượn nó. Ràng buộc đó đã tạo ra một kiến trúc sạch hơn nhiều so với những gì tôi sẽ tự nguyện thiết kế.

Con Bug Khó Nhất

MCP qua stdio có nghĩa là Drengr đọc các yêu cầu JSON-RPC từ stdin và ghi phản hồi ra stdout. Đơn giản đủ — cho đến khi bạn tạo ra một lệnh shell ADB cũng ghi ra stdout.

Lần đầu tiên điều này xảy ra, máy khách MCP nhận được một phản hồi bắt đầu bằng một khung JSON-RPC hợp lệ, tiếp tục là "List of devices attached", và sau đó có một khung JSON-RPC khác. Máy client hiển nhiên bị "choking" (kẹt).

Giải pháp yêu cầu chuyển hướng tất cả stdout của quy trình con sang /dev/null hoặc bộ đệm đã ghi, sử dụng os::unix::iodup2 để quản lý các bộ mô tả tệp ở mức gọi hệ thống. Nó khoảng 30 dòng code. Tôi đã mất hai ngày rưỡi để debug, vì các triệu chứng không liên tục — ADB chỉ ghi ra stdout trong các điều kiện nhất định, vì vậy sự hỏng hóc của MCP là thưa thớt.

Đây là loại bug không tồn tại trong các kiến trúc đơn giản hơn. Nếu Drengr là một máy chủ HTTP thay vì máy chủ stdio, vấn đề sẽ không bao giờ nảy sinh. Nhưng MCP qua stdio là tiêu chuẩn cho các máy chủ công cụ cục bộ, và có lý do chính đáng — nó đơn giản hơn cho máy khách, không yêu cầu quản lý cổng, và hoạt động trong các môi trường sandbox. Sự phức tạp là xứng đáng; cái bug là giá phải trả.

Sự Mỉa Mai Khi Sử Dụng AI Để Xây Dựng Công Cụ AI

Tôi sử dụng Claude Code hàng ngày để làm việc trên Drengr. Claude giúp tôi viết code dạy cho Claude cách sử dụng điện thoại. Sự đệ quy này không bị tôi bỏ qua.

Nó thực sự năng suất. Claude giỏi Rust — nó hiểu các mẫu ownership, đề xuất các cách tiếp cận idiom (đặc trưng), và bắt các vấn đề tôi bỏ sót. Khi tôi đang triển khai động cơ tình huống, Claude đã giúp tôi suy nghĩ logic so sánh trạng thái. Khi tôi đang vật lộn với các đối tượng trait async, Claude đã giải thích mẫu Pin<Box<dyn Future>> theo cách mà cuối cùng tôi cũng hiểu được.

Mỉa mai hơn thế nữa. Mọi cải tiến tôi thực hiện cho Drengr đều làm cho Claude tốt hơn một chút trong việc tương tác với thiết bị di động. Hệ thống chú thích màn hình tốt hơn có nghĩa là Claude nhận được thông tin tốt hơn. Động cơ tình huống tốt hơn có nghĩa là Claude mắc ít lỗi hơn. Tôi đang xây dựng một công cụ cải thiện khả năng của AI giúp tôi xây dựng công cụ đó.

Tôi không nghĩ điều này là duy nhất cho dự án của tôi. Mọi lập trình viên sử dụng AI để xây dựng công cụ AI đều ở trong vòng lặp phản hồi này. Nhưng làm việc với nó hàng ngày khiến vòng lặp trở nên rất hữu hình.

Những Điều Sẽ Có Trên Lộ Trình

Ba thứ tôi đang tích cực làm việc:

  • Dashboard. Một giao diện web để trực quan hóa các lần chạy kiểm thử, xem xét các quyết định của tác nhân và tương quan các hành động UI với lưu lượng mạng. Đặc tả kỹ thuật đã được viết; việc triển khai là tiếp theo.
  • Điều khiển thời gian thực (Real-time steering). Khả năng xem một tác nhân chạy và chuyển hướng nó giữa phiên. "Dừng khám phá cài đặt, hãy đi kiểm tra luồng thanh toán thay vào đó." Điều này yêu cầu kết nối WebSocket giữa dashboard và quy trình Drengr đang chạy.
  • Giám sát mạng. Một SDK mà các ứng dụng có thể tích hợp để bắt lưu lượng mạng trong các phiên Drengr. Điều này cho phép dashboard hiển thị các cuộc gọi API nào đã xảy ra cùng với mỗi hành động UI — vô giá để debug các vấn đề tích hợp.

Cách Thức Tham Gia

Drengr là độc quyền, nhưng cộng đồng là mở. Tôi đã thiết lập GitHub Discussions cho các câu hỏi, phản hồi và yêu cầu tính năng. Các khu vực tôi đánh giá cao sự đóng góp nhất:

  • Kiểm thử trên các thiết bị đa dạng. Tôi phát triển trên một tập hợp hạn chế các cấu hình trình giả lập. Các báo cáo về cách Drengr hoạt động trên các phiên bản Android, kích thước màn hình và lớp phủ giao diện của nhà sản xuất khác nhau là cực kỳ có giá trị.
  • Prompt engineering cho các kịch bản kiểm thử. Chất lượng kiểm thử tự động của Drengr phụ thuộc nhiều vào việc mục tiêu được diễn đạt như thế nào. Tôi đang thu thập các prompt hiệu quả và rất mong muốn các đóng góp.
  • Báo cáo lỗi và ý tưởng tính năng. Cách tốt nhất để định hình hướng đi của Drengr là sử dụng nó và cho tôi biết còn thiếu gì.

Hoặc chỉ cần thử nó. curl -fsSL https://drengr.dev/install.sh | bash. Kết nối một thiết bị. Hướng Claude vào nó. Cho tôi biết điều gì xảy ra.

Phản hồi tốt nhất không phải là "dự án tuyệt vời". Nó là "tôi đã thử cái này và nó bị lỗi". Đó là cách công cụ trở nên tốt hơn.

Xây dựng công khai có nghĩa là chấp nhận rằng mọi người sẽ thấy những góc cạnh thô ráp. Tôi ổn với điều đó. Những góc cạnh thô ráp là nơi những vấn đề thú vị sống.

Drengr miễn phí để sử dụng và có sẵn trên npm. Nó hỗ trợ Android (thiết bị vật lý, trình giả lập), trình giả lập iOS (hỗ trợ cử chỉ đầy đủ) và các trang thiết bị đám mây (BrowserStack, SauceLabs, AWS Device Farm, LambdaTest, Perfecto, Kobiton). Được xây dựng bằng Rust. Tệp nhị phân duy nhất. Không có phụ thuộc runtime.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗