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 cho tool chạy lâu.

  • 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.
  • Cơ chế này chuyển gánh nặng chi phí và credential sang Client: Server không cần API key, Client (vốn đã kết nối sẵn tới Claude) mới là bên gọi và trả tiền.
  • 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.

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

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. 🟡 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. 🟢 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.


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.

Đề thi thử (14 câu)

Đề thi thử tự biên soạn, bám sát đề thi chứng chỉ thật. 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