Tưởng tượng bạn đang ngồi trong một cuộc họp có phiên dịch viên. Bạn nói tiếng Việt, đối tác nói tiếng Nhật. Phiên dịch viên dịch qua lại. Nếu bạn muốn biết chính xác 2 bên đã nói gì, bạn cần đọc biên bản — nơi ghi lại từng câu, ai nói với ai, loại nào là câu hỏi (cần trả lời), loại nào là thông báo (không cần trả lời).
- Giải thích được mọi MCP message đều là JSON và phân 2 loại chính: request-result và notification.
- Nhận diện được ai là phía gửi (client hay server) cho từng loại message cụ thể.
- Liệt kê được 4-6 message type phổ biến nhất trong production (CallTool, ListTools, Initialize, Progress, v.v.).
- Tự đọc được MCP specification trên GitHub và tìm ra định nghĩa của bất kỳ message nào.
- Hiểu được vì sao hiểu message types là điều kiện tiên quyết để debug transport issues ở các bài sau.
Mở đầu: Mọi cuộc hội thoại đều là bản chép lại
MCP hoạt động y hệt vậy.
Khi Claude Desktop (client) gọi một tool trên MCP server của bạn, về mặt kỹ thuật không có ma thuật nào xảy ra. Chỉ có các JSON message được gửi qua lại. Bạn có thể mở một terminal, paste một JSON vào stdin của server, và nhận về response — không cần Claude, không cần SDK phức tạp gì cả.
Bài này dạy bạn ngôn ngữ trao đổi đó. Một khi bạn hiểu MCP "nói" gì, bạn có thể:
- Debug bằng terminal khi SDK log không đủ rõ.
- Đọc MCP spec thay vì đoán mò.
- Hiểu vì sao một số tính năng "không work trên HTTP" (hint: loại message nào không đi qua được HTTP đơn giản).
Bắt đầu từ format chuẩn nhé.
Mọi message đều là JSON-RPC 2.0
MCP không tự phát minh ra format mới. Nó dùng chuẩn JSON-RPC 2.0 — một spec đã có từ 2010, được dùng rộng rãi trong Ethereum, Lightning Network, Language Server Protocol (LSP), và nhiều hệ thống khác.
Ý tưởng chính: mọi message đều là JSON object với một số field tối thiểu cố định.
Ví dụ minh họa: Call Tool Request
Khi Claude muốn gọi tool add(a=2, b=3) của bạn, client gửi JSON sau:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 2, "b": 3 }
}
}
Server xử lý xong, trả về:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{ "type": "text", "text": "5" }
]
}
}
Chú ý id: 42 — đó là cái "ticket" để client biết response nào match với request nào khi chúng xen kẽ trên cùng một kênh.
Hai loại message — quan trọng nhất trong bài
MCP chia mọi message thành 2 nhóm lớn, và sự khác biệt giữa 2 nhóm này là gốc rễ của mọi thứ bạn sẽ học về transport sau này.
┌─────────────────────────────────────────────────────────┐
│ │
│ NHÓM 1: REQUEST ↔ RESULT │
│ ──────────────────── │
│ • Đi theo cặp: 1 request → đúng 1 result │
│ • Có field "id" để match request với result │
│ • Bên gửi ĐỢI bên kia trả lời │
│ │
│ NHÓM 2: NOTIFICATION │
│ ──────────────────── │
│ • Đi một chiều — gửi xong là xong │
│ • KHÔNG có field "id" │
│ • Bên gửi KHÔNG đợi phản hồi │
│ │
└─────────────────────────────────────────────────────────┘
Nhớ 2 tiêu chí phân biệt: có id không và bên gửi có đợi response không.
Nhóm 1: Request ↔ Result
Đây là các cặp "gọi và trả lời":
| Request (A gửi) | Result (B trả về) | Mục đích |
|---|---|---|
| initialize | InitializeResult | Client tự giới thiệu & khai báo capabilities |
| tools/list | ListToolsResult | Lấy danh sách tool server đang expose |
| tools/call | CallToolResult | Gọi 1 tool cụ thể |
| resources/list | ListResourcesResult | Lấy danh sách resources |
| resources/read | ReadResourceResult | Đọc nội dung 1 resource |
| prompts/list | ListPromptsResult | Lấy danh sách prompt templates |
| prompts/get | GetPromptResult | Lấy nội dung 1 prompt cụ thể |
| sampling/createMessage | CreateMessageResult | Server xin client gọi model (bài 10.8) |
| roots/list | ListRootsResult | Server xin client danh sách thư mục được phép (bài 10.10) |
Để ý 2 dòng cuối: server là bên gửi request. Đây là điểm mấu chốt — MCP bidirectional, không chỉ client → server.
Nhóm 2: Notification
Các message "thông báo một chiều", không có response:
| Notification | Hướng | Mục đích |
|---|---|---|
| notifications/initialized | Client → Server | "Tôi đã sẵn sàng rồi, bắt đầu được" |
| notifications/progress | Server → Client | Báo tiến độ cho tool chạy lâu |
| notifications/message (log) | Server → Client | Log message để user xem |
| notifications/tools/list_changed | Server → Client | "Danh sách tool tôi vừa cập nhật, hãy refresh lại" |
| notifications/resources/updated | Server → Client | Resource X đã thay đổi |
| notifications/cancelled | Cả hai chiều | Huỷ request đang chạy |
Notification không có id. Bên gửi bắn đi và quên luôn — không đợi trả lời, không có callback. Đây là lý do chúng phù hợp với progress updates và logs: gửi nhiều, gửi nhanh, không block.
So sánh nhanh: Request vs Notification
| Tiêu chí | Request-Result | Notification |
|---|---|---|
| Có field id | Có | Không |
| Cần response | Đúng 1 response | Không |
| Block sender | Sender đợi | Fire-and-forget |
| Ví dụ điển hình | tools/call, initialize | Progress, log, resource updated |
| Phù hợp với | Thao tác cần kết quả | Thông báo trạng thái |
| Gửi được trên HTTP đơn giản? | Client → Server: dễ; Server → Client: khó (bài 10.3) | Cả 2 chiều đều khó trên HTTP |
Dòng cuối cùng sẽ quay lại ám ảnh bạn ở bài 10.3, 10.5, 10.6.
Spec ở đâu? Đọc TypeScript mà không chạy TypeScript
Toàn bộ message types được định nghĩa chính thức trong repo:
👉 github.com/modelcontextprotocol/specification
Điều quan trọng bạn cần biết:
- Spec được viết bằng TypeScript không phải vì MCP chạy TypeScript, mà vì TypeScript là ngôn ngữ mô tả data structure rõ nhất cho developer ngoài ecosystem JS.
- Các SDK (Python, TypeScript, Go, Rust, Java...) là repo riêng. Chúng implement theo spec, không định nghĩa spec.
- Khi có conflict giữa SDK behavior và spec → spec thắng. SDK có thể có bug; spec là ground truth.
Mini-tour spec file
File trung tâm: schema.ts. Ví dụ cách spec định nghĩa CallToolRequest:
export interface CallToolRequest extends Request {
method: "tools/call";
params: {
name: string;
arguments?: { [key: string]: unknown };
};
}
Đọc giống đọc Python class — field nào required, field nào optional, kiểu dữ liệu gì. Khi lỗi xảy ra trong production, bạn mở file này ra, find cái message tên lạ, biết ngay client đang expect gì.
Ai gửi cho ai — Client messages vs Server messages
Spec phân loại message theo bên gửi, không theo bên nhận. Điều này quan trọng khi bạn debug.
Client gửi đi (Client messages)
- Tất cả request kiểu "get/list/call":
initialize,tools/list,tools/call,resources/read,prompts/get - Notification:
notifications/initialized,notifications/cancelled, thỉnh thoảngnotifications/roots/list_changed - Response cho các server request:
CreateMessageResult(đáp lại sampling),ListRootsResult(đáp lại roots request)
Server gửi đi (Server messages)
- Kết quả của mọi request từ client (Result object)
- Request của chính server:
sampling/createMessage,roots/list - Notifications:
progress,message(log),tools/list_changed,resources/updated,resources/list_changed,prompts/list_changed
Nhấn mạnh lại: server cũng gửi request được, không chỉ nhận. Nhóm này gồm sampling và roots — chính là 2 chủ đề chính của cụm bài sau.
Ví dụ thực chiến: Đọc raw message flow
Tình huống: user hỏi Claude "Dùng MCP research tool của tôi để research về Transformer architecture."
Đây là sequence raw message (rút gọn, bỏ các field metadata):
[1] Client → Server (request, id=1)
method: "initialize"
params: { clientInfo: {name: "Claude Desktop", version: "..."} }
[2] Server → Client (result, id=1)
result: { serverInfo: {...}, capabilities: {tools: {}, sampling: {}} }
[3] Client → Server (notification)
method: "notifications/initialized"
[4] Client → Server (request, id=2)
method: "tools/list"
[5] Server → Client (result, id=2)
result: { tools: [{name: "research", description: "..."}] }
[6] Client → Server (request, id=3)
method: "tools/call"
params: { name: "research", arguments: {topic: "Transformer"} }
[7] Server → Client (notification)
method: "notifications/progress"
params: { progress: 20, total: 100, message: "Fetching Wikipedia..." }
[8] Server → Client (request, id=4) ← server gửi request!
method: "sampling/createMessage"
params: { messages: [...], maxTokens: 4000 }
[9] Client → Server (result, id=4)
result: { content: {type: "text", text: "Summary..."} }
[10] Server → Client (notification)
method: "notifications/progress"
params: { progress: 90, total: 100, message: "Finalizing..." }
[11] Server → Client (result, id=3)
result: { content: [{type: "text", text: "...report..."}] }
Đọc kỹ. Bạn thấy:
- Step 1-3: handshake 3 bước (
initialize→InitializeResult→initializednotification). Đây là handshake bắt buộc của mọi session MCP. - Step 4-5: client hỏi "có tool gì", server list.
- Step 6: client gọi tool
research, id=3. - Step 7 & 10: server gửi progress notification trong khi đang xử lý. User thấy progress bar chạy.
- Step 8-9: server gửi
sampling/createMessagerequest tới client. Client tự gọi Claude (dùng API key của client), rồi trảCreateMessageResultvề. - Step 11: sau khi có kết quả sampling, server hoàn thành
researchvà trảCallToolResultcho client.
Quan sát quan trọng: Step 8 là server khởi tạo request. Trên stdio transport, không vấn đề gì. Trên HTTP thô, điều này cực kỳ khó — server không có URL của client để gọi ngược. Đây chính là lý do phải có SSE workaround trong Streamable HTTP (bài 10.4).
Ví dụ theo ngành — Vì sao message types matter
Product Engineer build MCP server cho internal tool
Pain: Team đang dùng MCP server nội bộ để Claude truy vấn feature flags. Vừa deploy lên Cloud Run, user report "tool calls đôi khi không trả về gì, phải reload session".
Giải pháp sau khi hiểu message types:
- Debug bằng cách log mọi message với
id→ phát hiện một sốCallToolResultbị mất khistateless_http=True+ nhiều instance sau load balancer. - Solution: switch sang
statefulvới session affinity, hoặc accept trade-off. - Kết quả: thời gian debug 3 ngày → 30 phút (nhờ đọc raw message thay vì mò qua SDK log).
Finance Analyst dùng MCP server để query warehouse
Pain: Tool query_warehouse chạy 90 giây cho query lớn, user tưởng Claude bị treo, hủy và retry liên tục, dẫn tới load spike.
Giải pháp:
- Thêm
notifications/progressvới step: "Parsing SQL... (10%)", "Executing... (50%)", "Fetching rows... (90%)". - User thấy progress bar, không hủy nữa.
- Kết quả: Retry rate 35% → 2%, không cần tăng concurrency limit.
DX Engineer duy trì MCP SDK của startup
Pain: User báo "prompt templates không refresh khi tôi edit server code". Team đang debug 2 tuần.
Giải pháp sau khi đọc spec:
- Phát hiện server không gửi
notifications/prompts/list_changedsau khi reload internal state. - Fix: 1 dòng
await ctx.session.send_notification(...)trong reload handler. - Kết quả: user không cần restart session. Support tickets liên quan giảm 80%.
Anti-patterns — Những sai lầm cần tránh
Nhầm notification với request
- Hiện tượng
- Bạn viết server gửi
notifications/progressvà rồiawaitđợi response. - Tại sao sai
- Notification không có response. Code bạn sẽ treo vô tận hoặc timeout.
- Cách đúng
- Trong SDK Python, dùng
ctx.report_progress(...)— nó biết đây là notification nên không block. Đừng tự tay build JSON rồi đợi.
Dùng id trùng nhau cho 2 request đồng thời
- Hiện tượng
- Bạn implement client thô, gửi 2
tools/callliên tiếp với id=1. - Tại sao sai
- Khi nhận 2 result cùng id=1, bạn không biết result nào của request nào. Gần như chắc chắn sai data cho user.
- Cách đúng
- Sinh id unique (UUID, counter atomic) cho mỗi request trong session. SDK tự lo việc này, nhưng nếu build raw thì phải cẩn thận.
Assume mọi transport đều hỗ trợ server → client request
- Hiện tượng
- Bạn viết server dùng
sampling, test trênstdioOK, deploy HTTP vớistateless_http=True, sampling fail im lặng. - Tại sao sai
stateless_httpcắt đứt đường server → client. Sampling request của server không có cách nào đến client.- Cách đúng
- Test với transport production sẽ dùng. Bài 10.5 sẽ list đầy đủ flag nào cắt đứt khả năng nào.
Coi message types là chi tiết kỹ thuật, không đọc spec
- Hiện tượng
- "Tôi dùng SDK rồi, cần gì đọc spec."
- Tại sao sai
- Khi SDK có bug hoặc version mới thêm message type, bạn sẽ bế tắc nếu không quen đọc spec.
- Cách đúng
- Đánh dấu trang schema.ts của spec. Mỗi khi có message type lạ, mở ra đọc 30 giây.
Mẹo nâng cao
Mẹo 1: Bật verbose log khi debug
Cách đơn giản nhất để xem raw JSON message là dùng MCP Inspector (mẹo 2) thay vì env var SDK — env var và log behavior thay đổi giữa các SDK version.
Nếu muốn log raw trong code của bạn, attach Python logging với level DEBUG trên module mcp:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("mcp").setLevel(logging.DEBUG)
Hoặc tự write middleware wrap session.send_request() / session.send_notification() để trace payload trước khi gửi. Verbose output giúp bạn thấy đúng những JSON object như ví dụ step 1-11 ở trên.
Mẹo 2: Dùng MCP Inspector để visualize
Anthropic có tool MCP Inspector — GUI visualize mọi message giữa client-server. Dùng nó khi bạn không muốn đọc log text thô:
npx @modelcontextprotocol/inspector uv run server.py
Nó sẽ mở browser, bạn click quanh và thấy từng message như một chat app.
Mẹo 3: Nhớ 3 message types "bất thường"
Nếu thời gian chỉ học 3 message types để nhớ lâu, chọn 3 cái sau (đây là 3 cái người mới hay miss):
notifications/initialized— bước 3 của handshake, thiếu sẽ không gọi tool được.sampling/createMessage— server request, không phải client.notifications/cancelled— có thể đi cả 2 chiều.
Áp dụng ngay
Bài tập 1: Đọc raw messages bằng MCP Inspector (10 phút)
Bước 1: Tạo một server MCP đơn giản với 1 tool add:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="demo")
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
mcp.run()
Bước 2: Chạy Inspector:
uv run mcp dev server.py
Bước 3: Trong Inspector UI, click "List Tools" rồi "Call Tool add" với a=3, b=4. Ghi lại:
- Số lượng message trao đổi (không đếm initialize): ___________
idcủatools/callrequest: ___________- Có notification nào xuất hiện không? ___________
Bài tập 2 (optional): Viết client thô bằng curl/bash
Chạy server với stdio và gửi JSON thẳng bằng terminal (sẽ học chi tiết ở bài 10.2). Cố gắng hoàn thành một tools/call mà không dùng SDK.
Tóm tắt bài học
- MCP = JSON-RPC 2.0 — mọi message là JSON object với format chuẩn, không ma thuật.
- Hai nhóm message: Request↔Result và Notification — khác nhau ở field
idvà việc sender có đợi response không. - MCP bidirectional — client gửi request cho server, server cũng gửi request cho client (sampling, roots). Đây là gốc rễ của transport complexity các bài sau.
- Spec là ground truth — SDK có thể sai, spec thì không. Bookmark
schema.tsđể tham khảo khi debug. - Handshake 3 bước bắt buộc —
initialize→InitializeResult→initialized notification. Bỏ sót bước 3 là session hỏng.
Tài liệu tham khảo
- MCP Specification Schema — file TypeScript định nghĩa mọi message type
- JSON-RPC 2.0 Spec — chuẩn gốc MCP dựa vào
- MCP Inspector — GUI tool để visualize message flow
- Python MCP SDK — repo SDK dùng trong walkthrough