Tưởng tượng bạn vừa build xong một MCP server thú vị — chẳng hạn server expose đồ thị kiến thức nội bộ của công ty. Nó chạy ngon trên máy bạn với stdio.
- Giải thích được vì sao cần transport HTTP bên cạnh stdio (remote hosting, scale, multi-tenant).
- Nhận diện được hai limitation cốt lõi của HTTP thô đối với bidirectional protocol như MCP.
- Liệt kê được chính xác các message type bị ảnh hưởng khi MCP chạy qua HTTP.
- Hiểu được tồn tại 2 flag stateless_http và json_response và cảm giác chung chúng làm gì (chi tiết ở bài 10.4-10.6).
- Biết khi nào nên chọn HTTP transport và khi nào nên tránh.
Vì sao không chỉ "dùng HTTP" rồi xong?
Hãy nhìn lại 4 kịch bản giao tiếp mà mọi transport MCP phải xử lý (nhắc lại từ bài 10.2):
Trên stdio, cả 4 case đều dễ vì stdin & stdout là 2 ống full-duplex.
Trên HTTP thô, tình huống khác hẳn:
Đây là vấn đề asymmetry cơ bản của HTTP: được thiết kế cho client initiate, server respond. Còn MCP yêu cầu cả 2 bên đều có thể initiate.
Những feature bị ảnh hưởng
Vậy cụ thể HTTP thô không hỗ trợ được message nào của MCP?
Server-initiated requests (không chuyển được qua HTTP thô):
Notifications (đi 1 chiều, không theo pattern request-response):
Nhìn vào list, bạn thấy ngay: các tính năng hấp dẫn nhất của MCP đều dùng đến server → client. Nếu HTTP thô mất hẳn những tính năng này, MCP trên HTTP sẽ trở thành "REST API wrapper" tầm thường.
- sampling/createMessage — server xin client gọi model
- roots/list — server xin client danh sách thư mục
- notifications/progress — server báo tiến độ
- notifications/message — server gửi log
- notifications/initialized — client xác nhận ready (ít ảnh hưởng hơn vì gửi trong initialization flow)
- notifications/cancelled — huỷ request
- notifications/tools/list_changed, notifications/resources/updated, ... — server broadcast thay đổi
┌────────────────────────────────────────────────────────────┐ │ │ │ ✅ Case (1): Client → Server REQUEST │ │ Dễ. Client có URL server, gửi POST là xong. │ │ │ │ ✅ Case (2): Server → Client RESPONSE │ │ Dễ. Đi kèm với response của case (1). │ │ │ │ ❌ Case (3): Server → Client REQUEST │ │ Khó! Server không biết URL client. │ │ Client đâu có HTTP endpoint public... │ │ │ │ ❌ Case (4): Client → Server RESPONSE (mà không phải │ │ response của case (1)) │ │ Khó! Response phải đi "ngoài cặp request/response" │ │ bình thường của HTTP. │ │ │ └────────────────────────────────────────────────────────────┘
(1) Client → Server REQUEST (client gọi tool)
(2) Server → Client RESPONSE (server trả kết quả)
(3) Server → Client REQUEST (server xin sampling, roots)
(4) Client → Server RESPONSE (client trả sampling/roots result)StreamableHTTP — Giải pháp với một chút "phép thuật"
Spec MCP không từ bỏ HTTP. Thay vào đó, chúng ta có Streamable HTTP transport — cách giải quyết thông minh dùng Server-Sent Events (SSE).
Ý tưởng cốt lõi (chi tiết ở bài 10.4):
SSE (Server-Sent Events) là một tính năng HTTP chuẩn: client mở GET request, server giữ response "mở" và stream message qua dần. Trình duyệt hỗ trợ sẵn từ HTML5 — đây không phải công nghệ mới.
Streamable HTTP dùng SSE cho hướng server → client, và HTTP POST thường cho hướng client → server. Hai kết hợp lại, bạn có bidirectional communication hoạt động trên infrastructure HTTP chuẩn — không cần WebSocket, không cần gRPC, không cần long-polling.
┌──────────────────────────────────────────────────────────┐ │ │ │ 1. Client GET /mcp │ │ ──────────────────▶ Server │ │ (mở 1 SSE connection, long-lived) │ │ ◀───────────────── │ │ (server giữ kết nối mở, có thể stream message ra │ │ bất cứ lúc nào qua kết nối này) │ │ │ │ 2. Client POST /mcp (để gọi tool, list, ...) │ │ ──────────────────▶ Server │ │ (request-response bình thường) │ │ │ │ Server có 1 "kênh ngầm" (SSE) để gửi message │ │ bất đồng bộ → giải quyết được server → client! │ │ │ └──────────────────────────────────────────────────────────┘
Điều kiện đi kèm — 2 flag thay đổi mọi thứ
Tại sao bài này không dạy luôn hết mà chia thành 3 bài? Vì SDK expose 2 flag cấu hình có thể phá vỡ SSE workaround này:
Cả 2 flag đều False by default — nên nếu bạn chỉ chạy SDK mặc định, mọi thứ hoạt động tốt. Vấn đề là production thường ép bạn bật ít nhất 1 trong 2:
Và khi bạn bật chúng, một số message types không còn đi qua được nữa. Bạn sẽ thấy progress bar biến mất, log im lặng, sampling timeout.
Cảnh báo: đây chính là nguồn gốc của câu hỏi kinh điển mà developer MCP nào cũng hỏi ít nhất 1 lần: "Vì sao code này chạy ngon trên stdio, vừa deploy HTTP là hỏng?"
Bài 10.4 và 10.6 sẽ đào sâu 2 flag này. Bài này chỉ cần bạn nhớ 2 cái tên và biết sự tồn tại của chúng.
- stateless_http=True thường được bật khi bạn deploy với nhiều replica sau load balancer. Nếu không bật, một request GET SSE có thể đến instance A còn POST tool call đến instance B — hai instance phải phối hợp qua Redis/database, rất phức tạp.
- json_response=True thường được bật khi bạn cần tích hợp với hệ thống không hiểu SSE (proxy cũ, API gateway strict).
| Flag | Default | Hiệu ứng khi = True |
|---|---|---|
| stateless_http | False | Không track session, mất khả năng server → client |
| json_response | False | POST response trả JSON thuần, không streaming qua SSE |
So sánh: 3 cách chạy MCP qua HTTP
Dòng đầu tiên là full-feature nhưng khó scale. Dòng 2 là scale tốt nhưng mất năng lực. Dòng 3 là lựa chọn an toàn khi bạn không cần advanced features.
Chọn đúng flag dựa trên ma trận: bạn cần tính năng advanced (sampling/notifications/roots) không, và bạn cần scale bao nhiêu replica?
| Chế độ | Hỗ trợ server→client? | Scale horizontal? | Ideal cho |
|---|---|---|---|
| stateless_http=False, json_response=False (default) | ✅ Đầy đủ | ⚠️ Cần sticky session | Single instance prod, small scale |
| stateless_http=True, json_response=False | ❌ Không | ✅ Dễ | Public API, horizontal scale |
| stateless_http=True, json_response=True | ❌ Không | ✅ Dễ nhất | Integrate với hệ thống không support SSE |
| stateless_http=False, json_response=True | ⚠️ Hạn chế | ⚠️ Cần sticky session | Hiếm khi dùng |
Ví dụ thực chiến: Hành trình deploy một MCP server
Hãy theo dõi một tình huống điển hình — startup deploy server MCP cho user công khai.
Tuần 1: Local dev với stdio
Developer viết server Python với đủ tính năng: tool research, progress notifications, sampling để tóm tắt. Test với Claude Desktop local, stdio, mọi thứ đẹp như mơ.
Tuần 2: Deploy lên Cloud Run (single instance)
Developer đổi sang streamable-http, để flag mặc định:
Cloud Run 1 instance. Mọi thứ vẫn chạy. Progress bar hiện, sampling work, log stream xuống. Happy.
Tuần 3: 1,000 user đồng thời, latency tăng
Single instance không chịu nổi. Cloud Run auto-scale lên 10 instance. Bắt đầu report bug: "Progress bar đôi lúc không hiện", "tool gọi mãi không trả về".
Nguyên nhân: load balancer chia request ngẫu nhiên. Client mở GET SSE vào instance A, nhưng POST tool call đến instance B. Instance B không có reference tới SSE connection của client → không gửi progress được.
Tuần 4: Bật stateless_http=True
Developer đọc docs, bật:
mcp.run(transport="streamable-http")Tuần 4: Bật stateless_http=True
Scale lên 10 instance trơn tru, không còn routing issue. Nhưng:
User complain. Trade-off hiện rõ: scale vs. features.
Tuần 5: Kiến trúc lại — 2 tier
Giải pháp:
Cuối cùng, có 2 endpoint cho 2 loại use case. Architecture phức tạp hơn nhưng đúng với đặc tính của từng tool.
Bài học: không có flag "đúng". Có flag phù hợp với kiến trúc và đánh đổi có ý thức.
- Progress bar biến mất hoàn toàn.
- Sampling tool fail.
- Log không stream.
- Tier 1 (stateless, scale cao): các tool đơn giản không cần notifications/sampling — bật stateless_http=True.
- Tier 2 (stateful, sticky session): tool cần progress, sampling, log — deploy với stateless_http=False sau load balancer có session affinity.
mcp.run(transport="streamable-http", stateless_http=True)Ví dụ theo ngành
🏢 Enterprise SaaS expose MCP cho khách hàng
Pain: Công ty có SaaS (vd Asana). Muốn expose MCP server cho khách hàng connect Claude với dữ liệu.
Giải pháp:
🎧 B2C startup với 10,000+ concurrent users
Pain: Consumer app muốn Claude có access vào backend của họ. 10k user online cùng lúc, chi phí sampling mỗi user × token × giờ.
Giải pháp:
🔬 Academic group chia sẻ dataset qua MCP
Pain: Lab nghiên cứu muốn share dataset phân tích qua MCP, có cả progress cho query nặng (dataset 100GB).
Giải pháp:
- Deploy với streamable-http, sau OAuth gateway của công ty.
- Dùng session affinity (sticky cookies) thay vì stateless_http.
- Giữ được notifications và sampling cho feature "tóm tắt project".
- Kết quả: Khách hàng enterprise connect trực tiếp từ Claude Desktop, không cần cài gì, với full feature MCP.
- Bật stateless_http=True vì khả năng scale là critical.
- Chấp nhận không có sampling — thay vào đó user tự gọi LLM phía client (mỗi user có Claude subscription).
- Chấp nhận không có progress notifications — tool được thiết kế để trả nhanh (< 2s).
- Kết quả: 10k user concurrent chỉ với 3-5 instance, cost predictable.
- Single instance trên GPU server lab — không scale.
- Giữ default flag (cả 2 = False) → full features.
- Progress bar giúp user biết query đang làm gì trong 2-3 phút.
- Kết quả: 50 lab member share 1 server, mọi user có visibility tốt vào tool execution.
Anti-patterns
❌ Copy-paste stateless_http=True vì thấy trong tutorial cloud
Hiện tượng: Tutorial "Deploy MCP to Cloud Run" bảo bật cờ này, bạn bật theo, sau đó thấy sampling không work.
Cách đúng: Hỏi mình 2 câu trước khi bật:
❌ Dev trên stdio, chỉ test HTTP khi deploy
Hiện tượng: Code passes local tests, fail trong production.
Cách đúng:
❌ Assume SSE chạy mọi nơi
Hiện tượng: Deploy xong mới phát hiện API Gateway của công ty buffer SSE response, ăn hết tính năng streaming.
Cách đúng:
❌ Không có fallback khi SSE drop
Hiện tượng: Network blip, SSE disconnect, server không biết client còn có hay không. Client không reconnect.
Cách đúng:
- Tôi có cần scale hơn 1 instance không? (Nếu không → không cần bật)
- Tôi có OK mất progress/log/sampling không? (Nếu không → tìm cách khác để scale, vd session affinity)
- CI chạy test với streamable-http transport.
- Dev env có ít nhất 1 instance test HTTP (ngoài stdio).
- Biết từ trước: feature nào phụ thuộc server→client → phải test chế độ HTTP tương ứng.
- Test end-to-end từ client thật (Claude Desktop) qua proxy/gateway trước khi announce.
- Một số proxy (Cloudflare, older nginx config) buffer response mặc định — cần config rõ để stream pass-through.
- Client SDK nên tự reconnect (SDK chính thức có, kiểm tra version).
- Server nên có timeout reasonable cho idle session.
- Thiết kế tool idempotent → user retry không double-side-effect.
Mẹo nâng cao
Mẹo 1: Test SSE bằng curl
Streamable HTTP GET endpoint là SSE. Bạn có thể test connection nó bằng:
-N là no-buffering. Bạn sẽ thấy event stream ra dần, không bị block.
Mẹo 2: MCP Inspector hỗ trợ HTTP transport
Trong khi dev, dùng:
curl -N -H "Accept: text/event-stream" http://localhost:8000/mcpMẹo 2: MCP Inspector hỗ trợ HTTP transport
Visualize request, SSE message, tất cả. Tốt hơn đọc log raw.
Mẹo 3: Có thể chạy cùng lúc 2 transport
SDK Python cho phép server expose cả stdio và HTTP:
npx @modelcontextprotocol/inspector http http://localhost:8000/mcpMẹo 3: Có thể chạy cùng lúc 2 transport
User nào cài local dùng --stdio, user remote dùng HTTP. Cùng 1 codebase.
Mẹo 4: Hiểu tại sao HTTP2 vs HTTP/1.1 matter
SSE trên HTTP/1.1 dùng 1 TCP connection per stream. HTTP/2 mux nhiều stream trên 1 connection → ít tốn socket hơn khi scale. Chọn web server (uvicorn, hypercorn) hỗ trợ HTTP/2 khi traffic cao.
import sys
transport = "stdio" if "--stdio" in sys.argv else "streamable-http"
mcp.run(transport=transport)Áp dụng ngay
Bài tập 1: Deploy server test với streamable-http (20 phút)
Bước 1: Sửa calc.py ở bài 10.2:
Bước 2: Chạy:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="calc")
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)Bài tập 1: Deploy server test với streamable-http (20 phút)
Server giờ listen HTTP port 8000.
Bước 3: Test bằng curl:
uv run calc.pyÁp dụng ngay (tiếp)
Bước 4: Quan sát response. Có header mcp-session-id không? Ghi xuống: ___________
Bài tập 2 (optional): Quan sát SSE
Mở 2 terminal:
Terminal 1 sẽ nhận các message. Quan sát format của SSE (các event với data: prefix).
- Terminal 1: Dùng curl -N mở SSE GET (cần session ID từ bước 4 ở trên).
- Terminal 2: Gửi tools/call POST.
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'Tóm tắt bài học
🎯 HTTP thô không đủ cho MCP — MCP bidirectional, HTTP thì không. Case (3) & (4) khó.
🎯 StreamableHTTP = HTTP POST + SSE GET — POST cho client→server, SSE cho server→client. Hybrid solution.
🎯 Hai flag quyết định tất cả — stateless_http và json_response. Default False, nhưng production thường ép bật.
🎯 Mỗi flag là một trade-off — bật để scale/đơn giản hoá, giá là mất feature. Không có free lunch.
🎯 Phải test với transport production — stdio dev rồi deploy HTTP mà không test = fast track đến bug.
🎯 Đừng sợ SSE — nó là HTTP chuẩn, browser/proxy hiện đại đều support. Chỉ cẩn thận với old proxy buffering.
- MCP Transport Specification — HTTP — spec chính thức
- MDN: Server-Sent Events — nền tảng SSE
- Python SDK streamable-http example — code mẫu
- Transcript: "Building the Future of Agents with Claude" (2025-10-02) — thảo luận về remote MCP deployments