Transport StreamableHTTP — Khi MCP gặp thế giới thực

Message & Transport LayerTrung cấp25 phút

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.

Bạn sẽ học được
  • 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).
FlagDefaultHiệu ứng khi = True
stateless_httpFalseKhông track session, mất khả năng server → client
json_responseFalsePOST 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 sessionSingle 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ấtIntegrate với hệ thống không support SSE
stateless_http=False, json_response=True⚠️ Hạn chế⚠️ Cần sticky sessionHiế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/mcp

Mẹ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/mcp

Mẹ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.

Tài liệu tham khảo
  • 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
Nội dung này có hữu ích không?