Máy chủ MCP của tôi từng đột ngột ngừng hoạt động. Không có bản ghi lỗi (crash dump). Không có dấu vết ngăn xếp (stack trace) trong nhật ký (logs). Các client vẫn kết nối mà không hề phàn nàn, rồi sau vài giờ, toàn bộ hệ thống trở nên im lìm. Các yêu cầu biến mất và tác nhân AI (AI agent) ở đầu bên kia không nhận được gì ngoài sự trống rỗng.

Đây là một câu chuyện phổ biến đến mức gây nản lòng trong hệ sinh thái Model Context Protocol (MCP). Giao thức này định nghĩa cách các tác nhân AI khám phá và gọi các công cụ bên ngoài, nhưng đặc tả kỹ thuật lại mặc định rằng bạn sẽ tự xử lý các lỗi. Hầu hết các hướng dẫn và các bản triển khai ban đầu đều bỏ qua phần đó. Họ chỉ tập trung vào "happy path" (luồng hoạt động lý tưởng): chú thích một hàm, công khai nó thông qua máy chủ và trả về một kết quả sạch sẽ. Họ hiếm khi chỉ cho bạn điều gì sẽ xảy ra khi có sự cố mạng nhỏ xảy ra với API bên ngoài, hoặc khi mô hình bị "ảo giác" (hallucinate) tên tham số và gửi đầu vào rác. Kết quả là một máy chủ mong manh, trông có vẻ vẫn đang hoạt động bình thường nhưng thực tế đã "chết" từ nhiều giờ trước.

Tại sao phản hồi trống còn tệ hơn cả việc bị sập (crash)

Khi một ngoại lệ (exception) không được xử lý lọt qua trình xử lý công cụ MCP, lớp vận chuyển (transport layer) thường sẽ "nuốt chửng" nó. Tiến trình máy chủ vẫn duy trì, socket vẫn mở, nhưng client lại nhận được một phản hồi trống rỗng. Điều này còn nguy hiểm hơn một vụ sập máy rõ ràng vì hệ thống giám sát của bạn có thể sẽ không nhận ra. Tiến trình vẫn đang chạy. Cổng (port) vẫn đang lắng nghe. Thế nhưng, mọi lệnh gọi công cụ đều không trả về gì cả.

Mô hình AI không hiểu sự im lặng là một thất bại. Nó hiểu sự im lặng là một lệnh gọi thành công nhưng không tạo ra dữ liệu. Phản hồi trống đó sẽ "huấn luyện" mô hình cách ứng biến sai lầm. Nó bắt đầu ảo giác ra các sự thật để lấp đầy khoảng trống, hoặc rơi vào vòng lặp thử lại cùng một lệnh gọi bị lỗi. Những vấn đề nhỏ như hết thời gian chờ mạng tạm thời (transient network timeout) hoặc tham số công cụ không hợp lệ không bao giờ được phép gây ra kiểu hành vi này.

Mô hình Wrapper: Ba lớp phòng thủ

Tôi đã khắc phục điều này bằng cách bao bọc (wrap) mọi trình xử lý công cụ trong một lớp phục hồi lỗi mỏng. Wrapper này không cố gắng dự đoán mọi lỗi có thể xảy ra. Thay vào đó, nó phân loại chúng và phản hồi một cách tương ứng.

ConnectionError và TimeoutError
Những lỗi này phát sinh khi máy chủ của bạn giao tiếp với một API bên ngoài và mạng gặp trục trặc. Cách sửa chữa theo bản năng là khởi động lại toàn bộ tiến trình máy chủ MCP. Đừng làm vậy. Việc khởi động lại sẽ làm ngắt các kết nối client đang hoạt động, xóa sạch mọi trạng thái trong bộ nhớ và buộc phải khởi tạo lại toàn bộ. Thay vào đó, hãy bắt lỗi kết nối và chỉ kết nối lại lớp vận chuyển hoặc HTTP client mà công cụ của bạn đang sử dụng. Máy chủ sẽ luôn ở trạng thái sẵn sàng cho yêu cầu tiếp theo ngay lập tức.

ValueError
Đây là lỗi bạn gặp phải khi client AI gửi các tham số sai định dạng. Có thể mô hình đã tự chế ra một tham số, truyền một chuỗi (string) vào nơi cần số nguyên (integer), hoặc quên một trường bắt buộc. Nếu bạn để lỗi này trôi lên mà không xử lý, client sẽ nhận được một vụ sập hoặc một phản hồi trống. Hãy bắt lỗi bên trong wrapper, sau đó xây dựng một thông báo rõ ràng, cụ thể để cho mô hình biết chính xác điều gì đã sai. Hãy giải thích tham số nào bị lỗi và giá trị mong đợi là gì. Hầu hết các mô hình AI hiện đại sẽ đọc thông báo đó và tự sửa lỗi ngay trong lượt tiếp theo. Một lỗi mơ hồ sẽ làm lãng phí một chu kỳ suy luận. Một lỗi chính xác sẽ giải quyết vấn đề ngay lập tức.

General Exceptions
Hãy giữ một lưới an toàn. Nếu một lỗi nằm ngoài các danh mục trên, hãy ghi nhật ký chi tiết để tự kiểm tra và trả về một phản hồi thất bại chung, sạch sẽ cho client. Điều này ngăn chặn một trường hợp biên (edge case) kỳ lạ làm hỏng phiên làm việc của tất cả mọi người. Máy chủ vẫn tồn tại, client nhận được tín hiệu rằng có lỗi xảy ra, và bạn vẫn giữ đủ ngữ cảnh trong nhật ký để gỡ lỗi sau này.

Cờ isError là điều bắt buộc

Đây là chi tiết thực sự quyết định liệu cách sửa lỗi của bạn có hiệu quả hay không. Các phản hồi MCP bao gồm một trường boolean isError. Nếu một ngoại lệ xảy ra và bạn trả về một thông báo lỗi mà không đặt isError thành true, client sẽ coi văn bản lỗi đó như một kết quả công cụ thành công.

Hãy tưởng tượng API bên ngoài của bạn chạm giới hạn tốc độ (rate limit). Bạn bắt được ngoại lệ và trả về chuỗi "API rate limit exceeded" nhưng lại để isErrorfalse. Client sẽ đưa chuỗi đó vào cửa sổ ngữ cảnh (context window) của mô hình như thể đó là kết quả thực sự của công cụ. Sau đó, mô hình sẽ cố gắng suy luận dựa trên văn bản đó như thể nó là dữ liệu. Nó có thể trích dẫn lỗi trong một bản tóm tắt, hoặc tệ hơn, nó có thể ảo giác ra các mối quan hệ giữa văn bản lỗi đó và các sự thật khác. Bạn đã biến một sự cố hạ tầng tạm thời thành một nguồn thông tin sai lệch.

Luôn đặt isError thành true khi bạn trả về một error payload. Điều này cung cấp cho client một tín hiệu rõ ràng rằng việc gọi tool đã thất bại, cho phép model quyết định xem nên thử lại, yêu cầu làm rõ, hoặc thử một tool hoàn toàn khác.

Biết cái gì cần bắt (catch) và cái gì cần dừng (kill)

Đừng bao bọc toàn bộ server của bạn trong một khối try-catch mù quáng nhằm nuốt chửng mọi thứ. Một số lỗi có nghĩa là server nên dừng lại ngay lập tức. Nếu một biến môi trường (environment variable) bắt buộc bị thiếu khi khởi động, hoặc tệp cấu hình của bạn bị hỏng, thì việc bắt lỗi ở cấp độ request cũng không có tác dụng gì. Hãy tạo một class exception cụ thể cho các lỗi nghiêm trọng (fatal errors) như thế này và để chúng làm crash tiến trình.

Quy tắc rất đơn giản. Nếu lỗi chỉ mang tính tạm thời hoặc chỉ xảy ra với một request duy nhất, hãy bắt lỗi và khôi phục. Nếu lỗi đó có nghĩa là mọi request tiếp theo chắc chắn sẽ thất bại, hãy để server "chết" một cách rõ ràng. Một lỗi xảy ra nhanh chóng ngay khi khởi động sẽ tốt hơn vô vàn lần so với một server cứ lết đi trong tình trạng lỗi suốt nhiều ngày.

Thêm khả năng quan sát (Observability) trước khi bạn thực sự cần đến nó

Một khi bạn đã triển khai xong wrapper, hãy kết hợp nó với structured logging. Hãy log mọi lần gọi tool và kết quả của nó dưới định dạng JSON. Bao gồm tên tool, các tham số thô (raw arguments), độ trễ (latency), và liệu nó đã thành công, thất bại, hay được thử lại.

Kỷ luật này sẽ mang lại thành quả nhanh chóng. Khi bạn nhận thấy số lượng lỗi tăng đột biến, bạn có thể lọc theo tool và phát hiện ra các quy luật chỉ trong vài phút. Có thể một API bên ngoài cụ thể bắt đầu gặp lỗi timeout vào cùng một thời điểm mỗi ngày, ám chỉ một khung giờ bảo trì định kỳ mà bạn không hề biết. Có thể một tool liên tục nhận được các tham số sai định dạng (malformed arguments), tiết lộ một lỗi prompt engineering ở phía thượng nguồn (upstream). Các log dạng văn bản thuần túy bị chôn vùi trong stack traces khiến công việc điều tra này trở nên đau đớn. JSON có cấu trúc sẽ khiến việc này trở nên cực kỳ dễ dàng.

Kết quả thực tế trên môi trường Production

Tôi đã chạy pattern wrapper này trên hai MCP server production trong ba tuần qua. Trong khoảng thời gian đó, tôi không thấy bất kỳ lỗi im lặng (silent failure) nào. Trước khi thêm wrapper, trung bình mỗi ngày tôi gặp khoảng một lỗi không rõ nguyên nhân. Pattern này không phức tạp, nhưng tác động của nó là cực kỳ lớn vì nó tách biệt được những "nhiễu" có thể chịu đựng được khỏi những vấn đề thực sự.

Những lỗi im lặng gây tốn kém hơn cả các vụ crash. Một vụ crash sẽ kích hoạt hệ thống cảnh báo của bạn. Sự im lặng chỉ làm xói mòn lòng tin. Một ngày nọ, AI agent của bạn trả về dữ liệu tool hữu ích, nhưng ngày hôm sau nó bắt đầu "chế" ra mọi thứ vì server đã ngừng phản hồi từ nhiều giờ trước. Pattern wrapper giúp lấp đầy khoảng trống đó. Nó giữ cho server của bạn tiếp tục chạy qua những biến động nhỏ, cung cấp cho model đủ ngữ cảnh để tự sửa lỗi, và đảm bảo rằng khi có điều gì đó thực sự nghiêm trọng xảy ra, bạn sẽ biết ngay lập tức.

Nếu bạn đang xây dựng các MCP tool ngay hôm nay, hãy bắt đầu với wrapper và flag isError. Mọi thứ khác chỉ là bước dọn dẹp sau đó.