
Dựng MCP server cho SaaS: discovery mở, gate nằm trong code
Mổ xẻ MCP server production 44 tool: stateless transport, discovery không cần key, auth Bearer hash, và vì sao gate của write tool phải nằm trong code.
Mục lục
- Quy mô tham chiếu: 44 tool, 26 read, 18 write
- Transport: stateless, một server cho mỗi POST
- Discovery mở, mọi thứ còn lại cần key
- Auth: Bearer key, chỉ lưu hash
- Annotation: nói cho client biết tool nào an toàn
- Write tool: preview trước, confirm sau
- Vì sao gate phải nằm trong code
- Checklist trước khi public server
Bạn có dashboard, nhưng user đã ngồi sẵn trong Claude, ChatGPT hoặc Cursor. Mỗi lần cần số liệu, họ phải rời chỗ đang làm việc để mở thêm một tab nữa.
Một team SaaS vừa công bố toàn bộ quyết định thiết kế của MCP server họ đang chạy production, kèm code thật. Bài này đi qua các quyết định đó theo thứ tự bạn sẽ phải đưa ra khi tự dựng server cho sản phẩm của mình.
Quy mô tham chiếu: 44 tool, 26 read, 18 write
Tác giả Raphaël Aubry mô tả server chạy production tại https://app.howseen.ai/api/mcp, "version 1.3.0, with 44 tools: 26 read tools and 18 write tools". Con số này đáng ghi lại vì nó cho bạn một mốc tham chiếu có thật về quy mô một bộ tool đang phục vụ user, thay vì một ví dụ hello-world.
Transport: stateless, một server cho mỗi POST
Server là một Next.js route handler chạy trên serverless runtime. Lý do bỏ session: "Keeping MCP sessions in memory between requests does not fit that model, so the server is stateless: one server and one transport per POST, JSON responses, no session id."
Hai config quyết định điều đó. sessionIdGenerator: undefined tắt session, còn enableJsonResponse: true "returns a plain JSON body instead of opening an event stream". Hệ quả: một GET xin text/event-stream nhận 405, còn GET thường trả về "a small JSON description (name, version, tool names, auth rules) that doubles as a health check".
Có một cái bẫy rất dễ dính nếu bạn tự viết: bạn đọc body request một lần để quyết định auth, nên phải đưa cho transport một Request mới với cùng bytes. Nếu không, theo tác giả, "the transport finds an already consumed body".
Discovery mở, mọi thứ còn lại cần key
Đây là quyết định dễ làm sai nhất. Client và directory probe server trước khi user kịp dán key: gọi initialize, rồi tools/list. Hậu quả nếu bạn chặn: "If those calls return 401, your server looks broken in every listing."
Nhóm method được cho qua không cần key gồm initialize, notifications/initialized, ping, tools/list, prompts/list, resources/list và resources/templates/list — toàn bộ chỉ trả metadata tĩnh.
Hai lớp bảo vệ giữ cho lỗ hổng này không bị lợi dụng. Thứ nhất là luật batch: vì JSON-RPC cho phép gửi nhiều message một lượt, hàm kiểm tra "requires every message in the batch to be a discovery method: one tools/call hidden in a batch makes the whole request need a key". Thứ hai là context không key nhận một database handle dạng Proxy "that throws on any access", nên "even a coding mistake cannot leak data".
Auth: Bearer key, chỉ lưu hash
Key do user tự tạo trong Settings, bắt đầu bằng hs_live_, "is shown once, and only its sha256 hash is stored". Server đọc từ header Authorization: Bearer.
Có AI app không gửi được custom header, nên key cũng có thể nằm trong URL dạng ?key=. Nếu làm vậy, tài liệu phải ghi rõ URL đó "must be treated like a password" và key lộ thì revoke được trong Settings.
Một chi tiết ảnh hưởng trực tiếp tới trải nghiệm: khi thiếu hoặc sai key, response 401 mang theo body lỗi JSON-RPC chứ không chỉ status code. Lý do: "MCP clients display that message to the user, so it is where you explain what to do next".
Annotation: nói cho client biết tool nào an toàn
Mỗi tool khai báo annotation. Ví dụ tool đọc đầu tiên trả về "readOnlyHint": true, "destructiveHint": false, "openWorldHint": false. Client dùng readOnlyHint "to decide what they can run without asking" — tức là annotation của bạn quyết định user bị hỏi permission bao nhiêu lần.
Một nguyên tắc nữa đáng bê nguyên: các read tool "call the same data layer as our REST API, so the numbers in Claude match the dashboard". Tác giả giải thích vì sao điều này quan trọng hơn vẻ ngoài của nó: một con số trong chat lệch với số trong app sẽ khiến user nghi ngờ cả hai.
Write tool: preview trước, confirm sau
18 tool ghi đi qua một gate duy nhất, theo thứ tự cố định: thiếu scope write thì forbidden, bị chặn thì blocked, chưa có confirm: true thì trả preview, có rồi mới execute.
Mặc định key là read-only; muốn thay đổi dữ liệu thì key phải có scope write. Khi chưa confirm, tool trả về preview gồm "what will change, the quota impact, and the exact arguments to send back with confirm: true".
Câu quan trọng nhất của cả bài nằm ở đây. Server instructions có dặn model hiện preview, chờ user đồng ý, và không gộp nhiều thay đổi vào một lần approve. Nhưng tác giả viết: "A model can still ignore instructions, which is why the gate is in code and not only in the prompt."
Vì sao gate phải nằm trong code
Một bài tổng hợp 17 tiêu chí cho agent harness, viết từ việc đọc source của OpenHands SDK, OpenCode, Hermes Agent, DeepSeek và Strands, rút ra cùng một luật: "the prompt is for judgement; anything a script can check is enforced in code".
Benchmark MCPHallu (arXiv 2611.07689) cho thấy rủi ro cụ thể. Nhóm tác giả chạy 358 task trên 36 production MCP server trong container cô lập, đánh giá 14 agent từ 8 vendor qua 5.012 trajectory. Điểm trung bình là 0.642 trên thang [0,1]. Nhóm task "Unreachable Goal" tụt xuống 0.458 với chỉ 23,2% số lần chạy đạt, và nhóm nghiên cứu diễn giải rằng agent "often continue issuing tool calls or claim successful completion rather than recognizing infeasible goals".
Đó chính là kịch bản một write tool không có gate sẽ phải chịu: model báo xong trong khi thao tác chưa hợp lệ. Đây là preprint và thang điểm do chính benchmark định nghĩa, nên đọc nó như tín hiệu về hướng rủi ro.
Checklist trước khi public server
- Stateless ngay từ đầu, không dựng session store trên serverless.
- Mở discovery, khóa mọi thứ còn lại, và nhớ luật batch.
- Một data layer dùng chung cho app, REST API và MCP.
- Write tool preview mặc định, enforce bằng code chứ không bằng prompt.
- Thông báo lỗi viết cho người đang đọc trong Claude, không phải cho log file.
- Đặt rate limit theo key — bản tham chiếu dùng 120 request/phút và 5.000/ngày.
Một độc giả trong phần bình luận cho rằng số lượng tool ít quan trọng hơn kích thước schema, vì "every definition goes into each request, so two or three long descriptions can weigh more than forty short ones". Đây là nhận định cá nhân, chưa kèm số đo — nhưng nó gợi một việc nên làm: đo token của tools/list trước khi lo cắt bớt tool.
Việc tiếp theo nếu bạn định dựng: gọi thử tools/list trên server tham chiếu bằng curl để thấy shape response thật, rồi viết gate cho write tool trước khi viết tool thứ hai. Thứ tự đó khó đảo ngược sau này.
Không spam, hủy đăng ký bất kỳ lúc nào.
Bài viết liên quan

Kiểm soát chi phí LLM: ngân sách, fallback và cách đo đúng
05 thg 11, 2026
Cắt chi phí token: prompt caching và đếm token cho đúng
04 thg 11, 2026