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.
- 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ọ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:
Server xử lý xong, trả về:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 2, "b": 3 }
}
}Ví dụ minh họa: Call Tool Request
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.
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{ "type": "text", "text": "5" }
]
}
}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ớ 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":
Để ý 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 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.
| 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 |
┌─────────────────────────────────────────────────────────┐ │ │ │ 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 │ │ │ └─────────────────────────────────────────────────────────┘
So sánh nhanh: Request vs Notification
Dòng cuối cùng sẽ quay lại ám ảnh bạn ở bài 10.3, 10.5, 10.6.
| 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 |
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:
Mini-tour spec file
File trung tâm: schema.ts. Ví dụ cách spec định nghĩa CallToolRequest:
Đọ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ì.
- 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.
export interface CallToolRequest extends Request {
method: "tools/call";
params: {
name: string;
arguments?: { [key: string]: unknown };
};
}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)
Server gửi đi (Server messages)
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.
- 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ảng notifications/roots/list_changed
- Response cho các server request: CreateMessageResult (đáp lại sampling), ListRootsResult (đáp lại roots request)
- 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
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):
Đọc kỹ. Bạn thấy:
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).
- Step 1-3: handshake 3 bước (initialize → InitializeResult → initialized notification). Đâ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/createMessage request tới client. Client tự gọi Claude (dùng API key của client), rồi trả CreateMessageResult về.
- Step 11: sau khi có kết quả sampling, server hoàn thành research và trả CallToolResult cho client.
[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..."}] }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:
💰 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:
📣 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:
- Debug bằng cách log mọi message với id → phát hiện một số CallToolResult bị mất khi stateless_http=True + nhiều instance sau load balancer.
- Solution: switch sang stateful vớ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).
- Thêm notifications/progress vớ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.
- Phát hiện server không gửi notifications/prompts/list_changed sau 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/progress và rồi await đợ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/call liê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ên stdio OK, deploy HTTP với stateless_http=True, sampling fail im lặng.
Tại sao sai: stateless_http cắ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:
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ô:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("mcp").setLevel(logging.DEBUG)Mẹo 2: Dùng MCP Inspector để visualize
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.
npx @modelcontextprotocol/inspector uv run server.pyÁ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:
Bước 2: Chạy Inspector:
# server.py
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ài tập 1: Đọc raw messages bằng MCP Inspector (10 phút)
Bước 3: Trong Inspector UI, click "List Tools" rồi "Call Tool add" với a=3, b=4. Ghi lại:
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.
- Số lượng message trao đổi (không đếm initialize): ___________
- id của tools/call request: ___________
- Có notification nào xuất hiện không? ___________
uv run mcp dev server.pyTó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 id và 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.
- 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