Viết MCP server để agent LLM đọc được hộp thư của tôi
Tôi bọc IMAP trong một MCP server TypeScript nhỏ: cách thiết kế tool, ranh giới an toàn (chỉ tạo nháp, không gửi) và những gì tôi sẽ sửa.
Tôi muốn agent trả lời một loại câu hỏi: “đêm qua có thư gì, thư nào cần trả lời?”. Dán từng email vào khung chat thì không đi xa được, nên tôi viết mcp-imap-server, một server TypeScript nhỏ cho Claude truy cập hộp thư có kiểm soát. Khoảng 800 dòng, bốn commit, nên đọc bài này như ghi chép từ một dự án nhỏ, không phải hệ thống production.
MCP là gì
Model Context Protocol là chuẩn để một host agent (Claude Desktop, Claude Code) nói chuyện với một tiến trình cục bộ hoặc từ xa có cung cấp tool. Server khai báo mỗi tool bằng tên, mô tả và schema đầu vào. Model đọc những thứ đó, tự quyết khi nào gọi, host chạy tool rồi trả kết quả về. Với tôi, đó là một tiến trình stdio do host khởi động, thông tin đăng nhập hộp thư truyền qua biến môi trường.
Thiết kế tool
Agent không nên thấy IMAP thô. Folder, UID, flag là từ vựng của hộp thư, nên tôi giữ tool sát với việc người ta làm:
list_folders,list_messages,get_message,search_messagesđể đọcmark_message,move_message,delete_messageđể phân loạicreate_draft,create_reply_draftđể soạn thưlist_templates,render_templatecho mẫu trả lời dùng lại
Đăng ký tool gồm tên, mô tả và schema zod. Model thực sự đọc phần mô tả, nên tôi viết nó như docstring cho một đồng nghiệp:
server.tool(
"list_messages",
"List recent messages in a folder with subject, from, date. Default folder is INBOX.",
{
folder: z.string().default("INBOX").describe("Mailbox folder path"),
limit: z.number().min(1).max(100).default(20).describe("Max messages to return"),
},
async ({ folder, limit }) => {
const client = await getClient();
const lock = await client.getMailboxLock(folder);
try {
// lấy envelope + flag của `limit` thư gần nhất
} finally {
lock.release();
}
}
);
Ba quyết định nhỏ có ích. list_messages chỉ trả envelope và cờ đã đọc, nên danh sách 20 thư rẻ về token, agent chỉ gọi get_message cho thư thật sự cần. limit bị chặn ở 100 ngay trong schema, model không xin được cả hộp thư. Và mỗi handler lấy mailbox lock rồi nhả trong finally, một lần gọi lỗi không làm kẹt kết nối.
Ranh giới an toàn nằm ở đâu
Nói thẳng: server này không phải read-only. Nó đặt flag, chuyển và xoá thư được, và delete_message có tuỳ chọn permanent đặt \Deleted rồi expunge. Mặc định là chuyển vào Trash. Tuỳ chọn đó là thứ rủi ro nhất trong dự án, và nó có mặt vì tôi thêm vào mà chưa nghĩ kỹ.
Phần ghi thì tôi làm đúng. Không có dòng SMTP nào. create_draft và create_reply_draft chỉ thêm thư vào folder Drafts rồi dừng. Bản nháp trả lời đặt In-Reply-To và References để vào đúng luồng thư. Người thật mở nháp, đọc, rồi tự bấm gửi. Nếu model nhầm, hoặc một email chứa prompt injection, kết quả xấu nhất của nhóm tool ghi là một bản nháp tệ nằm trong folder.
Thông tin đăng nhập lấy từ IMAP_HOST, IMAP_USER, IMAP_PASSWORD, IMAP_PORT, IMAP_TLS, đọc một lần trong getConfigFromEnv() và báo lỗi nếu thiếu. .env nằm trong .gitignore, repo chỉ có .env.example với giá trị giả như imap.example.com. Model không thấy credential vì tool trả nội dung thư chứ không trả cấu hình. Nhưng chúng nằm trong file cấu hình MCP, nên file đó cần được bảo vệ như chính hộp thư.
Tôi sẽ sửa gì
- Tách đọc và ghi. Cách sửa thật thà là hai server, hoặc một cờ lúc khởi động, để phiên “tóm tắt hộp thư” không thể xoá gì. Tôi cũng sẽ bỏ xoá vĩnh viễn hoặc đặt sau một opt-in riêng.
- Coi thư là đầu vào không tin cậy. Mọi thứ từ
get_messageđi thẳng vào ngữ cảnh của model. Một email xấu có thể mang chỉ thị. Quy tắc không-gửi là thứ giới hạn thiệt hại, tôi sẽ không bỏ nó. - Kiểm tra repo build được từ bản clone sạch.
tools.tsimport./signature.js, một file tôi chưa commit, nên clone mới sẽ không biên dịch được. Máy tôi chạy được, và đúng kiểu lỗi này sống sót như vậy. Tôi đã commit file đó và clone sạch giờ build được. - Thêm test. Hiện chưa có. Các lệnh gọi IMAP mỏng, nhưng header của bản nháp đáng có ít nhất một phép kiểm.
Điều rút ra
Danh sách tool quyết định agent làm được gì, bất kể prompt viết ra sao. Cho agent soạn nháp nhưng không bao giờ gửi chỉ tốn một lựa chọn thiết kế, không thêm dòng code nào. Xoá vĩnh viễn là tool tôi lẽ ra phải hỏi lại sớm hơn.