- 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;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).
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ó.
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ử".