Tưởng tượng bạn build MCP server research-pro — expose tool research(topic) fetch 20 nguồn web, summarize thành báo cáo. Tool cần LLM để summarize, và bạn đã lập trình gọi Claude API trực tiếp trong server code.
- Giải thích được vấn đề cost & credentials mà sampling giải quyết.
- Mô tả flow sampling: server generate prompt → request → client call model → return → server dùng.
- Phân biệt 2 pattern "server tự call model" vs "server sampling qua client" và trade-off của mỗi cái.
- Implement sampling trên cả server Python và client bằng Anthropic SDK.
- Biết khi nào không nên dùng sampling (và dùng cái gì thay thế).
Vấn đề sampling giải quyết — 2 cách xử lý LLM trong tool
Khi tool của bạn cần dùng LLM (summarize, extract, transform...), có 2 cách:
Flow sampling:
Điểm mấu chốt: client-side API key, client-side bill. Server chỉ biết prompt và kết quả text.
- Client gọi tool research(topic).
- Server fetch data, build prompt để summarize.
- Server gửi sampling/createMessage request tới client với prompt đó.
- Client dùng API key của chính mình (Anthropic/OpenAI/...) để call model.
- Client return kết quả (text generated) về server qua sampling response.
- Server dùng text đó làm step tiếp theo của tool.
- Server return final CallToolResult cho client.
┌─────────────────────────────────────────────────────────────┐ │ │ │ OPTION 1: Server tự call LLM │ │ ───────────────────── │ │ │ │ ┌────────┐ tool call ┌────────┐ │ │ │ Client │ ────────────▶ │ Server │ │ │ └────────┘ └───┬────┘ │ │ │ │ │ │ call Claude API │ │ │ (server's API key) │ │ ▼ │ │ ┌──────────┐ │ │ │ Claude │ │ │ │ (API) │ │ │ └──────────┘ │ │ │ │ ❌ Server phải có API key │ │ ❌ Server trả tiền token │ │ ❌ Server phải manage rate limit, retry │ │ ❌ Khó cho public server │ │ │ │ │ │ OPTION 2: Sampling (server mượn client's model) │ │ ────────────────────────────────────────── │ │ │ │ ┌────────┐ tool call ┌────────┐ │ │ │ Client │ ────────────▶ │ Server │ │ │ └────────┘ └───┬────┘ │ │ ▲ │ │ │ │ sampling request │ │ │ │ "please call model" │ │ │ ▼ │ │ │ ┌────────┐ │ │ │ │ Client │ │ │ │ │ (has │ sampling resp. │ │ │ │ API │ ◀───────────────── │ │ │ key) │ │ │ └───┬────┘ │ │ │ │ │ │ call Claude │ │ ▼ │ │ ┌──────────┐ │ │ │ Claude │ │ │ └──────────┘ │ │ │ │ ✅ Server KHÔNG cần API key │ │ ✅ Client trả tiền token │ │ ✅ Perfect cho public server │ │ │ └─────────────────────────────────────────────────────────────┘
Benefits — Ngoài cost còn gì?
1. Cost burden shift
Server không ôm cost → public server sustainable. Đây là benefit số 1.
2. No API key ở server
Security benefit lớn. Server không có secret → không lo leak. Dù server bị compromise, attacker không có access Claude API.
3. User toàn quyền kiểm soát
4. Recursive pattern — "MCP agents"
Đây là benefit subtle nhưng powerful. Từ video "MCP 201" của David Soria Parra:
Sampling cho phép chain MCP server — server A gọi server B, B cần LLM → bubble request lên tới client gốc. Client là single source of truth cho mọi LLM call trong chain. Đây là foundation cho agentic workflow kết nối nhiều MCP server.
5. No vendor lock-in
Server không hard-code "phải dùng Claude". Client quyết định dùng model nào. Server của bạn portable across ecosystems.
- Model: client có thể dùng Claude Sonnet, Opus, hoặc thậm chí model khác (OpenAI, Gemini). Server không biết.
- Privacy: client toàn quyền xem prompt trước khi forward, block nếu sensitive.
- Subscription: client có Claude Pro subscription → subscription đó cover cost.
Implementation — Server side
Dùng ctx.session.create_message() trong tool:
Dissect
1. SamplingMessage có cấu trúc giống Anthropic API
from mcp.server.fastmcp import FastMCP, Context
from mcp.types import SamplingMessage, TextContent
from pydantic import Field
mcp = FastMCP(name="summary-tool")
@mcp.tool()
async def summarize(
text_to_summarize: str = Field(description="Text to summarize"),
*,
ctx: Context,
) -> str:
# Build prompt
prompt = f"""Please summarize the following text in 3 bullet points:
{text_to_summarize}
"""
# Request sampling từ client
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",
)
# Dùng kết quả
if result.content.type == "text":
return result.content.text
else:
raise ValueError("Sampling returned non-text content")
if __name__ == "__main__":
mcp.run()Dissect
Có thể đa round: gửi nhiều message với role alternating user/assistant cho conversation context.
2. Parameters optional
SamplingMessage(
role="user", # hoặc "assistant"
content=TextContent(type="text", text=prompt)
)Implementation — Server side (tiếp)
model_preferences là hint. Client không bắt buộc tuân theo. Pattern phổ biến:
await ctx.session.create_message(
messages=[...],
max_tokens=4000, # required
system_prompt="...", # optional
temperature=0.7, # optional
model_preferences=..., # optional — hint model nào client nên dùng
include_context="thisServer", # optional — request context từ MCP
stop_sequences=["---"], # optional
)Implementation — Server side (tiếp)
3. Error handling
Nếu client từ chối sampling (không implement, hoặc user bấm deny), create_message sẽ raise exception. Tool nên catch và graceful fallback:
model_preferences={"hints": [{"name": "claude-3-5-sonnet"}]}Implementation — Server side (tiếp)
try:
result = await ctx.session.create_message(...)
except Exception as e:
await ctx.warning(f"Sampling unavailable: {e}. Using fallback.")
return fallback_summary(text)Implementation — Client side
Client đăng ký sampling_callback khi tạo session. Callback sẽ được gọi khi server emit sampling/createMessage.
Dissect
1. Callback signature
import asyncio
from anthropic import AsyncAnthropic
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp import ClientSession, RequestContext
from mcp.types import (
CreateMessageRequestParams,
CreateMessageResult,
TextContent,
)
anthropic = AsyncAnthropic() # dùng ANTHROPIC_API_KEY env var
async def sampling_callback(
context: RequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:
# Convert MCP messages format → Anthropic format
messages = []
for m in params.messages:
messages.append({
"role": m.role,
"content": [{"type": "text", "text": m.content.text}]
})
# Gọi Claude
response = await anthropic.messages.create(
model="claude-sonnet-5", # model client chọn
max_tokens=params.max_tokens,
system=params.system_prompt or "",
messages=messages,
)
# Extract text response
text = response.content[0].text
# Return result cho server
return CreateMessageResult(
role="assistant",
model="claude-sonnet-5",
content=TextContent(type="text", text=text),
)
async def run():
server_params = StdioServerParameters(command="uv", args=["run", "server.py"])
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write,
sampling_callback=sampling_callback, # ← register
) as session:
await session.initialize()
result = await session.call_tool(
name="summarize",
arguments={"text_to_summarize": "Long text here..."},
)
print(result.content[0].text)
if __name__ == "__main__":
asyncio.run(run())Dissect
Return type strict. SDK validate.
2. Convert format
MCP messages ≈ Anthropic messages (cả 2 đều JSON role/content). Conversion thường chỉ là structural mapping.
3. Dùng Anthropic SDK
Client có thể dùng bất kỳ SDK LLM nào. Ở đây Anthropic. Có thể swap sang OpenAI:
async def sampling_callback(
context: RequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:Implementation — Client side (tiếp)
Server hoàn toàn không biết client dùng model gì. Server-client decouple ở đây.
from openai import AsyncOpenAI
openai = AsyncOpenAI()
async def sampling_callback(context, params):
response = await openai.chat.completions.create(
model="gpt-4",
messages=[{"role": m.role, "content": m.content.text} for m in params.messages],
max_tokens=params.max_tokens,
)
text = response.choices[0].message.content
return CreateMessageResult(role="assistant", model="gpt-4", content=TextContent(type="text", text=text))Khi nào dùng sampling — Và khi nào không
✅ Dùng khi
❌ Không dùng khi
- MCP server public, nhiều user — tránh ôm cost.
- User có subscription LLM — leverage cái họ đã trả.
- Tool có thể dùng nhiều model khác nhau — không lock vào Claude/OpenAI.
- Chain MCP server — cần bubble LLM call lên cao nhất.
- Internal tool, số user nhỏ — dùng server API key đơn giản hơn.
- Tool cần model rất cụ thể (vd fine-tuned model của bạn) — server tự call hợp lý hơn.
- Client không support sampling — dùng server-side call hoặc fallback.
- Latency critical — sampling qua client thêm 1 hop, latency cao hơn server tự call.
- Prompt chứa data nhạy cảm mà client không nên thấy (rare, thường không issue vì client là user chính chủ).
Client support matrix
Tính đến 2026, không phải client nào cũng support sampling:
Trước khi feature depend on sampling, bạn nên try/except graceful thay vì check capability tĩnh (SDK chưa expose capability API nhất quán giữa các version):
Một số SDK version có expose ctx.session.check_client_capability(...) hoặc property tương tự — consult docs của SDK version bạn đang dùng.
| Client | Hỗ trợ sampling? |
|---|---|
| Claude Desktop | ✅ (via Anthropic account của user) |
| Claude Code | ✅ |
| Cursor | ⚠️ Partial (kiểm tra version) |
| Cline | ⚠️ Partial |
| Custom SDK client | ✅ (nếu bạn viết callback) |
| MCP Inspector | ✅ (for testing) |
# Server side — graceful handling
try:
result = await ctx.session.create_message(...)
return result.content.text
except Exception as e:
await ctx.warning(f"Sampling not available: {e}")
return "Sampling not supported by client. Falling back to plain text."Ví dụ theo ngành
🔍 Public research MCP — 10k+ concurrent users
Pain: Server nhận 10k request/phút, mỗi request gọi LLM để summarize. Server API bill $50k/tháng.
Giải pháp sampling:
💰 Finance MCP — Summarize reports
Pain: Tool summarize_earnings_call cần LLM. Triển khai enterprise, mỗi khách hàng có budget model khác nhau.
Giải pháp:
🛠️ Developer tool — explain_error trong IDE
Pain: MCP server IDE plugin. Tool explain_error(stack_trace) cần LLM.
Giải pháp:
🎧 Customer support MCP — Nội bộ công ty
Pain: Tool analyze_ticket_sentiment. 200 CS agent dùng.
Giải pháp:
Moral: sampling là tool, không phải always-on solution.
- Server chỉ fetch web và build prompt.
- Client (Claude Desktop của user) tự gọi Claude với subscription của họ.
- Kết quả: Server bill $500/tháng (infra only). User happy vì không có friction signup.
- Dùng sampling → client (Bloomberg terminal, Excel plugin) dùng subscription model của công ty họ.
- Công ty A dùng Claude, B dùng GPT, C dùng self-hosted LLM.
- Kết quả: Server neutral, công ty nào cũng dùng được, không lock-in.
- Sampling → IDE (Cursor/Cline) dùng Anthropic key của developer.
- Developer đã trả Claude Pro, không mất thêm.
- Kết quả: Tool free cho user IDE plugin, chỉ cần họ có Claude Pro.
- KHÔNG dùng sampling — cost predictable, server tự call với company API key tiện hơn.
- Server-side key stored in secret manager.
- Kết quả: Internal tool simple, không phải lo về subscription model của từng agent.
Anti-patterns
❌ Sampling cho internal tool
Hiện tượng: Dùng sampling khi chỉ có 20 user nội bộ. Mỗi user phải cài API key, onboard friction cao.
Cách đúng: Internal → server-side key. Sampling cho public use case.
❌ Prompt sampling quá verbose
Hiện tượng: Mỗi sampling request 10k token context → user pay tiền nhiều, tool chậm.
Cách đúng:
❌ Không handle khi client không support
Hiện tượng: Server crash khi gọi create_message với client không implement sampling.
Cách đúng: Check capability trước, graceful fallback.
❌ Expose sensitive data qua sampling
Hiện tượng: Server send prompt chứa PII, medical data → client thấy được (dù user là owner data, vẫn vi phạm compliance).
Cách đúng:
❌ Dùng sampling để làm toàn bộ tool logic
Hiện tượng: Tool chỉ là wrapper "send prompt X, return result" — thực ra là prompt template, không tool thật.
Cách đúng: Nếu tool chỉ là prompt, dùng MCP prompts primitive thay vì tool. Tools là actions (có side effect), không phải rephrased prompt.
❌ Không set max_tokens hợp lý
Hiện tượng: max_tokens=100000 cho mỗi sampling → user pay rất nhiều dù output ngắn.
Cách đúng: Estimate output length reasonable (thường 500-4000). Chỉ cao khi thực sự cần.
- Compress prompt trước sampling.
- Dùng cached prompt nếu giống nhau giữa các call.
- Cân nhắc cache kết quả sampling server-side (với permission).
- Redact PII trước khi put vào prompt.
- Audit log sampling request.
- Tôn trọng data residency: sampling có thể đi qua client ở region khác.
Mẹo nâng cao
Mẹo 1: Model preferences cho quality control
Dùng Pydantic types ModelPreferences + ModelHint thay vì raw dict để tránh validation error:
Client có thể dùng hint để chọn tier model phù hợp (Sonnet vs Haiku). Hint là gợi ý, không bắt buộc — client toàn quyền ignore.
Mẹo 2: Multi-turn sampling
Gửi conversation nhiều turn để provide context:
from mcp.types import ModelPreferences, ModelHint
await ctx.session.create_message(
messages=[...],
max_tokens=2000,
model_preferences=ModelPreferences(
hints=[ModelHint(name="claude-sonnet-5")],
intelligence_priority=0.8,
cost_priority=0.2,
speed_priority=0.5,
),
)Mẹo 2: Multi-turn sampling
Pattern này useful cho chain-of-thought hoặc few-shot learning.
Mẹo 3: include_context parameter
Một số SDK support parameter include_context:
messages = [
SamplingMessage(role="user", content=TextContent(type="text", text="Context: ...")),
SamplingMessage(role="assistant", content=TextContent(type="text", text="Understood.")),
SamplingMessage(role="user", content=TextContent(type="text", text="Now do X...")),
]Mẹo 3: include_context parameter
Cho phép sampling tham khảo đến resources/prompts của server. Experimental nhưng powerful.
Mẹo 4: Cache sampling response
Nếu prompt giống nhau → kết quả sampling có thể cache:
await ctx.session.create_message(
messages=[...],
include_context="thisServer", # include context từ current MCP session
)Mẹo 4: Cache sampling response
Cẩn thận: cache phải scope per-user nếu prompt có user data.
Mẹo 5: Monitor sampling latency
Sampling = 3 network hops (server → client → LLM → client → server). Monitor p95/p99 để catch slow client side.
cache_key = hash(prompt + system_prompt)
if cached := await redis.get(cache_key):
return cached
result = await ctx.session.create_message(...)
await redis.set(cache_key, result.content.text, ex=3600)
return result.content.textÁp dụng ngay
Bài tập 1: Build sampling tool đơn giản (30 phút)
Bước 1: Tạo server với tool translate(text, target_language):
Bước 2: Viết client với sampling_callback dùng Anthropic SDK.
Bước 3: Chạy: translate "Hello world" sang Vietnamese.
Bước 4: Observe:
Bài tập 2 (optional): A/B test 2 pattern
Viết cùng tool với 2 cách: (a) sampling, (b) server tự call. Đo:
Present trade-off cho team bằng 1 slide.
- Thời gian tool call: ___________
- Tokens used (từ Anthropic response): ___________
- Có latency chậm hơn server tự call không? ___________
- Latency p50/p95.
- Code complexity (dòng code, deps).
- Failure mode (khi client không support).
@mcp.tool()
async def translate(
text: str,
target_language: str,
*,
ctx: Context
) -> str:
prompt = f"Translate the following to {target_language}. Respond with ONLY the translation:\n\n{text}"
result = await ctx.session.create_message(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=2000,
)
return result.content.textTóm tắt bài học
🎯 Sampling = server xin client gọi LLM — đảo ngược flow thông thường.
🎯 Cost burden shift — server không trả tiền token, client trả. Critical cho public MCP.
🎯 Server không cần API key — security & simplicity benefit.
🎯 Recursive pattern — sampling bubble qua chain MCP server, client gốc là root authority.
🎯 Không phải client nào cũng support — check capability trước khi depend.
🎯 Không dùng cho internal tool có trust boundary clear — server-side key đơn giản hơn.
🎯 Sampling = tool, không phải silver bullet — chọn dựa vào use case, không phải default.
- MCP Sampling Spec
- Transcript: MCP 201 — Code w/ Claude (David Soria Parra) — 11:50 về sampling
- Anthropic SDK Python — để implement client callback
- MCP Python SDK examples/sampling