Lộ trình học›MCP›MCP: Advanced Topics Mở bản đồ

10.8 · Sampling — Khi server "mượn" model của client

Bài viếtTrung cấp → Nâng25 phút đọc

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.

Sau bài này, bạn sẽ
  1. Giải thích được vấn đề cost & credentials mà sampling giải quyết.
  2. Mô tả flow sampling: server generate prompt → request → client call model → return → server dùng.
  3. Phân biệt 2 pattern "server tự call model" vs "server sampling qua client" và trade-off của mỗi cái.
  4. Implement sampling trên cả server Python và client bằng Anthropic SDK.
  5. 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:

  1. Tắt serveruser mất tool thú vị.
  2. 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:

  1. Client gọi tool research(topic).
  2. Server fetch data, build prompt để summarize.
  3. Server gửi sampling/createMessage request tới client với prompt đó.
  4. Client dùng API key của chính mình (Anthropic/OpenAI/...) để call model.
  5. Client return kết quả (text generated) về server qua sampling response.
  6. Server dùng text đó làm step tiếp theo của tool.
  7. Server return final CallToolResult cho 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:

ClientHỗ trợ sampling?
Claude Desktop(via Anthropic account của user)
Claude Code
CursorPartial (kiểm tra version)
ClinePartial
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_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:

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

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_callback dù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

Những điều cốt lõi
  • 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

Kiểm tra nhanh2 câu hỏi · chọn đáp án để xem giải thích
    Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.

    Khóa học cùng lộ trình