Learn > Claude > Model Context Protocol: Advanced Topics

Model Context Protocol: Advanced Topics

MCP nâng cao: sampling (server mượn model qua client), logging & progress notifications, và roots (truy cập file local có ranh giới).

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

TL;DRSampling 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).

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

MCP: Client vs Server — flow thường và flow sampling Client (vd Claude Code) có bộ não Claude và credit. MCP Server chỉ có công cụ, không có bộ não. Flow thường: Client gọi tool máy móc của Server. Flow sampling: tool của Server cần AI nên nhờ Client gọi Claude hộ; Client là bên trả tiền token. luồng thường (tool máy móc) luồng sampling (tool cần AI) 🧠 = bộ não Claude · 💰 = credit token ① FLOW THƯỜNG — tool chỉ làm việc máy móc, không cần AI MCP Server 🔧 tool: fetch web / query DB CLIENT 🧠 kết nối Claude + 💰 credit (vd: Claude Code) Claude ☁️ (model) gọi tool trả data thô chat AI trả lời → Server KHÔNG đụng tới Claude. Client trả tiền phần chat AI. Server chỉ cào / đọc dữ liệu. ② FLOW SAMPLING — tool của Server cần AI, nhưng Server không có bộ não MCP Server 🔧 tool: summarize() cần AI → không có não! CLIENT 🧠 + 💰 credit sampling_callback Claude ☁️ (model) ① nhờ gọi Claude create_message ② Client gọi (Client trả 💰) ③ text tóm tắt ④ trả text → Server "mượn" bộ não của Client. Server KHÔNG cần API key, KHÔNG trả tiền. Client là bên trả 💰. Chốt lại — ai là ai: CLIENT = bên có BỘ NÃO (kết nối Claude) + CREDIT token. Vd: Claude Code, Claude Desktop. SERVER = bên có CÔNG CỤ (fetch, DB, API), KHÔNG có bộ não. Vd: Cloudflare MCP, GitHub MCP. Khi tôi ngồi ở một Client (vd Claude Code), tôi là bên trả tiền phần AI — kể cả khi một Server dùng sampling, Client vẫn gọi Claude bằng credit của tôi. Tôi chỉ KHÔNG trả tiền khi tôi là tác giả Server — lúc đó Client của người dùng khác trả.
Flow thường (xám) là ca dùng ~99% thời gian: Server làm việc máy móc, không đụng model. Sampling (cam) là ca đặc biệt khi bản thân tool cần AI — Server mượn bộ não của Client; Client là bên trả token.

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

Loggingprogress 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: contextkeyword-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 print message + 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/False cho 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_rootsbản đồ, còn is_path_allowed()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 đồ"

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 HTTPgiớ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 stdin củ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).

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 clientbắt buộc phải có trong mọi request về sau.

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.

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.

Bài toán scale ngang: hai kết nối MCP bị lạc sang hai instance MCP client mở hai kết nối: GET SSE để nhận request từ server, và POST để gọi tool. Load balancer có thể đẩy chúng tới hai instance khác nhau — instance xử lý POST cần sampling lại không giữ đường SSE, nên phải phối hợp với instance kia. Đây là lý do stateless_http tồn tại. Bài toán: load balancer làm lạc hai kết nối của một client MCP Client cần 2 kết nối riêng biệt GET SSE (nhận server→client) POST (gọi tool) Load Balancer MCP Server #1 đang giữ đường GET SSE (kênh nói xuống client) MCP Server #2 xử lý POST — tool cần sampling nhưng không có đường SSE! phối hợp?! ⇒ Điều phối giữa các instance là rất phức tạp. stateless_http=True xoá bài toán này — bằng cách bỏ luôn trạng thái.
Client mở hai kết nối, nhưng load balancer có thể đẩy chúng sang hai instance khác nhau. Instance xử lý POST muốn sampling lại không nắm đường SSE — đây chính là bài toán mà stateless_http xoá bỏ.

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.

Hai flag tắt những gì: stateless_http và json_response stateless_http=True vô hiệu đường GET SSE nên mất session ID, request server đến client, sampling, progress và subscriptions, đổi lại không cần khởi tạo. json_response=True chỉ tắt streaming cho response của POST nên mất progress trung gian và log, chỉ còn kết quả cuối dạng JSON.

stateless_http = True cắt đường server→client

Client Server POST ✓ GET SSE

✗ session ID (không track client) ✗ request server → client ✗ sampling (không gọi được model) ✗ progress reports ✗ subscriptions (resource update) ✓ KHÔNG cần initialization

Bật khi: • cần scale ngang với load balancer • không cần giao tiếp server→client • tool không cần sampling • muốn giảm overhead kết nối

json_response = True chỉ tắt streaming của POST response

Client Server POST ✓ 1 JSON cuối

✗ progress trung gian ✗ log lúc thực thi ✓ chỉ kết quả cuối (JSON thuần)

Bật khi: • không cần streaming response • muốn response HTTP đơn giản • tích hợp hệ thống chỉ hiểu JSON thuần
Hai flag tắt hai thứ rất khác nhau: stateless_http cắt hẳn chiều server→client (mất sampling/progress/subscriptions, đổi lại khỏi init), còn json_response chỉ tắt streaming của POST response (mất progress trung gian và log, chỉ còn kết quả cuối).

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 thimessage 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_response là "chỉ tắt streaming cho response của POST". Việc sampling/roots còn sống ở tổ hợp false/true là suy ra từ chỗ chúng đi qua Primary SSE (mở bằng GET) 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ế độ statefulstateless 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.

Á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_httpjson_response (mặc định false; bật truephá 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 SSEPOST 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.

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ử".

Câu hỏi thường gặp

Sampling trong MCP là gì?
Sampling là cơ chế cho phép 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. Thay vì Server tự gọi Claude, nó gửi một sampling request nhờ Client gọi hộ và trả kết quả về. Điều này chuyển trách nhiệm và chi phí sinh văn bản từ Server sang Client.
Vì sao nên dùng Sampling cho public MCP server?
Vì với server công khai, bạn không muốn mọi user sinh văn bản không giới hạn bằng chi phí của mình. Sampling để mỗi Client tự gọi và tự trả phí token của họ, nên Server không cần API key, không tích hợp trực tiếp mô hình và không gánh chi phí AI cho từng người dùng.
Hàm nào dùng để yêu cầu sinh văn bản phía Server trong Sampling?
Phía Server, tool gọi ctx.session.create_message(...) với danh sách messages (SamplingMessage), max_tokens và system_prompt. Hàm này không tự gọi Claude mà gửi yêu cầu để Client thực thi; phía Client phải đăng ký một sampling_callback (truyền vào ClientSession) — đây mới là nơi thực sự gọi Claude qua Anthropic SDK.
Logging và Progress notifications trong MCP dùng để làm gì?
Chúng gửi log và tiến độ real-time từ MCP Server về Client trong lúc một tool chạy lâu, để người dùng thấy đang có gì diễn ra thay vì màn hình đứng im. Phía Server dùng context.info() để gửi log và context.report_progress (current, total) để cập nhật tiến độ; phía Client đăng ký logging_callback (ở ClientSession) và progress_callback (ở từng call_tool). Tính năng này hoàn toàn tùy chọn, chỉ để cải thiện trải nghiệm.
Roots trong MCP là gì?
Roots là cơ chế cấp cho MCP Server quyền truy cập các file/thư mục cụ thể trên máy local, đồng thời cho Claude ngữ cảnh để tự tìm file (qua list_roots và read_dir) mà người dùng không phải gõ đường dẫn đầy đủ. Roots vừa là ranh giới bảo mật vừa là bản đồ tìm kiếm. Lưu ý quan trọng: MCP SDK không tự thực thi ranh giới này — lập trình viên phải tự kiểm tra, thường bằng một hàm như is_path_allowed() gọi trước mọi thao tác file.
Stdio transport trong MCP hoạt động thế nào?
Transport là kênh truyền mà message JSON của MCP thật sự đi qua. Với stdio transport, client khởi chạy MCP server như một subprocess rồi giao tiếp qua luồng vào/ra chuẩn: client gửi message vào stdin của server, server trả lời bằng cách ghi ra stdout. Ống được chọn theo hướng đi chứ không theo vai trò message, nên cả hai bên đều có thể gửi bất cứ lúc nào. Hạn chế: stdio chỉ dùng được khi client và server chạy trên cùng một máy.
Một kết nối MCP bắt đầu bằng những message nào?
Mọi kết nối MCP phải mở đầu bằng đúng ba message theo thứ tự: Initialize Request (client gửi trước), Initialize Result (server trả lời kèm capabilities), rồi Initialized Notification (client xác nhận, không cần hồi đáp). 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.
Streamable HTTP transport khác stdio thế nào và có hạn chế gì?
Streamable HTTP cho phép MCP client kết nối tới server host từ xa qua HTTP, mở đường cho public MCP server — khác stdio vốn bắt client và server cùng một máy. Hạn chế nằm ở bản chất HTTP: client dễ gửi request tới server (vì server có URL đã biết), nhưng server không thể chủ động gửi request tới client vì client không có URL. Vì vậy các message do server khởi xướng (Create Message của sampling, List Roots) và các notification (Progress, Logging, Initialized, Cancelled) khó triển khai trên HTTP thuần.
StreamableHTTP lách giới hạn của HTTP bằng cách nào?
Bằng Server-Sent Events (SSE). Sau khi khởi tạo, client gửi một GET request để mở một SSE connection — một HTTP response sống lâu mà server có thể stream message về client bất cứ lúc nào, nhờ đó khôi phục chiều server→client. Khi gọi tool, hệ thống dùng hai kết nối: Primary SSE (mở vô hạn, cho request do server khởi xướng và progress notification) và Tool-specific SSE (mở cho mỗi tool call, mang logging message cùng Call Tool Result, tự đóng khi result được gửi). Mọi request sau khởi tạo đều phải kèm header mcp-session-id.
stateless_http và json_response ảnh hưởng gì tới MCP server?
Đây là hai setting của streamable HTTP transport, mặc định đều là false. Một số kịch bản deploy buộc phải bật lên true, và khi đó 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 như sampling. Bật chúng nghĩa là yêu cầu transport hoạt động trong ràng buộc của HTTP thay vì lách qua ràng buộc đó. Nếu ứng dụng chạy tốt với stdio ở local nhưng vỡ khi deploy qua HTTP, đây thường là thủ phạm.
Khi nào nên bật stateless_http cho MCP server?
Khi cần scale ngang với load balancer, không cần giao tiếp server→client, tool không cần sampling, và muốn giảm overhead kết nối. Lý do: client MCP mở hai kết nối (GET SSE và POST), load balancer có thể đẩy chúng tới hai instance khác nhau, khiến instance xử lý POST phải phối hợp với instance giữ SSE. Bật stateless_http xoá bài toán đó nhưng mất session ID, request server→client, sampling, progress report và subscriptions — đổi lại client không còn phải khởi tạo.
stateless_http và json_response có bật cùng lúc được không?
Có. Chúng là hai flag độc lập điều khiển hai thứ khác nhau, không loại trừ nhau — ví dụ của khoá bật cả hai cùng lúc: FastMCP("mcp-server", stateless_http=True, json_response=True). stateless_http cắt đường GET SSE nên mất session ID, request server→client, sampling, progress và subscriptions (đổi lại khỏi cần initialization); json_response chỉ tắt streaming cho POST response nên mất log và message tiến độ trung gian, chỉ còn kết quả cuối. Bật cả hai thì MCP server tụt xuống mức request/response JSON thuần không trạng thái: scale tối đa nhưng chức năng tối thiểu.
Nên dev bằng stdio rồi deploy bằng HTTP không?
Nên test bằng đúng transport sẽ dùng ở production. Nếu dev local bằng stdio nhưng deploy bằng HTTP transport, khác biệt hành vi giữa chế độ stateful và stateless có thể rất lớn — bắt lỗi lúc phát triển vẫn hơn phát hiện sau khi deploy. Đây là nguyên nhân thường gặp của triệu chứng chạy tốt ở local nhưng vỡ trên production.
MCP có những loại message nào?
MCP giao tiếp bằng JSON (chuẩn JSON-RPC 2.0), chia thành hai nhóm. Request–Result messages đi theo cặp và có phản hồi, ghép với nhau bằng trường id — ví dụ Call Tool Request → Call Tool Result, hay Initialize Request → Initialize Result. Notification messages là một chiều, chỉ báo sự kiện và không cần phản hồi — ví dụ Progress, Logging Message, Tool List Changed, Resource Updated. MCP là giao thức hai chiều: cả client lẫn server đều có thể chủ động gửi message, điều này quan trọng khi chọn transport.

Đề thi thử (57 câu)

Đề thi thử tự biên soạn, bám sát đề thi chứng chỉ thật — trích 20 câu đầu dưới đây. Bản tương tác — chấm điểm, đáp án & giải thích từng câu — nằm ở tab “Đề thi thử” trên trang.

  1. Sampling What does MCP sampling allow a server to do?

    • A. Directly call Claude using the server's own API key
    • B. Access a language model like Claude through the connected MCP client
    • C. Randomly select which tool to run next
    • D. Stream tokens faster by caching model responses on the server
  2. Sampling With sampling, which party bears the cost of token usage?

    • A. The MCP server
    • B. The MCP client
    • C. Anthropic covers it for public servers
    • D. The cost is split evenly between client and server
  3. Sampling Why is sampling especially valuable for publicly accessible MCP servers?

    • A. It makes the server respond faster to many concurrent users
    • B. It lets each client pay for its own AI usage, so the server does not rack up unlimited costs
    • C. It hides the server's tools from unauthorized users
    • D. It allows the server to run without an internet connection
  4. Sampling On the SERVER side, which function is used inside a tool to request text generation via sampling?

    • A. ctx.session.create_message(...)
    • B. anthropic.messages.create(...)
    • C. ctx.sample(...)
    • D. sampling_callback(...)
  5. Sampling On the CLIENT side, how is sampling wired up so the client can handle the server's requests?

    • A. By passing a sampling_callback when initializing the ClientSession
    • B. By decorating a function with @mcp.sampling
    • C. By setting an ANTHROPIC_API_KEY environment variable on the server
    • D. By calling session.enable_sampling() after initialize()
  6. Sampling Which statement about the server's create_message call is TRUE?

    • A. It directly calls Claude and returns the completion synchronously
    • B. It requires the server to store an Anthropic API key
    • C. It sends a sampling request that the client fulfills; the server itself does not call Claude
    • D. It can only be used outside of tool functions
  7. Sampling In the summarize tool example, what does the server do with result.content when result.content.type == 'text'?

    • A. Raises ValueError('Sampling failed')
    • B. Returns result.content.text
    • C. Forwards it to another MCP server
    • D. Caches it and returns an empty string
  8. Sampling Which is a genuine benefit of sampling for the MCP server author? (Select all that apply)

    • A. The server does not need to integrate directly with a language model
    • B. The server does not need Claude API credentials
    • C. The token cost shifts to the client
    • D. The server gains full control over which model and parameters the client uses
  9. Logging & Progress In the Python MCP SDK, how does a tool send logging and progress notifications back to the client?

    • A. By raising special exceptions the client catches
    • B. Through methods on the Context argument passed to the tool function
    • C. By writing to a shared log file the client tails
    • D. By returning a dict with a 'progress' key from the tool
  10. Logging & Progress Which Context method reports progress, and what arguments does it take?

    • A. context.info(message)
    • B. context.report_progress(current, total)
    • C. context.progress(percentage)
    • D. context.update(step, message)
  11. Logging & Progress On the client side, where are the logging callback and the progress callback registered, respectively?

    • A. Both are passed to ClientSession(...)
    • B. Both are passed to call_tool(...)
    • C. logging_callback on ClientSession(...); progress_callback on call_tool(...)
    • D. logging_callback on call_tool(...); progress_callback on ClientSession(...)
  12. Logging & Progress A progress callback has signature (progress, total, message). What does total being None indicate?

    • A. The operation has failed
    • B. There is no known total, so a percentage cannot be computed — report raw progress instead
    • C. Progress is complete
    • D. Logging is disabled for this call
  13. Logging & Progress Which statement about logging and progress notifications is TRUE?

    • A. They are mandatory for every MCP tool to function
    • B. They change the tool's return value based on progress
    • C. They are entirely optional and are purely a user-experience enhancement
    • D. They can only be used in web applications, not CLI
  14. Logging & Progress For a WEB application, which mechanisms are appropriate for pushing MCP progress updates to the browser? (Select all that apply)

    • A. WebSockets
    • B. Server-sent events (SSE)
    • C. Polling
    • D. Printing to the server's terminal only
  15. Roots What are MCP roots primarily used for?

    • A. To speed up the language model's token generation
    • B. To grant an MCP server access to specific files and folders on the local machine, and give Claude context to find files
    • C. To store the server's API keys securely
    • D. To define which tools a server exposes
  16. Roots Without roots, what problem occurs when a user says 'convert biking.mp4 to mov'?

    • A. Claude converts the wrong file format
    • B. The server runs out of memory
    • C. Claude only has the filename and has no way to search the filesystem to find where the file actually lives
    • D. The tool automatically deletes the original file
  17. Roots In the roots workflow, which two calls does Claude use to locate a file within accessible directories?

    • A. list_tools and call_tool
    • B. list_roots and read_dir
    • C. create_message and sampling_callback
    • D. info and report_progress
  18. Roots Does the MCP SDK automatically enforce root restrictions?

    • A. Yes, any path outside a root is blocked automatically
    • B. No — you must implement enforcement yourself, e.g. an is_path_allowed() helper called before file operations
    • C. Yes, but only for read operations
    • D. Only if you enable a strict_mode flag on the server
  19. Roots If a server is only granted a root for the Desktop folder, what happens when Claude tries to access a file in Documents?

    • A. It silently succeeds because roots are advisory only
    • B. It gets an error and can inform the user the file isn't accessible with the current server configuration
    • C. The Documents folder is automatically added as a new root
    • D. The server crashes
  20. Roots Which are stated benefits of roots? (Select all that apply)

    • A. Users don't need to provide full file paths
    • B. Claude only searches approved directories, making file discovery faster
    • C. Prevents accidental access to sensitive files outside approved areas
    • D. Roots can be provided through tools or injected directly into prompts

…và 37 câu nữa trong bản đề thi thử đầy đủ (57 câu) — mở tab “Đề thi thử” trên trang để làm toàn bộ, có chấm điểm & giải thích.