Lộ trình học›MCP›MCP: Advanced Topics Mở bản đồ

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

Bài viếtTrung cấp25 phút đọc

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. Đồng nghiệp hỏi:

Sau bài này, bạn sẽ
  1. Giải thích được vì sao cần transport HTTP bên cạnh stdio (remote hosting, scale, multi-tenant).
  2. Nhận diện được hai limitation cốt lõi của HTTP thô đối với bidirectional protocol như MCP.
  3. Liệt kê được chính xác các message type bị ảnh hưởng khi MCP chạy qua HTTP.
  4. 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).
  5. Biết khi nào nên chọn HTTP transport và khi nào nên tránh.

Mở đầu: Ai đó trên Internet muốn dùng server của bạn

Server của bạn hay quá, em cài được không? Em đang dùng Claude Desktop.

Bạn nhớ ra cách stdio hoạt động: client spawn subprocess server trên máy của client. Tức là để đồng nghiệp em dùng được, bạn phải:

  • Đưa source code Python.
  • Bảo em cài Python, uv, dependencies.
  • Bảo em copy config vào claude_desktop_config.json.
  • Đợi em báo bug vì máy em là Windows còn máy bạn Mac.

Rất không scale. Đấy là chưa kể user bên ngoài công ty — bạn không thể bảo tất cả mọi người trên Internet "cài Python, clone repo, uv run".

Giải pháp hiển nhiên: host server trên một cái URL. User trỏ Claude Desktop vào URL, xong. Đây chính là động lực ra đời của Streamable HTTP transport.

Nhưng… HTTP không thân thiện với MCP như stdio. Bài này giải thích vì sao, và giới thiệu 2 flag sẽ ám ảnh bạn suốt phần còn lại của khóa học.

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):

(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)

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:

┌────────────────────────────────────────────────────────────┐
│                                                            │
│   ✅ 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.                                 │
│                                                            │
└────────────────────────────────────────────────────────────┘

Đâ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ô):

  • sampling/createMessage — server xin client gọi model
  • roots/list — server xin client danh sách thư mục

Notifications (đi 1 chiều, không theo pattern request-response):

  • 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

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.

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):

┌──────────────────────────────────────────────────────────┐
│                                                          │
│   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!         │
│                                                          │
└──────────────────────────────────────────────────────────┘

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.

Đ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:

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

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:

  • 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).

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.

** đâ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.

So sánh: 3 cách chạy MCP qua HTTP

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=FalseKhôngDễPublic API, horizontal scale
stateless_http=True, json_response=TrueKhôngDễ nhấtIntegrate với hệ thống không support SSE
stateless_http=False, json_response=TrueHạn chếCần sticky sessionHiếm khi dùng

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?

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:

mcp.run(transport="streamable-http")

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", stateless_http=True)

Scale lên 10 instance trơn tru, không còn routing issue. Nhưng:

  • Progress bar biến mất hoàn toàn.
  • Sampling tool fail.
  • Log không stream.

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:

  • 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.

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.

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:

  • 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.

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:

  • 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.

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:

  • 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:
  1. Tôi có cần scale hơn 1 instance không? (Nếu không → không cần bật)
  2. 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)

Dev trên stdio, chỉ test HTTP khi deploy

Hiện tượng: Code passes local tests, fail trong production.

Cách đúng:

  • 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.

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:

  • 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.

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:

  • 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:

curl -N -H "Accept: text/event-stream" http://localhost:8000/mcp

-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:

npx @modelcontextprotocol/inspector http http://localhost:8000/mcp

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:

import sys
transport = "stdio" if "--stdio" in sys.argv else "streamable-http"
mcp.run(transport=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.

Á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:

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ước 2: Chạy:

uv run calc.py

Server giờ listen HTTP port 8000.

Bước 3: Test bằng curl:

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"}}}'

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: Dùng curl -N mở SSE GET (cần session ID từ bước 4 ở trên).
  • Terminal 2: Gửi tools/call POST.

Terminal 1 sẽ nhận các message. Quan sát format của SSE (các event với data: prefix).

Tóm tắt bài học

Những điều cốt lõi
  • 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

Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.

Khóa học cùng lộ trình