Ở bài 10.1 bạn đã học về JSON message — nội dung trao đổi. Nhưng message cần một con đường để đi từ A sang B. Con đường đó gọi là transport.
- Giải thích được transport là gì và vì sao MCP tách riêng khái niệm này ra khỏi message format.
- Mô tả được cách STDIO transport hoạt động: subprocess, stdin, stdout.
- Tự chạy một MCP server bằng stdio không cần client — chỉ dùng terminal.
- Liệt kê được 4 tình huống giao tiếp mà mọi transport phải hỗ trợ (client↔server, request/response).
- Hiểu được vì sao stdio là baseline — các transport khác đều chỉ cố gắng đạt được năng lực của stdio.
Mở đầu: Transport là đường ống, message là nước
Transport có thể là bất kỳ kênh nào miễn là nó có thể chuyển byte qua lại:
- HTTP request/response
- WebSocket connection
- gRPC stream
- ... thậm chí viết JSON lên bưu thiếp và gửi bằng đường bưu điện (không khuyến khích cho production, nhưng đúng về mặt kỹ thuật)
MCP không ép bạn chọn transport nào. Hiện tại spec chuẩn hóa 2 cái: stdio (bài này) và streamable-http (bài 10.3 → 10.6). Bạn có thể tự viết transport riêng nếu muốn — miễn là 2 bên đều đồng ý dùng chung.
Bài này tập trung vào stdio — transport đơn giản nhất, sạch nhất, và là "case lý tưởng" để các transport khác so sánh.
Stdio là gì — Nói bằng ngôn ngữ Unix cổ điển
Từ năm 1970, Unix đã dạy chúng ta một ý tưởng thiên tài: mọi chương trình đều có 3 file mặc định — stdin, stdout, stderr. Dữ liệu vào qua stdin, kết quả ra qua stdout, lỗi ra stderr. Đó là lý do bạn viết được:
cat file.txt | grep "error" | wc -l
STDIO transport của MCP dùng đúng ý tưởng đó cho client-server MCP:
┌────────────────────────────────────────────────────────────┐
│ │
│ CLIENT PROCESS (vd: Claude Desktop) │
│ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ 1. spawn subprocess │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────┐ │ │
│ │ │ SERVER PROCESS│ │ │
│ │ │ (your code) │ │ │
│ │ └───────────────┘ │ │
│ │ ▲ │ │ │
│ │ │ │ │ │
│ │ write │ │ read │ │
│ │ │ ▼ │ │
│ │ stdin stdout │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
Ba điều cần nhớ:
- Client khởi động server như subprocess — ví dụ
uv run server.pyhoặcnpx mcp-server-xyz. - Client ghi vào
stdincủa server — đó là cách gửi request. - Server ghi ra
stdout— đó là cách trả response.
Mỗi message là 1 dòng JSON, kết thúc bằng \n. Đơn giản đến mức bạn có thể tự test bằng tay.
Stdio trong thực tế — Test server không cần client
Đây là sức mạnh của stdio: bạn không cần Claude Desktop, không cần Python client code. Chỉ cần terminal.
Bước 1: Chạy server
Giả sử bạn có server.py với 1 tool add:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="calculator")
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
mcp.run() # default = stdio transport
Chạy nó:
uv run server.py
Terminal sẽ im lặng — không print gì. Đó là vì server đang lắng nghe stdin, đợi bạn gửi JSON.
Bước 2: Gõ message initialize vào stdin
Paste JSON sau (1 dòng, enter để gửi):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"1.0"}}}
Server sẽ in ngay ra một dòng JSON response:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"calculator","version":"..."}}}
Bước 3: Hoàn tất handshake
Gửi notification initialized (không id vì là notification):
{"jsonrpc":"2.0","method":"notifications/initialized"}
Server không trả về gì (đúng như spec — notification không có response).
Bước 4: Gọi tool
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}
Response:
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"5"}]}}
Bạn vừa chạy MCP bằng tay, không cần Claude, không cần framework. Đây là cái đẹp của stdio — protocol lộ ra thật rõ, không bị che bởi SDK magic.
Handshake bắt buộc — 3 message, không thể thiếu
Mọi session stdio (và cả HTTP) đều bắt đầu bằng handshake 3 bước:
CLIENT SERVER
│ │
│───── initialize ───────▶│
│ │ (học về client, lưu capabilities)
│◀──── InitializeResult ──│
│ │
│── notifications/ │
│ initialized ────────▶│
│ │ (giờ mới có thể phục vụ tools/call)
│ │
│────── tools/call ──────▶│
│◀──── CallToolResult ────│
│ ... │
Quy tắc vàng:
- Client phải gửi
initializeđầu tiên, không được gửi gì khác trước đó. - Server phải trả
InitializeResultvới capabilities nó support (tools, prompts, resources, sampling, roots...). - Client phải gửi notification
initializedtrước khi gọi bất kỳ request nào khác.
Nếu bạn bỏ bước 3, server sẽ reject mọi request sau đó. Đây là pattern "tôi đã nhận xong capabilities, giờ mới nói chuyện được".
Bốn kịch bản giao tiếp — Và vì sao stdio dễ
Bất kỳ MCP transport nào cũng phải xử lý được 4 hướng message dưới đây:
┌───────────────────────────────────────────────────────────┐
│ │
│ (1) Client → Server REQUEST │
│ Ví dụ: tools/call, resources/read │
│ Stdio: client ghi stdin │
│ │
│ (2) Server → Client RESPONSE │
│ Ví dụ: CallToolResult, ReadResourceResult │
│ Stdio: server ghi stdout │
│ │
│ (3) Server → Client REQUEST │
│ Ví dụ: sampling/createMessage, roots/list │
│ Stdio: server ghi stdout (bất cứ lúc nào) │
│ │
│ (4) Client → Server RESPONSE │
│ Ví dụ: CreateMessageResult, ListRootsResult │
│ Stdio: client ghi stdin │
│ │
└───────────────────────────────────────────────────────────┘
Stdio xử lý tất cả 4 case một cách thanh lịch — vì stdin và stdout là 2 kênh full-duplex, bất kỳ bên nào cũng có thể gửi bất cứ lúc nào.
Nhưng hãy giữ 4 case này trong đầu, vì ở bài 10.3 bạn sẽ thấy HTTP gặp khó khăn với case (3) và (4): server không có URL của client, làm sao gửi request tới nó?
So sánh: Stdio vs cái gì sau đó?
| Tiêu chí | Stdio | HTTP thô | Streamable HTTP |
|---|---|---|---|
| Full bidirectional? | Có | Không | Có (qua SSE) |
| Client & Server cùng máy? | Bắt buộc | Không cần | Không cần |
| Dễ debug? | Gõ bằng tay được | Cần curl/Postman | SSE hơi phức tạp |
| Scale được? | 1 subprocess / session | Stateless scale tốt | Cần session affinity |
| Dùng khi? | Dev & local user app | - | Production remote |
Nhìn bảng, bạn thấy stdio thắng tuyệt đối trong dev — dễ debug, full bidirectional, gõ tay được. Nhược điểm duy nhất: client và server phải cùng máy. Không có cách nào Claude Desktop trên máy bạn giao tiếp stdio với server trên Cloud Run được.
Đây là lý do chúng ta cần HTTP transport cho production remote — với cái giá phải trả mà bạn sẽ thấy ở bài sau.
Stdio trong hệ sinh thái Anthropic
Khi bạn cài MCP server cho Claude Desktop, file config claude_desktop_config.json trông như:
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": ["run", "/path/to/server.py"]
}
}
}
chỉ có command và args. Không có URL, không có port. Claude Desktop sẽ spawn** subprocess uv run /path/to/server.py và dùng stdio của subprocess đó. Y hệt như khi bạn tự chạy uv run server.py trong terminal — nhưng Claude Desktop đóng vai trò "terminal".
Tương tự với Cursor, Cline, mcp-cli — tất cả đều dùng stdio cho local development.
Ví dụ theo ngành
Solo developer build personal automation
Pain: Muốn Claude access Obsidian vault, nhưng không muốn deploy server lên cloud.
Giải pháp:
- Viết MCP server Python ~100 dòng, expose tool
search_notes,read_note,create_note. - Config Claude Desktop trỏ tới
uv run obsidian-mcp.py. - Run local, không cần auth, không cần HTTPS.
- Kết quả: 30 phút từ ý tưởng đến dùng được. Zero infrastructure cost.
DevRel engineer chạy demo workshop
Pain: Workshop có 50 developer, muốn ai cũng thử MCP server mẫu mà không cần setup server riêng.
Giải pháp:
- Publish server dưới dạng
npx mcp-demo-xyzlên npm. - Attendee chỉ copy-paste config vào Claude Desktop, Claude tự
npxinstall và chạy stdio. - Kết quả: Thời gian setup trung bình 3 phút/người, không ai bị stuck vì server down.
Enterprise data engineer prototype internal MCP
Pain: Muốn POC MCP để Claude query internal data warehouse, nhưng security team chưa duyệt network deployment.
Giải pháp:
- Viết server local chạy stdio.
- Server chạy trên máy cá nhân engineer với VPN sẵn có.
- Không có network endpoint mới — không cần security review.
- Kết quả: POC chạy trong 1 tuần, demo cho leadership approve full deployment.
Anti-patterns — Sai lầm thường gặp với stdio
In log text ra stdout trong server code
- Hiện tượng
- Bạn
print("DEBUG: calling tool")trong server code. Client báo "Invalid JSON message". - Tại sao sai
- Stdout là kênh truyền JSON message. Bất kỳ text lạ nào ghi vào đó sẽ phá parser của client.
Cách đúng:
- Log ra
stderrthay vìstdout:print("DEBUG: ...", file=sys.stderr). - Tốt hơn: dùng
context.info(...)của MCP (notificationmessage) — sẽ học ở bài 10.6.
Buffer stdout
- Hiện tượng
- Gửi request, server "im lặng", nhưng thực ra đã xử lý xong. Response chỉ hiện khi server exit.
- Tại sao sai
- Python default buffer stdout khi không phải terminal. Client đợi response mãi không đến.
Cách đúng:
- Dùng
sys.stdout.flush()sau mỗi message (SDK tự làm). - Hoặc chạy server với
python -u server.py(unbuffered).
Assume server chạy forever
- Hiện tượng
- Server crash giữa chừng, client tưởng server chưa trả lời, đợi mãi.
- Tại sao sai
- Khi subprocess exit, stdio của nó đóng. Client cần detect EOF hoặc process exit signal.
- Cách đúng
- SDK tự handle. Nếu bạn build client raw, watch cả
stdin EOFlẫn process signal. Khi detect, re-spawn server.
Hardcode path Unix trong server config
Hiện tượng: args: ["run", "/Users/alice/server.py"] — cho Windows user không chạy được.
Cách đúng:
- Publish server lên package manager (pip, npm) để tránh path hardcode.
- Hoặc dùng env var:
${HOME}/server.py.
Mẹo nâng cao
Mẹo 1: Lưu JSON messages mẫu thành snippet
Lưu file stdin-samples.txt chứa các JSON message handshake + tool call mẫu. Khi cần test server mới, copy-paste vào terminal là xong. Tiết kiệm gõ đi gõ lại.
Mẹo 2: Pipe từ file vào server
Không cần gõ bằng tay. Viết file test.jsonl chứa 4 message (1 dòng 1 JSON):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}
Chạy:
cat test.jsonl | uv run server.py
Server xử lý từng dòng, in response ra stdout. Dùng làm smoke test cho CI.
Mẹo 3: Tách riêng stdin và stdout log
Khi debug, chạy:
uv run server.py 2> server-errors.log < test.jsonl > responses.jsonl
2> là stderr (log), < là stdin input, > là stdout output. Sau khi chạy, bạn có 2 file sạch để so sánh.
Mẹo 4: Test stdio speed — biết baseline throughput
Stdio là transport nhanh nhất (trong process tree cùng máy). Benchmark đơn giản:
time (cat 1000-messages.jsonl | uv run server.py > /dev/null)
Nếu bạn thấy HTTP deploy chậm hơn stdio rõ rệt, so sánh với baseline này để biết đâu là overhead transport vs đâu là logic tool.
Áp dụng ngay
Bài tập 1: Handshake bằng tay (15 phút)
Bước 1: Tạo calc.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="calc")
@mcp.tool()
def multiply(a: int, b: int) -> int:
return a * b
if __name__ == "__main__":
mcp.run()
- Bước 2
- Chạy
uv run calc.py. - Bước 3
- Gửi 4 message theo thứ tự:
initializerequest (id=1)notifications/initializedtools/list(id=2)tools/callchomultiply(a=6, b=7)(id=3)
Bước 4: Ghi lại:
- Protocol version server trả về: ___________
- Số tool trong
tools/listresult: ___________ - Giá trị text trong
CallToolResult: ___________
Bài tập 2 (optional): Log error handling
Sửa multiply để raise exception khi b=0 (giả sử giả vờ đây là phép chia). Gửi tools/call với b=0 qua stdin. Observe: server trả về gì? error field trong JSON trông thế nào? Đây là training data cho bạn debug production sau này.
Tóm tắt bài học
- Transport tách biệt với message — cùng JSON message, có thể đi qua nhiều transport khác nhau.
- Stdio = subprocess + stdin/stdout — client spawn server, ghi vào stdin, đọc từ stdout. Unix classic.
- Stdio là full-duplex — cả 4 loại message (2 chiều request & response) đều đi được dễ dàng.
- Handshake 3 bước bắt buộc —
initializerequest → result →initializednotification. Không có bước 3, server không phục vụ. - Stdio là baseline — các transport khác đều cố gắng đạt được năng lực của stdio. Nhớ "4 kịch bản giao tiếp" để so sánh khi đọc bài về HTTP.
- Stdio cho local, không cho remote — client & server phải cùng máy. Muốn remote → cần HTTP transport.
Tài liệu tham khảo
- MCP Transport Specification — chi tiết cả stdio & http
- Python MCP SDK — stdio example — mã mẫu chính thức
- Claude Desktop config guide — cách thêm stdio server