Sampling là feature MCP tôi thấy ít người dùng nhất, dù nó là feature “lật ngược” thú vị nhất của protocol. Bình thường client gọi server. Sampling thì ngược: server hỏi client gọi LLM giùm. Lần đầu đọc spec tôi tưởng đây là gimmick. Sáu tháng sau khi build một MCP server cần enrichment data (server tóm tắt ticket Jira cho LLM đọc), tôi mới hiểu vì sao spec dành riêng một section.

Prompts thì phổ biến hơn nhưng cũng hay bị nhầm với slash command Claude Code. Hai cơ chế nhìn giống nhau, dùng được cho cùng use case, nhưng trade-off rất khác.

Bài này tách rõ hai entity còn lại của MCP: Prompts (template user trigger) và Sampling (server xin LLM call). Hai cơ chế cho phép MCP server tham gia sâu vào workflow, không chỉ là “ống dẫn data” thụ động.

Prompt resources là gì

Theo spec MCP 2025-06-18, Prompt là một template message có sẵn trên server, client lấy về và đưa vào conversation với LLM. Prompts được thiết kế là user-controlled: user chủ động chọn prompt nào để chạy, không phải LLM tự pick.

Hai khía cạnh đáng để ý. Prompt sống trên server, không hardcode trong client code, server update là mọi client connect vào đều thấy bản mới mà không phải push update. Và prompt nhận arguments (tham số) có cấu trúc, client điền arg vào trước khi dùng, server render ra message cuối, không phải string template raw.

Ví dụ một prompt code_review định nghĩa trên server:

{
  "name": "code_review",
  "title": "Request Code Review",
  "description": "Asks the LLM to analyze code quality and suggest improvements",
  "arguments": [
    {
      "name": "code",
      "description": "The code to review",
      "required": true
    }
  ]
}

Khi user trigger prompt này từ UI (ví dụ qua slash command trong Claude Desktop), client gửi prompts/get với argument code và nhận về một mảng messages đã render. Mảng messages đó được đẩy thẳng vào context của LLM như user đã tự gõ ra.

Đây là điểm tinh tế: prompt không phải tool call. LLM không “gọi” prompt. User pick prompt, MCP client gọi prompts/get, kết quả messages được prepend vào conversation. LLM chỉ thấy một loạt message như user gõ tay.

So sánh với slash command Claude Code

Nếu bạn quen Claude Code, bạn sẽ thấy MCP Prompts trông giống slash command. Đúng, nhưng có vài khác biệt quan trọng.

Slash command Claude Code là một file markdown trong .claude/commands/ (project) hoặc ~/.claude/commands/ (personal). Tên file thành tên command. Body file thành prompt được inject vào session khi bạn gõ /<name>. Có thêm $ARGUMENTS để substitute text sau lệnh slash.

Cụ thể đối chiếu:

Tiêu chíClaude Code slash commandMCP Prompt
Nơi định nghĩaFile .md trên disk của userTrên MCP server (có thể remote)
Cách phân phốiCommit vào repo hoặc copy thủ côngServer expose, mọi client connect đều thấy
Argument$ARGUMENTS (raw text sau lệnh)Structured args với type, required, completion
Auto-completeKhông có hỗ trợ chính thức cho argcompletion/complete API cho từng arg
Cross-clientChỉ Claude Code dùng đượcBất kỳ MCP client nào (Claude Desktop, custom agent, Continue, Zed)
Update cyclePush file mới, user pull vềServer update, push notifications/prompts/list_changed, client refresh tự động

Nói cách khác, slash command Claude Code mạnh ở phần personal/per-project, dễ hiểu, file-based. MCP Prompt mạnh ở phần cross-client + structured argument + dynamic update.

Nếu bạn chỉ dùng Claude Code, slash command đủ dùng. Nếu bạn muốn cùng một bộ prompt dùng được trong Claude Desktop trên Mac đồng nghiệp, Continue trên Windows của QA, và custom agent Python trên CI, thì MCP Prompt là lựa chọn đúng.

Một use case thực tế: team backend có 30 prompt chuẩn (review PR, generate test case, summary incident, viết changelog). Đặt vào một MCP server nội bộ. Mọi dev connect Claude Desktop hoặc Claude Code vào server đó. Khi prompt được sửa lại, không ai cần pull repo, chỉ restart client là có bản mới.

Parameter binding và completion

Phần argument trong MCP Prompt được spec định nghĩa kỹ. Mỗi arg có name, description, required. Khi client gọi prompts/get, nó truyền vào object arguments:

{
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "code": "def hello():\n    print('world')"
    }
  }
}

Server validate arg, render template, trả về messages. Lưu ý: server phải validate cả trước khi render (input arguments) và cả output (messages có chứa data nhạy cảm hay không).

Hay hơn nữa là API completion/complete. Khi user đang gõ giá trị cho arg, client có thể hỏi server “đề nghị giá trị nào”. Server trả về danh sách suggestion sorted theo relevance. Ví dụ user gõ language=py, server gợi ý python, pytorch, pyside.

Completion request:

{
  "method": "completion/complete",
  "params": {
    "ref": {
      "type": "ref/prompt",
      "name": "code_review"
    },
    "argument": {
      "name": "language",
      "value": "py"
    }
  }
}

Completion response:

{
  "result": {
    "completion": {
      "values": ["python", "pytorch", "pyside"],
      "total": 10,
      "hasMore": true
    }
  }
}

Cộng với context của các argument đã chọn trước (ví dụ đã chọn language=python, giờ gõ framework=fla thì server gợi ý flask), trải nghiệm gần như IDE autocomplete. Đây là điểm slash command Claude Code chưa có.

Tính năng completions là capability riêng. Server phải declare lúc initialize, client phải implement UI cho dropdown. Nếu một bên không support, fall back về nhập tay thủ công.

Sampling: server hỏi client gọi LLM

Đây là phần đảo chiều quan trọng nhất của MCP, và cũng là phần tôi mất nhiều thời gian nhất mới hiểu khi đọc spec lần đầu. Bình thường, flow là client gọi server (gọi tool, đọc resource, lấy prompt). Sampling đảo lại: server gửi request, client gọi LLM giùm, trả kết quả lại server.

Khái niệm trong spec: server thực hiện một sampling/createMessage request, client xử lý, gọi LLM, trả về completion.

Tại sao cần thế? Vì có những lúc server cần khả năng inference của LLM để hoàn thành task của nó, mà server không muốn (hoặc không thể) quản lý API key riêng.

Ví dụ: một MCP server cho database. Server có tool query_table thông thường. Nhưng server cũng muốn cung cấp một tool explain_schema_in_plain_english. Tool này cần một LLM để rewrite schema thành tiếng tự nhiên. Nếu server tự gọi OpenAI API, nó phải tự manage API key, tự bill, tự rate limit. Phức tạp.

Với sampling, server gửi sampling/createMessage đến client. Client (Claude Desktop chẳng hạn) đã có sẵn LLM session của user, đã có API key của user. Client gọi LLM, hỏi user approve, trả kết quả về server. Server không cần biết LLM nào được dùng, không cần API key, không cần bill ai.

{
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Explain this schema in plain English: ..."
        }
      }
    ],
    "modelPreferences": {
      "hints": [
        { "name": "claude-3-sonnet" }
      ],
      "intelligencePriority": 0.8,
      "speedPriority": 0.5
    },
    "systemPrompt": "You are a helpful assistant.",
    "maxTokens": 100
  }
}

Server không gọi model trực tiếp. Nó express preferences (hints + priority cost/speed/intelligence). Client final quyết chọn model nào. Nếu client là Claude Desktop, nó dùng Claude. Nếu là một client khác có Gemini, nó map hint claude-3-sonnet sang gemini-1.5-pro dựa trên capability tương đương.

Đây là điểm spec rất chú trọng abstraction: server không bị lock vào provider cụ thể. Client làm middleware.

Human-in-the-loop là yêu cầu

Spec nhấn mạnh rất mạnh: với sampling, luôn cần human in the loop. Lý do là trust và security. Server có thể là bên thứ ba mà user không hoàn toàn tin tưởng. Nếu server tự gọi LLM bằng API key của user (qua client), nó có thể prompt-inject để leak data, gửi spam, hoặc gây cost lớn.

Yêu cầu spec đưa ra cho client:

  1. Show UI rõ ràng cho sampling request, để user review
  2. Cho phép user edit prompt trước khi gửi đi
  3. Show response trước khi trả lại server
  4. Cho phép từ chối request, server nhận error code -1 “User rejected sampling request”

Trong Claude Desktop, khi MCP server initiate sampling, user thấy popup “Server X is asking to run a model query. Review and approve?”. Có nút view prompt, edit, approve, deny. Đây không phải UX nice-to-have mà là requirement security.

Nếu bạn build MCP server có dùng sampling, đừng giả định client sẽ auto-approve. Mọi sampling request có thể bị reject, hãy code defensive.

Use cases tôi đã thấy hoạt động

Use case rõ nhất cho Prompt là team data có ~20 prompt phân tích log Datadog, query Snowflake, viết runbook. Đặt vào MCP server team-prompts. Mọi engineer connect vào, gõ slash command để trigger. Khi DBA cập nhật template slow_query_analysis, không ai cần pull repo, restart client là có bản mới. Cùng pattern áp dụng cho onboarding: company có 50 prompt hướng dẫn cách viết PR description, cách review code, cách viết doc theo style guide, junior dev gõ /review_pr_description là có template ngay không cần nhớ.

Sampling thì sáng ở hai chỗ. Enrichment data: MCP server cho Jira có tool summarize_ticket, nhận ticket ID, đọc ticket, gọi sampling để LLM tóm tắt; server không cần API key OpenAI, user thấy popup approve trước khi summary chạy. Agentic flow: MCP server cho data warehouse có tool optimize_query nhận SQL chậm, gọi sampling để LLM gợi ý index hoặc rewrite, server không host model riêng và không bill ai.

Use case mạnh nhất là kết hợp cả hai. Server expose prompt incident_postmortem. Khi user trigger, server không chỉ trả về template; server còn fetch incident data từ monitoring, gọi sampling với data đó để generate sections, rồi compose thành document. Tất cả bằng LLM của client, có sự cho phép của user. Đây là kiểu workflow tôi thấy “đáng đầu tư MCP” nhất, vì nếu code thẳng vào client app thì mỗi client phải tự lo data fetching và prompt orchestration.

Code example đầy đủ

Đây là MCP server Python với cả Prompt và Sampling. Server có một prompt summarize_pr và một tool auto_summarize_pr dùng sampling để gọi LLM.

from mcp.server.fastmcp import FastMCP
from mcp.types import (
    Prompt, PromptArgument, PromptMessage, TextContent,
    SamplingMessage, ModelPreferences, ModelHint
)

mcp = FastMCP("pr-helper")

@mcp.prompt()
def summarize_pr(pr_diff: str, style: str = "concise") -> list[PromptMessage]:
    """Generate a summary for a PR diff."""
    instruction = (
        "Write a concise PR summary."
        if style == "concise"
        else "Write a detailed PR summary with rationale."
    )
    return [
        PromptMessage(
            role="user",
            content=TextContent(
                type="text",
                text=f"{instruction}\n\nDiff:\n{pr_diff}"
            )
        )
    ]

@mcp.tool()
async def auto_summarize_pr(pr_diff: str, ctx) -> str:
    """Auto-summarize using client's LLM via sampling."""
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"Summarize this PR diff in 3 bullets:\n{pr_diff}"
                )
            )
        ],
        model_preferences=ModelPreferences(
            hints=[ModelHint(name="claude-3-sonnet")],
            intelligence_priority=0.7,
            speed_priority=0.6,
        ),
        max_tokens=300,
    )
    return result.content.text

if __name__ == "__main__":
    mcp.run()

Hai điểm cần lưu ý:

  1. @mcp.prompt() đăng ký prompt summarize_pr. Client gọi prompts/get với args pr_diff, style. Server render và trả message. User chủ động chọn prompt qua UI, không phải LLM auto-pick.
  2. @mcp.tool() đăng ký tool auto_summarize_pr. Tool này dùng ctx.session.create_message để initiate sampling. Client (qua Claude Desktop) sẽ hỏi user approve, gọi LLM, trả kết quả. Tool trả về string text cuối.

Khi build server, declare capability tương ứng:

# server capability auto declared by FastMCP
{
  "capabilities": {
    "prompts": { "listChanged": true },
    "tools": { "listChanged": true }
  }
}

# client phải khai báo sampling capability để server có thể request
{
  "capabilities": {
    "sampling": {}
  }
}

Nếu client không declare sampling, server không thể gọi create_message. Code defensive bằng cách check trước, fall back về error rõ ràng nếu client không support.

Khi nào dùng cái nào

Quy tắc tôi đang dùng đơn giản. Cần expose một template user pick để inject vào conversation thì dùng Prompt. Cần server tự gọi LLM để hoàn thành task của nó mà không muốn quản API key thì dùng Sampling. Cần cả hai (template có sẵn + dynamic generation) thì kết hợp: Prompt nhận argument render base template kèm chỉ dẫn cho LLM, hoặc Prompt expose ra, Tool gọi Sampling, kết quả compose thành conversation. Hai cơ chế ráp được vào nhau, không xung đột.

Hai cảnh báo từ kinh nghiệm của tôi. Đừng dùng Sampling chỉ để “make it AI-powered” cho dramatically. Mỗi request sampling đều cần user approve, mỗi approve làm gãy flow. Nếu logic deterministic làm được, dùng deterministic. Đừng dùng Prompt thay slash command cho per-user task chỉ một mình bạn dùng; slash command Claude Code đơn giản hơn nhiều, MCP Prompt chỉ đáng đầu tư khi cross-client hoặc cross-team.

Một câu kết

Quan điểm tôi đứng vững sau khi build Prompt + Sampling vào vài server thật: Prompt là feature dễ underestimate vì nhìn giống slash command, còn Sampling là feature dễ overestimate vì tên nghe ngầu. Sự thật ngược lại. Prompt valuable hơn nhiều người nghĩ khi team đông và workflow lặp lại. Sampling lại bị friction approve làm gãy flow, dùng đúng chỗ thì sáng, dùng sai chỗ thì phiền.

Sau khi đã có cả ba entity và biết kết hợp, phần còn lại là đưa server lên cho người khác dùng và tự bảo vệ khi public.

Reference dùng cho bài này: