- Sampling cho phép MCP Server yêu cầu Client gọi mô hình ngôn ngữ (Claude) thay mặt mình, thay vì Server tự tích hợp và tự trả chi phí token — Client giữ credential và trả phí, lý tưởng cho public MCP server.
- Logging và Progress notifications gửi log và tiến độ real-time từ Server về Client trong lúc tool chạy — thuần cải thiện trải nghiệm, hoàn toàn tùy chọn.
- Roots cấp cho MCP Server quyền truy cập một số file/thư mục local và ngữ cảnh để Claude tự tìm file (list_roots + read_dir); nhưng SDK không tự thực thi ranh giới — dev phải tự kiểm bằng hàm như is_path_allowed().
- MCP giao tiếp bằng JSON-RPC 2.0 với hai nhóm message: Request–Result (đi theo cặp, ghép bằng id, có phản hồi) và Notification (một chiều, không cần phản hồi); là giao thức hai chiều nên cả client và server đều gửi được — điều quan trọng khi chọn transport như streamable HTTP.
- Cập nhật 07/2026: MCP spec bản chính thức 2026-07-28 (release ngày 28/07/2026) đã deprecate cả Sampling, Logging và Roots — dạng annotation-only, vẫn chạy tối thiểu 12 tháng nữa; code mới nên dùng LLM API trực tiếp, stderr/OpenTelemetry và tool parameters/resource URIs thay thế.
TL;DR — Sampling cho phép MCP Server mượn mô hình ngôn ngữ (Claude) qua Client thay vì tự gọi trực tiếp. Server soạn prompt rồi nhờ Client gọi Claude; Client giữ credential và trả tiền token. Nhờ vậy Server không cần API key, không tích hợp mô hình, không ôm chi phí — lý tưởng cho public MCP server (mỗi user tự trả phí của mình).
⚠️ Đọc trước: cả 3 tính năng của bài này đã bị deprecate (07/2026)
MCP spec bản chính thức
2026-07-28(release ngày 28/07/2026) đánh dấu deprecated cảSampling,LoggingvàRoots— tức đúng ba tính năng bài này dạy.Đừng hiểu nhầm là "hỏng ngay". Đây là annotation-only deprecation: method, type và capability flag vẫn chạy bình thường ở bản này và ở mọi bản spec công bố trong vòng 12 tháng tới. Muốn xoá thật phải qua một SEP riêng theo feature lifecycle policy, và phải cách lần deprecate tối thiểu 12 tháng. Nên code đang chạy không vỡ, và đề thi vẫn hỏi theo bản khoá dạy — cứ học nguyên bài.
Cái đổi là lời khuyên cho code mới:
Tính năng Thay bằng Roots Tool parameters · resource URIs · server config Sampling Gọi thẳng LLM provider API Logging stderr (với stdio transport) · OpenTelemetry cho structured observability Đọc bài này để hiểu vì sao MCP từng thiết kế như vậy và để đọc được code cũ — nhưng khi viết server mới, chọn cột bên phải.
Toàn cảnh spec 2026-07-28 — không chỉ ba tính năng này
Ba dòng deprecate ở trên là phần đụng trực tiếp vào bài. Nhưng bản 2026-07-28 là một đợt viết lại tầng giao thức, nên đây là bức tranh đủ để không bị hụt khi đọc spec:
| Nhóm | Nội dung |
|---|---|
| Gỡ hẳn (removed) | Cặp initialize / initialized + header Mcp-Session-Id (SEP-2575, SEP-2567) → protocol core thành stateless. |
| Deprecated | Roots · Sampling · Logging (SEP-2577) · legacy HTTP+SSE transport · DCR (Dynamic Client Registration) nhường chỗ cho CIMD (Client ID Metadata Documents). Cửa sổ tối thiểu 12 tháng — sớm nhất chỉ có thể xoá ở bản release từ 28/07/2027. |
| ⭐ Thay cho request server→client | MRTR — Multi Round-Trip Requests (SEP-2322): thay vì server giữ stream mở để tự khởi xướng request, server trả về resultType: "input_required" kèm những thứ nó cần; client gọi lại với câu trả lời trong inputResponses. Đây chính là thứ thay thế mô hình hai SSE connection mà bài này dạy ở dưới. |
| Routing & cache | Header-based routing (SEP-2243): mỗi request mang header Mcp-Method + Mcp-Name → gateway / rate limiter / WAF định tuyến và đo đếm bằng header, khỏi parse JSON body. Cacheable list results (SEP-2549): tools/list, prompts/list, resources/list, resources/read mang thêm ttlMs và cacheScope. |
| Extensions | Khung extensions chính thức: Tasks (SEP-2663 — chuyển sang io.modelcontextprotocol/tasks, poll bằng tasks/get, thêm tasks/update, notification dời sang stream subscriptions/listen) · MCP Apps · Enterprise Managed Authorization (EMA). |
| Siết authorization | Bắt buộc RFC 9207 issuer validation (SEP-2468) · khai application_type khi DCR (SEP-837) · credential buộc vào đúng authorization server đã phát (SEP-2352). |
Bằng chứng đã tới hàng sản xuất: changelog Claude Code v2.1.233 có dòng sửa lỗi "MCP v2 connections endlessly reopening the
subscriptions/listenstream" — tức client thật đã nói giao thức mới, không còn là chuyện trên giấy.
Sampling là gì
Sampling là cơ chế cho phép một MCP Server truy cập một mô hình ngôn ngữ như Claude thông qua MCP Client đang kết nối với nó. Thay vì Server tự gọi Claude, nó nhờ Client thực hiện cuộc gọi đó thay mặt mình. Cách làm này dịch chuyển trách nhiệm và chi phí sinh văn bản từ Server sang Client.
Điểm mấu chốt cần nhớ: hướng gọi bị đảo ngược. Thông thường ta nghĩ "Server gọi API mô hình". Với sampling thì Server chỉ soạn yêu cầu, còn Client mới là bên thật sự gọi mô hình — vì Client thường đã có sẵn kết nối và credential.
Bài toán mà Sampling giải quyết
Giả sử có một MCP Server với một research tool cào thông tin từ Wikipedia. Sau khi gom xong dữ liệu, ta cần tóm tắt nó thành một báo cáo mạch lạc. Có hai lựa chọn:
- Lựa chọn 1 — cho Server truy cập Claude trực tiếp. Server phải có API key riêng, tự xử lý authentication, tự quản lý chi phí và tự viết toàn bộ code tích hợp Claude. Chạy được, nhưng thêm rất nhiều phức tạp — và nếu là server công khai thì Server gánh chi phí AI cho mọi user.
- Lựa chọn 2 — dùng Sampling. Server soạn một prompt và hỏi Client: "Gọi Claude giúp tôi được không?". Client — vốn đã có kết nối tới Claude — thực hiện cuộc gọi và trả kết quả về.
Cách Sampling hoạt động (luồng 6 bước)
Luồng khá thẳng: Server làm xong việc của mình, soạn prompt, gửi sampling request sang Client; Client gọi Claude rồi trả text về để Server dùng trong kết quả cuối.
| # | Bên thực hiện | Hành động |
|---|---|---|
| 1 | Server | Hoàn tất công việc của mình (vd: fetch các bài Wikipedia) |
| 2 | Server | Tạo một prompt yêu cầu sinh văn bản |
| 3 | Server | Gửi sampling request sang Client |
| 4 | Client | Gọi Claude với prompt được cung cấp |
| 5 | Client | Trả text sinh ra về cho Server |
| 6 | Server | Dùng text đó trong kết quả trả về |
Client vs Server: ai giữ bộ não, ai gọi Claude
Điểm dễ nhầm nhất của sampling là hướng gọi. Chìa khóa: trong MCP, Client là bên có "bộ não" (kết nối tới model Claude) và credit token — ví dụ Claude Code, Claude Desktop, hay app tôi tự viết. Server chỉ là kho công cụ (fetch web, query DB, gọi API), theo thiết kế không có bộ não. Vì vậy câu hỏi "ai gọi Claude?" luôn có một đáp án: bên có bộ não — tức Client. Sampling chỉ là tình huống Server không có bộ não nên nhờ Client gọi hộ.
Lợi ích của Sampling
- Giảm độ phức tạp phía Server — Server không cần tự tích hợp với mô hình ngôn ngữ.
- Chuyển gánh nặng chi phí — Client trả phí token, không phải Server.
- Không cần API key — Server không cần credential để gọi Claude.
- Lý tưởng cho public server — bạn không muốn một server công khai bị đội chi phí AI cho mỗi user.
Triển khai — code hai phía
Bật sampling cần code ở cả hai phía.
Server side
Trong hàm tool, dùng create_message để yêu cầu sinh văn bản:
@mcp.tool()
async def summarize(text_to_summarize: str, ctx: Context):
prompt = f"""
Please summarize the following text:
{text_to_summarize}
"""
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(
type="text",
text=prompt
)
)
],
max_tokens=4000,
system_prompt="You are a helpful research assistant",
)
if result.content.type == "text":
return result.content.text
else:
raise ValueError("Sampling failed")
Lưu ý: create_message là hàm async (phải await) và nó không tự gọi Claude — nó gửi yêu cầu để Client gọi hộ.
Client side
Tạo một sampling callback để xử lý các yêu cầu từ Server:
async def sampling_callback(
context: RequestContext, params: CreateMessageRequestParams
):
# Gọi Claude bằng Anthropic SDK
text = await chat(params.messages)
return CreateMessageResult(
role="assistant",
model=model,
content=TextContent(type="text", text=text),
)
Rồi truyền callback này vào khi khởi tạo client session:
async with ClientSession(
read,
write,
sampling_callback=sampling_callback
) as session:
await session.initialize()
Chính sampling_callback mới là nơi thực sự gọi Claude — nó nằm ở phía Client, dùng credential của Client.
Khi nào nên dùng Sampling
Sampling có giá trị nhất khi xây public MCP server (server truy cập công khai). Bạn không muốn user ngẫu nhiên sinh văn bản không giới hạn bằng chi phí của mình. Với sampling, mỗi Client tự trả phí AI của họ mà vẫn tận dụng được chức năng của Server.
Về bản chất, kỹ thuật này dời độ phức tạp tích hợp AI từ Server sang Client — nơi thường đã sẵn có kết nối và credential cần thiết.
Áp dụng thực tế (góc nhìn Delivery)
Ở góc độ vận hành, sampling là một quyết định kiến trúc chi phí hơn là kỹ thuật thuần túy. Khi tôi cân nhắc mở một MCP Server dùng chung cho nhiều team hoặc công khai, câu hỏi đầu tiên không phải "làm sao gọi Claude" mà là "ai chịu hóa đơn token". Nếu Server tự cầm API key, một tool sinh văn bản bị gọi lặp trong vòng lặp agent có thể thổi bay ngân sách mà chủ Server không kiểm soát được đầu vào. Sampling đẩy cả chi phí lẫn quyền chọn mô hình về phía Client — bên thật sự phát sinh nhu cầu — nên nó vừa an toàn ngân sách, vừa giảm bề mặt bảo mật (Server không giữ secret). Đổi lại, Server mất quyền quyết định model/tham số sinh, nên với internal tool dùng nội bộ có kiểm soát, gọi trực tiếp đôi khi vẫn đơn giản và đủ.
Logging và Progress notifications
Logging và progress notifications rất dễ triển khai nhưng tạo khác biệt lớn về trải nghiệm khi làm việc với MCP server. Chúng giúp người dùng biết đang có gì xảy ra trong những thao tác chạy lâu, thay vì phải phỏng đoán xem tool có bị treo hay không.
Bài toán
Khi Claude gọi một tool tốn thời gian — như research một chủ đề hay xử lý dữ liệu — người dùng thường không thấy gì cho tới khi thao tác kết thúc. Điều này gây khó chịu: họ không biết tool đang chạy hay đã "đứng hình". Bật logging và progress notifications thì người dùng nhận phản hồi real-time: log trạng thái, thanh tiến độ, thông báo chi tiết ngay trong lúc chạy.
Cách hoạt động — qua Context
Trong Python MCP SDK, cả hai đi qua tham số Context được tự động truyền vào hàm tool. Object này cho ta các method để "nói chuyện ngược" về Client trong lúc thực thi:
context.info()— gửi log message về Client.context.report_progress(current, total)— cập nhật tiến độ (giá trị hiện tại / tổng).
@mcp.tool(name="research", description="Research a given topic")
async def research(
topic: str = Field(description="Topic to research"),
*,
context: Context
):
await context.info("About to do research...")
await context.report_progress(20, 100)
sources = await do_research(topic)
await context.info("Writing report...")
await context.report_progress(70, 100)
results = await generate_report(sources)
return results
(Lưu ý dấu *, trong chữ ký hàm: context là keyword-only argument — luôn truyền theo tên.)
Phía Client — hai callback, hai chỗ gắn khác nhau
Server chỉ phát thông báo; Client toàn quyền quyết định trình bày thế nào. Đây là điểm dễ nhầm nhất và hay ra thi — hai callback gắn vào hai nơi khác nhau:
| Callback | Nhận gì | Gắn ở đâu |
|---|---|---|
logging_callback |
LoggingMessageNotificationParams (đọc params.data) |
ClientSession(...) — cấp session (1 lần) |
progress_callback |
(progress, total, message) |
session.call_tool(...) — cấp từng tool call |
async def logging_callback(params: LoggingMessageNotificationParams):
print(params.data)
async def print_progress_callback(
progress: float, total: float | None, message: str | None
):
if total is not None:
percentage = (progress / total) * 100
print(f"Progress: {progress}/{total} ({percentage:.1f}%)")
else:
print(f"Progress: {progress}")
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write, logging_callback=logging_callback # ← cấp session
) as session:
await session.initialize()
await session.call_tool(
name="add",
arguments={"a": 1, "b": 3},
progress_callback=print_progress_callback, # ← cấp từng call
)
Cách trình bày tùy loại ứng dụng
- CLI — chỉ cần
printmessage + tiến độ ra terminal. - Web — dùng WebSocket, server-sent events (SSE) hoặc polling để đẩy update lên browser.
- Desktop — cập nhật progress bar / status display trong UI.
Cả hai tính năng hoàn toàn tùy chọn: có thể phớt lờ, chỉ hiện một loại, hoặc trình bày kiểu gì tùy ứng dụng. Chúng thuần là enhancement trải nghiệm giúp người dùng hiểu chuyện gì đang diễn ra trong thao tác chạy lâu — không thay đổi kết quả của tool.
Áp dụng thực tế (góc nhìn Delivery)
So với sampling (quyết định kiến trúc chi phí), logging/progress là quyết định về niềm tin của người dùng. Với tool đồng bộ vài giây thì bỏ qua cũng được; nhưng với tool agentic chạy hàng chục giây tới vài phút (research, batch xử lý), im lặng = người dùng nghĩ hỏng rồi kill tiến trình hoặc bấm lại — vừa hỏng trải nghiệm vừa tốn compute lặp. Tôi coi report_progress ở các mốc có ý nghĩa (không phải mỗi vòng lặp — spam log cũng hại) như một hợp đồng ngầm với người dùng: "vẫn đang chạy, còn ngần này nữa". Vì đây là kênh một chiều Server→Client và Client có quyền lờ đi, thêm nó gần như không rủi ro — nên với tool chạy lâu, mặc định là nên có.
Roots — cấp quyền truy cập file + ngữ cảnh để tìm file
Roots là cách cấp cho MCP Server quyền truy cập một số file và thư mục cụ thể trên máy local. Có thể hình dung như một hệ thống phân quyền nói: "MCP server, mày được đụng vào những file này" — nhưng nó làm nhiều hơn chỉ cấp quyền: nó còn cho Claude ngữ cảnh để tự tìm file.
Bài toán Roots giải quyết
Không có roots sẽ vướng một vấn đề rất phổ biến. Giả sử có một MCP server với tool convert video: nhận vào một đường dẫn file rồi đổi MP4 sang MOV.
Khi người dùng bảo Claude "convert biking.mp4 to mov", Claude chỉ gọi tool với đúng cái tên file. Vấn đề: Claude không có cách nào dò khắp filesystem để biết file đó thực sự nằm đâu. Máy có thể phức tạp, file nằm rải rác nhiều thư mục. Người dùng biết biking.mp4 ở folder Movies, nhưng Claude thì không.
Có thể chữa bằng cách bắt user luôn nhập đường dẫn đầy đủ — nhưng rất kém thân thiện. Chẳng ai muốn gõ full path mỗi lần.
Roots hoạt động (4 bước, tự động)
Người dùng vẫn chỉ cần nói "convert biking.mp4" — Claude tự lo phần còn lại:
| # | Claude làm gì |
|---|---|
| 1 | Người dùng yêu cầu đổi một file video |
| 2 | Gọi list_roots để xem được phép truy cập những thư mục nào |
| 3 | Gọi read_dir trên các thư mục đó để dò tìm file |
| 4 | Tìm thấy → gọi tool convert với đường dẫn đầy đủ |
Bảo mật và ranh giới
Roots cũng tạo bảo mật bằng cách giới hạn truy cập. Nếu chỉ cấp quyền vào folder Desktop, MCP server không thể đụng file ở nơi khác như Documents hay Downloads. Khi Claude thử truy cập file ngoài các root đã duyệt, nó nhận lỗi và có thể báo cho người dùng biết file đó không truy cập được với cấu hình server hiện tại.
⚠️ Điểm dễ nhầm nhất — SDK KHÔNG tự thực thi
MCP SDK không tự động ép ràng buộc root — lập trình viên phải tự triển khai. Mẫu điển hình là viết một hàm helper is_path_allowed():
- Nhận đường dẫn file được yêu cầu.
- Lấy danh sách roots đã duyệt.
- Kiểm tra path có nằm trong một root nào không.
- Trả về
True/Falsecho quyền truy cập.
Rồi gọi hàm này trong mọi tool có đụng tới file/thư mục, trước khi thực hiện thao tác thật. Nói cách khác: list_roots là bản đồ, còn is_path_allowed() là bảo vệ đứng cửa — có bản đồ mà quên bảo vệ thì ranh giới chỉ là hình thức.
Lợi ích chính
- Thân thiện — người dùng không cần cung cấp đường dẫn đầy đủ.
- Tìm kiếm có tiêu điểm — Claude chỉ lục trong thư mục đã duyệt → tìm file nhanh hơn.
- Bảo mật — chặn truy cập nhầm các file nhạy cảm ngoài vùng cho phép.
- Linh hoạt — có thể cấp roots qua tool, hoặc inject thẳng vào prompt.
Áp dụng thực tế (góc nhìn Delivery)
Điểm tôi luôn nhấn với team: roots là "security theater" nếu quên is_path_allowed(). Việc khai báo root chỉ cho Claude biết nên tìm ở đâu — nó không tự chặn một tool đọc /etc/passwd hay C:\Users\...\.ssh. Ranh giới thật chỉ tồn tại khi mọi tool đụng file đều gọi hàm kiểm tra trước, và phải chống cả mẹo path traversal (../../) khi so khớp. Vì vậy tôi coi roots gồm hai nửa: nửa "trải nghiệm" (SDK lo — list_roots/read_dir) và nửa "an toàn" (dev tự lo — is_path_allowed). Nhớ đúng ranh giới trách nhiệm này là chỗ tách một MCP server đồ chơi khỏi một cái đủ an toàn để cắm vào máy thật.
Tổng hợp 3 tính năng nâng cao
Ba tính năng của bài này giải ba bài toán khác nhau, dễ lẫn — bảng dưới gom lại để ôn nhanh:
| Sampling | Logging & Progress | Roots | |
|---|---|---|---|
| Giải bài toán | Tool cần AI nhưng Server không nên tự nuôi model | Tool chạy lâu, user tưởng bị treo | Claude cần tìm file nhưng không biết ở đâu + cần ranh giới |
| Hướng đi | Server nhờ → Client gọi model | Server báo → Client hiển thị | Client hỏi → Server khai báo folder cho phép |
| Hàm phía Server | create_message (qua ctx.session) |
context.info() · context.report_progress() |
(tự viết) is_path_allowed() trong mỗi tool |
| Phía Client | sampling_callback (ở ClientSession) |
logging_callback (ở ClientSession) + progress_callback (ở call_tool) |
list_roots · read_dir |
| Bản chất | Chi phí & credential (Client trả token) | Trải nghiệm (trấn an user) | Bảo mật & ngữ cảnh (ranh giới + bản đồ) |
| Bắt buộc? | Tùy — hợp nhất cho public server | Optional, thuần UX | Ranh giới phải tự code (SDK không ép) |
| Nhớ 1 câu | "mượn bộ não" | "gắn đèn báo" | "chìa khoá + bản đồ" |
Elicitation — tính năng thứ 4 khoá chưa dạy
Khoá dừng ở ba tính năng trên, nhưng MCP còn một primitive nữa đáng biết — và khác ba cái kia, nó không nằm trong danh sách deprecated: Elicitation.
Elicitation cho phép MCP Server xin input có cấu trúc từ người dùng ngay giữa chừng một task. Khi Server cần một thông tin mà nó không tự lấy được (chọn môi trường deploy, xác nhận một tham số, điền một field còn thiếu), nó phát một elicitation request; Claude Code hiện một dialog tương tác và trả câu trả lời của bạn về cho Server. Không cần cấu hình gì phía người dùng — dialog tự hiện khi server yêu cầu.
Vài điểm thực tế:
- Muốn tự động trả lời mà không hiện dialog: dùng
Elicitationhook. - Một tool call đang chờ elicitation dialog không bị đẩy xuống background khi dialog còn mở — Server đang chờ input của bạn, không phải chạy chậm.
- Viết server có elicitation thì tra MCP elicitation specification để lấy protocol và schema mẫu.
Đừng lẫn Elicitation với Sampling — cả hai đều là "Server cần thứ nó không có", nhưng hỏi hai đối tượng khác nhau:
| Sampling (deprecated) | Elicitation (còn dùng) | |
|---|---|---|
| Server xin gì | Một lượt sinh văn bản từ mô hình | Một mẩu input có cấu trúc |
| Hỏi ai | Mô hình (qua Client gọi hộ) | Người dùng (qua dialog Client dựng) |
| Nhớ 1 câu | "mượn bộ não" | "hỏi lại chủ nhà" |
Message types — JSON-RPC và hai nhóm message
MCP xử lý giao tiếp giữa client và server hoàn toàn bằng JSON messages (chuẩn JSON-RPC 2.0). Hiểu các loại message rất quan trọng khi làm việc với MCP, đặc biệt khi đụng tới các transport khác nhau như streamable HTTP transport.
Định dạng message
Mỗi loại message phục vụ một mục đích cụ thể — gọi tool, liệt kê resource, hay báo sự kiện hệ thống. Ví dụ điển hình: khi Claude cần gọi một tool, client gửi Call Tool Request; server xử lý, chạy tool, rồi trả về Call Tool Result chứa output.
// Call Tool Request (client → server)
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "add", "arguments": { "a": 5, "b": 3 } } }
// Call Tool Result (server → client)
{ "jsonrpc": "2.0", "id": 1,
"result": { "content": [{ "type": "text", "text": "8" }], "isError": false } }
Trường id ghép "thư hỏi" với đúng "thư trả lời" của nó (như số vận đơn) — gửi nhiều request song song vẫn biết cái nào ứng với cái nào. Result còn có isError để báo tool chạy lỗi hay không.
Hai nhóm message
| Nhóm | Đặc điểm | Ví dụ |
|---|---|---|
| Request–Result | Luôn đi theo cặp, id khớp nhau; gửi request là chờ result |
Call Tool · List Prompts · Read Resource · Initialize (Request → Result) |
| Notification | Một chiều, báo sự kiện, KHÔNG cần phản hồi | Progress · Logging Message · Tool List Changed · Resource Updated |
Lưu ý liên hệ: Logging và Progress ở các mục trên chính là hai thành viên của nhóm Notification — nay đã rõ chúng thuộc "họ" nào.
Spec ≠ SDK
Danh sách đầy đủ các loại message được định nghĩa trong MCP Specification chính thức trên GitHub (github.com/modelcontextprotocol/modelcontextprotocol) — đây là nguồn chuẩn (authoritative) mô tả MCP nên hoạt động thế nào. Spec tách riêng khỏi các repo SDK (Python, TypeScript…). Các message types được viết bằng TypeScript cho tiện — không phải để chạy như code TS, mà vì TypeScript là cách rõ ràng để mô tả cấu trúc và kiểu dữ liệu.
Client vs Server messages — và tính hai chiều
Spec tổ chức message theo ai gửi: Client messages (request client gửi cho server như tool call, cùng các notification client gửi) và Server messages (request server gửi cho client, cùng notification server broadcast).
Điểm cốt lõi: MCP là giao thức hai chiều (bidirectional) — cả client lẫn server đều có thể chủ động khởi tạo giao tiếp, không phải lúc nào client cũng hỏi trước. Điều này đặc biệt quan trọng khi chọn transport: một số transport như streamable HTTP có giới hạn loại message nào được đi theo chiều nào. Biết rằng server-gửi-client là chuyện có thật giúp chọn đúng transport để notification từ server không bị "chết".
Transport và stdio
MCP client và server trao đổi message JSON — nhưng những message đó thật sự được truyền đi bằng cách nào? Kênh giao tiếp đó gọi là transport. Có nhiều cách hiện thực: HTTP request, WebSocket, thậm chí viết JSON lên bưu thiếp (dĩ nhiên cách cuối không dành cho production).
Stdio transport
Khi mới phát triển một MCP server hoặc client, transport hay dùng nhất là stdio transport. Cách làm rất thẳng: client khởi chạy MCP server như một subprocess và giao tiếp qua luồng vào/ra chuẩn:
- Client gửi message tới server qua
stdincủa server. - Server trả lời bằng cách ghi ra
stdout. - Server hoặc client đều có thể gửi message bất cứ lúc nào.
- Chỉ hoạt động khi client và server chạy trên cùng một máy.
Bốn tình huống giao tiếp — chỉ với hai ống
Với bất kỳ transport nào cũng phải xử lý bốn kiểu giao tiếp. Điểm tinh tế của stdio: ống được chọn theo hướng đi, không theo vai trò của message — cứ đi vào server thì qua stdin, đi ra khỏi server thì qua stdout.
| # | Tình huống | Kênh |
|---|---|---|
| 1 | Client gửi request → Server | Client ghi vào stdin |
| 2 | Server gửi response → Client | Server ghi ra stdout |
| 3 | Server gửi request → Client | Server ghi ra stdout |
| 4 | Client gửi response → Server | Client ghi vào stdin |
Vẻ đẹp của stdio nằm ở sự đơn giản: chỉ hai kênh, và bên nào cũng có thể chủ động mở lời bất cứ lúc nào.
Test server ngay trên terminal
Có thể kiểm thử một MCP server thẳng từ terminal, không cần viết client riêng. Khi chạy uv run server.py, server lắng nghe stdin và ghi phản hồi ra stdout — nghĩa là có thể dán thẳng message JSON vào terminal và thấy phản hồi ngay. Rất tiện để debug.
Trình tự khởi tạo kết nối (bắt tay 3 message)
Mọi kết nối MCP bắt buộc mở đầu bằng đúng ba message theo thứ tự:
| # | Message | Vai trò |
|---|---|---|
| 1 | Initialize Request | Client gửi trước tiên |
| 2 | Initialize Result | Server trả lời, khai báo capabilities |
| 3 | Initialized Notification | Client xác nhận — không chờ phản hồi |
Chỉ sau khi hoàn tất bắt tay này mới được gửi các request khác như tool call hay list prompts.
Ai khởi xướng message nào
Bảng dưới gom lại hướng đi của các message — và nó khớp lại toàn bộ các mục trên:
| Bên khởi xướng | Request–Result | Notification |
|---|---|---|
| Client → | Call Tool · List Prompts · Read Resource · Initialize |
Initialized · Cancelled |
| Server → | Create Message (= Sampling) · List Roots (= Roots) |
Progress · Logging |
Nhìn dòng Server sẽ thấy lý do MCP phải là giao thức hai chiều: Sampling (Create Message) và Roots (List Roots) đều là request do server gửi ngược cho client. Nếu transport không cho server chủ động mở lời, hai tính năng này không hoạt động được.
Vì sao stdio quan trọng
Hiểu stdio là then chốt vì nó đại diện cho trường hợp "lý tưởng": giao tiếp hai chiều diễn ra mượt mà, không ràng buộc. Khi chuyển sang transport khác như HTTP, sẽ gặp giới hạn: server không phải lúc nào cũng khởi tạo được request tới client. Stdio là mốc chuẩn (baseline) để hiểu một cuộc giao tiếp MCP đầy đủ trông ra sao, trước khi đối mặt ràng buộc của các transport khác.
Với phát triển và kiểm thử, stdio là hoàn hảo. Với production khi client và server cần chạy trên các máy khác nhau, phải cân nhắc transport khác cùng những đánh đổi riêng của chúng.
Streamable HTTP transport
Streamable HTTP transport cho phép MCP client kết nối tới server được host từ xa qua HTTP connection. Khác stdio vốn đòi client và server cùng một máy, transport này mở ra khả năng làm public MCP server mà ai cũng truy cập được (ví dụ host tại https://mcp-server.com/mcp).
⚡ Cập nhật (07/2026): transport
sseđã deprecated. Docs Claude Code ghi rõ "The SSE (Server-Sent Events) transport is deprecated. Use HTTP servers instead, where available." — tức khi khai báo server, dùng--transport httpthay cho--transport sse. Lưu ý phân biệt: SSE với tư cách một transport type riêng thì bị khai tử, nhưng SSE với tư cách cơ chế bên trong Streamable HTTP (phần dưới đây mô tả) vẫn nguyên — Streamable HTTP vẫn mở SSE connection để đẩy message server→client. Deprecate là cái vỏtype: "sse", không phải kỹ thuật SSE.
Nhưng có một cảnh báo quan trọng: vài cấu hình có thể giới hạn đáng kể chức năng của MCP server. Nếu ứng dụng chạy hoàn hảo với stdio ở local nhưng vỡ khi deploy bằng HTTP transport, đây nhiều khả năng là thủ phạm.
Hai setting cần biết
| Setting | Mặc định | Vai trò |
|---|---|---|
stateless_http |
false |
Điều khiển việc quản lý trạng thái kết nối |
json_response |
false |
Điều khiển việc xử lý định dạng response |
Mặc định cả hai đều false, nhưng một số kịch bản deploy buộc phải bật lên true. Khi bật, chúng có thể phá các chức năng lõi: progress notification, logging, và các request do server khởi xướng.
Gốc rễ: HTTP vốn không cho server mở lời
Để hiểu vì sao có giới hạn này, cần nhớ HTTP hoạt động ra sao. Với HTTP tiêu chuẩn:
- Client dễ dàng khởi tạo request tới server — vì server có URL đã biết.
- Server dễ dàng phản hồi các request đó.
- Server không thể dễ dàng khởi tạo request tới client — vì client không có URL đã biết.
- Kiểu "client phản hồi ngược lại server" trở nên rắc rối.
Điều này đúng với mọi HTTP request, không riêng gì MCP. Nói cách khác, HTTP thuần chỉ phục vụ tốt 2 trong 4 tình huống giao tiếp mà MCP cần.
Những message bị ảnh hưởng
Giới hạn trên tác động đúng vào các pattern sau — và đó chính là những tính năng đã học ở các mục trên:
| Nhóm | Message | Tương ứng tính năng |
|---|---|---|
| Request do Server khởi xướng | Create Message · List Roots |
Sampling · Roots |
| Notification | Progress · Logging · Initialized · Cancelled |
Logging & Progress |
Đây đúng là những thứ vỡ khi bật các setting HTTP hạn chế: thanh tiến độ biến mất, logging ngừng hoạt động, và sampling request do server khởi xướng thất bại.
Giải pháp: lách bằng Server-Sent Events (SSE)
StreamableHTTP giải bài toán này bằng một workaround khéo dùng Server-Sent Events (SSE). Ý tưởng cốt lõi: server không biết địa chỉ client, vậy thì để client tự gọi tới và giữ kết nối mở — server chỉ việc "nói xuống" đường dây có sẵn đó.
Bước 1 — khởi tạo và nhận mcp-session-id. Kết nối bắt đầu như mọi kết nối MCP, nhưng có thêm một chi tiết:
| # | Message | Điểm mới |
|---|---|---|
| 1 | Initialize Request |
Client gửi |
| 2 | Initialize Result |
Server trả về kèm header mcp-session-id (vd 7089d4160bbb4d) |
| 3 | Initialized Notification |
Client gửi lại kèm session ID |
mcp-session-id là then chốt: nó định danh duy nhất client và bắt buộc phải có trong mọi request về sau.
⚠️ Cập nhật spec
2026-07-28— "Bước 1" ở trên đã bị gỡ khỏi spec. Khác với ba tính năng ở đầu bài (chỉ deprecate, vẫn chạy), chỗ này là retire thật: bản spec chính thức2026-07-28đã bỏ hẳn cặpinitialize/initializedcùng với headerMcp-Session-Id.Thay bằng gì: mỗi request tự đi một mình, mang theo protocol version, client identity và client capabilities trong trường
_metacủa chính nó. Server nào cần biết trước capability thì gọi RPCserver/discover— và đây là tuỳ chọn, không bắt buộc.Vì sao đổi: bỏ phiên có định danh nghĩa là server không phải giữ state của phiên nữa, nên một request rơi vào bất kỳ instance nào sau load balancer cũng xử lý được, không cần shared storage. Đúng bài toán mà mô hình "session ID dán vào mọi request" ở trên không giải nổi khi scale ngang.
Vẫn nên đọc kỹ phần này: nó giải thích vì sao thiết kế cũ cần session ID, và ta còn phải đọc/bảo trì server, SDK viết trước 07/2026. Nhưng khi implement mới thì theo spec
2026-07-28— đừng dựng lại handshake đã bị gỡ.
Bước 2 — mở đường SSE. Sau khi khởi tạo, client gửi một GET request để thiết lập một SSE connection. Việc này tạo ra một HTTP response sống lâu (long-lived) mà server có thể dùng để stream message về client bất cứ lúc nào. Chính SSE connection này là chìa khoá mở lại chiều server→client: từ đây server gửi được request, notification và các message khác qua kênh bền vững này.
Mô hình hai SSE connection
Khi client gọi tool, mọi thứ phức tạp hơn: hệ thống tạo hai SSE connection riêng biệt.
| Kết nối | Mở khi | Dùng cho | Đóng khi |
|---|---|---|---|
| Primary SSE | Client GET sau khi init |
Request do server khởi xướng | Mở vô hạn (indefinitely) |
| Tool-specific SSE | Mỗi tool call (POST) |
Message liên quan tool call đó | Tự đóng khi Call Tool Result được gửi |
Định tuyến message — điểm rất dễ nhầm
Các loại message được định tuyến qua những kết nối khác nhau:
| Message | Đi qua |
|---|---|
Request do server khởi xướng (Create Message, List Roots) |
Primary SSE |
| Progress notification | Primary SSE |
| Logging message | Tool-specific SSE |
| Call Tool Result | Tool-specific SSE |
⚠️ Bẫy đáng nhớ: Progress đi qua Primary, còn Logging lại đi qua Tool-specific — dù cả hai đều phát ra trong lúc tool đang chạy.
⚠️ Cập nhật spec
2026-07-28— mô hình hai kết nối này đã có thứ thay thế. Cả bảng trên tồn tại chỉ vì một lý do: server cần giữ một stream mở để tự khởi xướng request về client. Spec mới bỏ nhu cầu đó bằng MRTR (Multi Round-Trip Requests, SEP-2322): server không giữ stream nữa mà trả về ngay một responseresultType: "input_required"liệt kê những gì nó cần; client gọi lại cùng request đó với câu trả lời đặt tronginputResponses. Một request → một response, đúng mô hình HTTP, khỏi Primary SSE.Kèm theo đó: notification của Tasks extension dời sang stream
subscriptions/listen— tức phần "server nói xuống" giờ là một stream có tên, opt-in, không phải hai kết nối ngầm phải tự suy ra.Vẫn nên nắm bảng định tuyến trên: nó là thực tế của mọi server/SDK viết trước 07/2026 mà ta còn phải debug, và đề thi hỏi theo bản này.
Hai flag phá cơ chế lách
Đặt stateless_http=True hoặc json_response=True phá vỡ cơ chế SSE workaround này. Có kịch bản buộc phải bật, nhưng bật là giới hạn toàn bộ chức năng MCP phụ thuộc giao tiếp server→client. Về bản chất, đó là bảo transport hoạt động bên trong ràng buộc của HTTP thay vì lách qua chúng.
mcp = FastMCP(
"mcp-server",
stateless_http=True,
json_response=True,
)
Vì sao cần stateless HTTP — bài toán scale ngang
Giả sử một MCP server trở nên phổ biến. Ban đầu chỉ vài client nối tới một instance. Khi lớn lên tới hàng ngàn client, một instance không gánh nổi. Giải pháp kinh điển là scale ngang (horizontal scaling): chạy nhiều instance sau một load balancer.
Nhưng đây là chỗ rắc rối. Client MCP cần hai kết nối riêng biệt: một GET SSE để nhận request từ server, và các POST để gọi tool cùng nhận kết quả. Load balancer có thể định tuyến hai kết nối này tới hai instance khác nhau. Nếu tool cần dùng Claude (qua sampling), instance đang xử lý POST phải phối hợp với instance đang giữ GET SSE — tạo ra một bài toán điều phối phức tạp giữa các server.
Stateless HTTP giải quyết thế nào — và cái giá
Đặt stateless_http=True loại bỏ bài toán điều phối, nhưng kèm đánh đổi đáng kể. Khi bật stateless HTTP:
- Client không nhận session ID — server không thể theo dõi từng client.
- Không còn request server→client — đường GET SSE trở nên vô dụng (server không ghép nổi kênh response đó với request đến nào).
- Không sampling — không dùng được Claude hay model AI khác.
- Không progress report — không gửi được cập nhật tiến độ khi chạy lâu.
- Không subscriptions — không báo được resource update cho client.
Đổi lại, có một lợi ích: không còn bắt buộc khởi tạo (initialization) — client gọi request thẳng, khỏi quá trình bắt tay ban đầu.
json_response — chỉ tắt streaming
Flag json_response=True đơn giản hơn nhiều: nó chỉ tắt streaming cho response của POST request. Thay vì nhận nhiều message SSE trong lúc tool chạy, ta chỉ nhận kết quả cuối dạng JSON thuần. Khi tắt streaming: không có message tiến độ trung gian, không có log trong lúc thực thi, chỉ còn kết quả cuối của tool.
Bật cả hai flag thì sao — bảng 4 tổ hợp
Hai flag này không loại trừ nhau: chúng là hai công tắc độc lập, điều khiển hai thứ khác nhau — stateless_http cắt đường GET SSE (kênh server mở lời), còn json_response cắt streaming trên POST response. Chính ví dụ của khoá cũng bật cả hai cùng lúc.
stateless_http |
json_response |
Kết quả |
|---|---|---|
false |
false |
Mặc định — MCP đầy đủ. Có session ID, GET SSE, sampling, roots, progress, logging, subscriptions, và POST response được stream. |
true |
false |
Mất: session ID · request server→client · sampling · progress · subscriptions. Được: khỏi initialization. POST response vẫn stream. |
false |
true |
Vẫn còn session ID và đường GET SSE (nên sampling/roots vẫn dùng được), nhưng POST response không stream → mất log lúc thực thi và message tiến độ trung gian, chỉ còn kết quả cuối. |
true |
true |
Hạn chế nhất — MCP tụt xuống mức request/response JSON thuần, không trạng thái: POST một phát, nhận một JSON cuối. Không session, không init, không chiều server→client, không stream. Scale tối đa, chức năng tối thiểu. |
Cách nhớ: stateless_http giết chiều server nói xuống; json_response giết dòng chảy của câu trả lời. Bật cả hai = server chỉ còn biết trả lời khi được hỏi, và trả đúng một lần.
Lưu ý đọc kỹ: khoá mô tả
json_responselà "chỉ tắt streaming cho response của POST". Việc sampling/roots còn sống ở tổ hợpfalse/truelà suy ra từ chỗ chúng đi qua Primary SSE (mở bằngGET) chứ không qua POST stream — khoá không nói thẳng ý này, nên khi thi cứ bám định nghĩa gốc của từng flag.
Stdio vs Streamable HTTP — bảng phân biệt
| Tiêu chí | stdio | Streamable HTTP |
|---|---|---|
| Kênh truyền | stdin (vào server) · stdout (ra server) |
POST (client→server) · GET SSE (server→client) |
| Client & server ở đâu | Bắt buộc cùng một máy | Khác máy — server host từ xa (public được) |
| Server chạy thế nào | Client spawn làm subprocess | Server chạy sẵn tại URL đã biết |
| Server mở lời được? | Được, tự nhiên — ống luôn mở hai chiều | Không tự nhiên — phải lách bằng GET SSE |
| Định danh phiên | Không cần | mcp-session-id bắt buộc mọi request sau init |
| Số kết nối | 2 ống cố định | Primary SSE + Tool-specific SSE (mỗi tool call) |
| Sampling · Roots · Progress | ✅ chạy tự nhiên | ✅ chỉ khi giữ được SSE (mất nếu stateless_http=True) |
| Scale ngang / load balancer | Không đặt ra (1 client ↔ 1 subprocess) | Vấn đề lớn → chính là lý do có stateless_http |
| Bắt tay init | Bắt buộc | Bắt buộc — trừ khi stateless_http=True |
| Dùng khi | Dev / test / local | Production / public server |
Dev vs Production
Nếu phát triển local bằng stdio transport nhưng dự định deploy bằng HTTP transport, hãy test bằng đúng transport sẽ dùng ở production. Khác biệt hành vi giữa chế độ stateful và stateless có thể rất lớn, và bắt lỗi lúc phát triển vẫn hơn phát hiện sau khi deploy. Đây cũng chính là lời giải cho triệu chứng "chạy ngon ở local, lên prod thì vỡ" nêu ở trên.
Các flag này thay đổi tận gốc cách MCP server vận hành, nên phải chọn dựa trên yêu cầu scale và yêu cầu chức năng cụ thể của mình.
Cập nhật 07/2026: hướng đi mà khoá mô tả qua flag
stateless_httpđã thành mặc định của chính giao thức — spec MCP 2026-07-28 bỏ handshake và session ID bắt buộc. Sau khi học xong khoá này, tôi phân tích thay đổi đó và soạn checklist migrate cho server author ở bài MCP chuyển stateless (spec 2026-07-28).
Điểm cốt lõi cần nhớ
StreamableHTTP phức tạp hơn các transport MCP khác vì phải lách giới hạn của HTTP. Workaround dựa trên SSE cho phép chạy đầy đủ chức năng MCP qua HTTP, nhưng hiểu mô hình hai kết nối là điều then chốt để debug và tối ưu. Khi build ứng dụng MCP với StreamableHTTP, nhớ rằng session ID là bắt buộc cho mọi request sau khởi tạo, và hệ thống tự động quản lý nhiều SSE connection để phục vụ các loại giao tiếp server→client khác nhau.
⚠️ Câu "session ID là bắt buộc" đúng với bản khoá dạy, KHÔNG còn đúng với spec
2026-07-28. Bản mới gỡ hẳnMcp-Session-Idcùng cặpinitialize/initialized, và thay mô hình giữ-stream-mở bằng MRTR. Nên phát biểu cho đúng theo mốc: trước2026-07-28— session ID bắt buộc, hai SSE connection; từ2026-07-28— mỗi request tự mang thông tin trong_meta, không session, không handshake.
Áp dụng thực tế (góc nhìn Delivery)
Hiểu các giới hạn này giúp ra quyết định có cơ sở về: chọn transport nào cho từng kịch bản deploy; thiết kế server sao cho vẫn hoạt động tử tế trong ràng buộc HTTP; và khi nào chấp nhận giảm chức năng để đổi lấy lợi ích của việc host từ xa.
Điểm mấu chốt là biết những hạn chế này tồn tại và quy hoạch kiến trúc MCP server tương ứng. Nếu ứng dụng phụ thuộc nặng vào server-initiated request hoặc notification real-time, có thể phải cân nhắc lại lựa chọn transport hoặc hiện thực các pattern giao tiếp thay thế. Với tôi, đây là câu hỏi cần đặt ở giai đoạn thiết kế, không phải lúc deploy: chọn HTTP để đi xa là một đánh đổi có thật, không phải một chi tiết cấu hình.
Từ khoá cần thuộc
🔴 Core: Sampling · create_message (Server side, qua ctx.session) · sampling_callback (Client side, truyền vào ClientSession) · "Client trả phí token, Server không cần API key" · dùng cho public MCP server · context.info() (log) · context.report_progress(current, total) (tiến độ) · logging_callback gắn ở ClientSession, progress_callback gắn ở call_tool · Roots · list_roots · read_dir · is_path_allowed() · SDK KHÔNG tự enforce roots — dev tự implement.
🟡 Important: SamplingMessage · CreateMessageResult · CreateMessageRequestParams · max_tokens · system_prompt · luồng 6 bước Server↔Client · Context (tham số keyword-only) · LoggingMessageNotificationParams (params.data) · logging/progress là optional, chỉ UX · roots = permission + context (2 vai) · cấp root hẹp = ranh giới bảo mật (ngoài root → lỗi).
🟢 Good-to-know: TextContent / type == "text" · RequestContext · trade-off sampling vs Server gọi trực tiếp (internal vs public) · cách trình bày CLI / Web (SSE, WebSocket) / Desktop · total có thể là None · cấp roots qua tool hoặc inject vào prompt · path traversal (../) khi so khớp root · message types viết bằng TypeScript (mô tả kiểu, không chạy) · method "tools/call" · streamable HTTP transport giới hạn chiều message.
Message types (bổ sung Core/Important): 🔴 JSON-RPC 2.0 · hai nhóm Request–Result vs Notification · MCP là bidirectional (client & server đều gửi được). 🟡 id ghép request↔result · isError · các Notification: Progress / Logging Message / Tool List Changed / Resource Updated · MCP Specification repo tách khỏi SDK repo.
Streamable HTTP (bổ sung): 🔴 streamable HTTP = nối tới server host từ xa (public server) · workaround = Server-Sent Events (SSE) · mcp-session-id (server trả trong Initialize Result, bắt buộc kèm mọi request sau đó) · client mở SSE bằng GET → response long-lived cho server stream về · mô hình 2 kết nối: Primary SSE (mở vô hạn, cho server-initiated request + Progress) vs Tool-specific SSE (mỗi tool call, mang Logging + Call Tool Result, tự đóng khi result gửi xong) · stateless_http và json_response (mặc định false; bật true → phá SSE workaround) · lý do gốc: client không có URL đã biết nên server không mở lời được. 🟡 triệu chứng: stdio local chạy ngon, deploy HTTP thì vỡ · bẫy định tuyến: Progress → Primary, Logging → Tool-specific · bật 2 setting = "chơi trong ràng buộc HTTP thay vì lách" · HTTP thuần chỉ ngon 2/4 tình huống. 🟢 giới hạn này đúng với mọi HTTP, không riêng MCP · hệ thống tự động quản lý nhiều SSE connection.
stateless_http / json_response (bổ sung): 🔴 lý do tồn tại = scale ngang + load balancer đẩy GET SSE và POST sang 2 instance khác nhau ⇒ bài toán điều phối · stateless_http=True tắt: session ID · request server→client · sampling · progress · subscriptions; được: khỏi initialization · json_response=True chỉ tắt streaming của POST response ⇒ mất progress trung gian + log, chỉ còn kết quả cuối (JSON thuần). 🟡 FastMCP("mcp-server", stateless_http=True, json_response=True) · test bằng đúng transport sẽ deploy (stateful vs stateless khác nhau rất lớn). 🟢 không session id ⇒ server không ghép nổi đường GET SSE với request đến.
Spec 2026-07-28 (bổ sung — ngoài phạm vi khoá): 🔴 removed: initialize / initialized + Mcp-Session-Id ⇒ protocol core stateless, thông tin đi trong _meta từng request, server/discover là tuỳ chọn · deprecated: Roots · Sampling · Logging (SEP-2577) · legacy HTTP+SSE transport · DCR → CIMD, cửa sổ tối thiểu 12 tháng (sớm nhất xoá từ 28/07/2027) · ⭐ MRTR (Multi Round-Trip Requests, SEP-2322) — resultType: "input_required" + inputResponses, thay cho việc server giữ stream mở để tự khởi xướng request. 🟡 header routing Mcp-Method + Mcp-Name (SEP-2243) · cacheable list results ttlMs + cacheScope (SEP-2549) · extensions: Tasks (tasks/get, tasks/update, notification qua subscriptions/listen) · MCP Apps · EMA. 🟢 siết authorization: RFC 9207 issuer validation · application_type khi DCR · credential buộc vào authorization server đã phát.
Transport & stdio (bổ sung): 🔴 transport = kênh truyền message · stdio: client chạy server như subprocess, client ghi stdin · server ghi stdout · chỉ cùng một máy · bắt tay 3 bước Initialize Request → Initialize Result → Initialized Notification (bắt buộc, trước mọi request khác) · request do server khởi xướng: Create Message (sampling) + List Roots (roots). 🟡 ống chọn theo hướng đi, không theo vai trò message (4 tình huống / 2 kênh) · Initialized Notification không chờ phản hồi · Cancelled Notification (client) · stdio = baseline lý tưởng, HTTP bị giới hạn server-khởi-xướng. 🟢 test server bằng cách dán JSON vào terminal (uv run server.py) · Initialize Result khai capabilities.
Muốn tự kiểm tra? Làm đề thi thử ở tab "Đề thi thử".
Nguồn: Model Context Protocol: Advanced Topics (Anthropic Academy) — Copyright Anthropic. Phần đề thi thử cho khoá này nằm ở tab "Đề thi thử".