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ế).
Mở đầu: Một MCP server public, và hóa đơn Claude API khổng lồ
Bạn open-source nó, đăng lên MCP registry. Tuần đầu, 50 user. Tuần thứ 2, 500 user. Tuần thứ 3, 5,000 user — hóa đơn Anthropic API report $3,800 cho tuần đó.
Bạn có 2 lựa chọn:
- Tắt serveruser mất tool thú vị.
- Tính phí useryêu cầu họ signup, nhập credit card, đăng nhập... mất 95% user vì friction.
Cả 2 đều tệ. Đây là core problem của public MCP server dùng LLM: ai trả tiền?
Sampling là câu trả lời của MCP protocol: "Bạn không trả. Client (user) trả. Server chỉ gửi prompt, client dùng subscription Claude/OpenAI/... của mình để gọi model và trả kết quả cho server."
Đây là một trong những design decision thông minh nhất của MCP — nó cho phép MCP server public sustainable mà không cần các startup host MCP phải ôm cost AI.
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:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 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 │
│ │
└─────────────────────────────────────────────────────────────┘
Flow sampling:
- Client gọi tool
research(topic). - Server fetch data, build prompt để summarize.
- Server gửi
sampling/createMessagerequest 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
CallToolResultcho client.
Điểm mấu chốt: client-side API key, client-side bill. Server chỉ biết prompt và kết quả text.
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
- 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.
4. Recursive pattern — "MCP agents"
Đây là benefit subtle nhưng powerful. Từ video "MCP 201" của David Soria Parra:
You can take an MCP server that exposes a tool. But during the tool execution, you might want to use more MCP servers downstream. And somewhere downstream, there might be an issue-tracker server that wants to go and have a completion. But using sampling, you can bubble up the request such that the client always stays in full control...
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.
Implementation — Server side
Dùng ctx.session.create_message() trong tool:
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
1. SamplingMessage có cấu trúc giống Anthropic API
SamplingMessage(
role="user", # hoặc "assistant"
content=TextContent(type="text", text=prompt)
)
Có thể đa round: gửi nhiều message với role alternating user/assistant cho conversation context.
2. Parameters optional
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
)
model_preferences là hint. Client không bắt buộc tuân theo. Pattern phổ biến:
model_preferences={"hints": [{"name": "claude-3-5-sonnet"}]}
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:
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.
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-4-6", # 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-4-6",
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
1. Callback signature
async def sampling_callback(
context: RequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:
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:
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))
Server hoàn toàn không biết client dùng model gì. Server-client decouple ở đây.
Khi nào dùng sampling — Và khi nào 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.
Không dùng khi
- 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:
| 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) |
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):
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."
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.
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:
- 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.
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:
- 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.
Developer tool — explain_error trong IDE
Pain: MCP server IDE plugin. Tool explain_error(stack_trace) cần LLM.
Giải pháp:
- 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.
Customer support MCP — Nội bộ công ty
Pain: Tool analyze_ticket_sentiment. 200 CS agent dùng.
Giải pháp:
- 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.
Moral: sampling là tool, không phải always-on solution.
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:
- 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).
Không handle khi client không support
- Hiện tượng
- Server crash khi gọi
create_messagevớ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:
- 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.
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=100000cho 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.
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:
from mcp.types import ModelPreferences, ModelHint
await ctx.session.create_message(
messages=[...],
max_tokens=2000,
model_preferences=ModelPreferences(
hints=[ModelHint(name="claude-sonnet-4-6")],
intelligence_priority=0.8,
cost_priority=0.2,
speed_priority=0.5,
),
)
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:
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...")),
]
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:
await ctx.session.create_message(
messages=[...],
include_context="thisServer", # include context từ current MCP session
)
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:
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
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.
Á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):
@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.text
- Bước 2
- Viết client với
sampling_callbackdùng Anthropic SDK. - Bước 3
- Chạy: translate "Hello world" sang Vietnamese.
- Bước 4
- Observe:
- Thời gian tool call: ___________
- Tokens used (từ Anthropic response): ___________
- Có latency chậm hơn server tự call không? ___________
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:
- Latency p50/p95.
- Code complexity (dòng code, deps).
- Failure mode (khi client không support).
Present trade-off cho team bằng 1 slide.
Tó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.
Tài liệu tham khảo
- 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