Introduction
In standard MCP flow, the host sends tool calls to the server. Sampling inverts this: the server requests the client to perform an LLM completion. This enables servers to leverage AI capabilities without embedding their own LLM, and it opens the door to human-in-the-loop workflows where the host can require user approval before executing destructive operations.
Key Concepts
- Sampling / createMessage: The
sampling/createMessagecapability allows an MCP server to request that the host's LLM generate a completion. The server sends a messages array and parameters, and the host returns the LLM's response. This is the inverse of normal tool calling. - Server-Initiated LLM Requests: Instead of the LLM calling the server's tools, the server asks the LLM for help. Use cases include summarizing large datasets before returning results, classifying incoming data, or generating human-readable reports from raw data.
- Human-in-the-Loop (HITL): The host controls whether to fulfill sampling requests. It can display an approval UI to the user, modify the request, or reject it entirely. This ensures that the human remains in control, especially for sensitive operations.
- Tool Annotations: Tools can declare behavioral hints via annotations:
title(human-readable display name),readOnlyHint(only reads),destructiveHint(modifies state),idempotentHint(safe to retry with same args), andopenWorldHint(interacts with external systems). Hosts use these to decide which operations need user confirmation. - Confirmation Workflows: For destructive operations, combine tool annotations with sampling to build explicit confirmation flows: the server requests the LLM to explain what it is about to do, the host shows this to the user, and the user approves or rejects.
- Elicitation: A capability that lets servers request direct input from users (not LLM completions). Supports form mode (structured data collection via rendered UI) and URL mode (opening a URL for OAuth flows or external configuration). Negotiated during initialization via
elicitationin client capabilities.
Real World Context
A database administration MCP server exposes a drop_table tool. Without human-in-the-loop, the LLM could execute this tool in a chain of operations without the user realizing what is happening. With HITL, the server marks drop_table with destructiveHint: true, and the host intercepts the call to show the user: "The AI wants to drop the 'users' table. Do you approve?" The user sees exactly what will happen and can reject dangerous operations.
Sampling is useful when the server needs intelligence. A log analysis server might receive 10,000 log lines and use sampling to ask the host's LLM: "Summarize the error patterns in these logs." The server gets an AI-generated summary without needing its own LLM access.
Deep Dive
Requesting an LLM Completion from a Server
The server uses the low-level Server API to request the client's LLM for a completion. On McpServer, access the underlying Server instance via server.server:
typescriptimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer( { name: "analytics-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.tool( "analyze_logs", "Analyze application logs and return a summary", { logData: z.string().describe("Raw log data to analyze") }, async ({ logData }) => { // Ask the host's LLM to summarize the logs const result = // Note: createMessage is on the low-level Server instance const result = await server.server.createMessage({ messages: [ { role: "user", content: { type: "text", text: `Summarize the error patterns in these logs:\n\n${logData}` } } ], maxTokens: 500 }); return { content: [{ type: "text", text: `Log Analysis Summary:\n${result.content.text}` }] }; } );
The server sends the log data to the LLM and gets back a structured summary. The host controls whether to fulfill this request, what model to use, and whether to show the user.
Tool Annotations for Behavioral Hints
Declare how your tools behave so hosts can make informed decisions about confirmation flows:
typescriptserver.tool( "delete_file", "Delete a file from the filesystem", { path: z.string() }, async ({ path }) => { await fs.unlink(path); return { content: [{ type: "text", text: `Deleted: ${path}` }] }; }, { annotations: { title: "Drop Table", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } } ); server.tool( "read_file", "Read a file from the filesystem", { path: z.string() }, async ({ path }) => { const content = await fs.readFile(path, "utf-8"); return { content: [{ type: "text", text: content }] }; }, { annotations: { title: "Read File", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } } );
Hosts like Claude Desktop use these annotations to decide whether to auto-approve a tool call or show a confirmation dialog. A tool marked destructiveHint: true will typically require explicit user approval.
Building a Confirmation Workflow
Combine sampling and annotations to create a full confirmation workflow for destructive operations:
typescriptserver.tool( "drop_table", "Drop a database table permanently", { tableName: z.string() }, async ({ tableName }) => { // Step 1: Use sampling to generate a human-readable explanation const explanation = // Note: createMessage is on the low-level Server instance const result = await server.server.createMessage({ messages: [ { role: "user", content: { type: "text", text: `Explain the consequences of dropping the database table "${tableName}" in 2-3 sentences. Be specific about data loss risks.` } } ], maxTokens: 200 }); // Step 2: The host will show this to the user for approval // because the tool is marked as destructive. // If we reach this point, the host has approved the operation. await db.query(`DROP TABLE IF EXISTS "${tableName}"`); return { content: [{ type: "text", text: `Table "${tableName}" has been dropped. ${explanation.content.text}` }] }; }, { annotations: { title: "Delete File", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } } );
The server uses sampling to generate a clear explanation of consequences, and the destructive annotation ensures the host asks the user for confirmation before executing.
How the Host Controls Sampling
The host has full control over sampling requests:
textHost Decision Outcome ───────────────────── ───────────────────────────────────── Auto-approve LLM completion runs immediately Show approval UI User sees the request and approves/rejects Modify request Host edits the messages before sending to LLM Reject Server receives an error response Rate limit Host throttles sampling requests
This design ensures the human is never bypassed. The server can request intelligence, but the host decides whether and how to provide it.
Elicitation: Server-Initiated User Input
While sampling lets servers request LLM completions, elicitation lets servers request direct input from users. This is a newer MCP capability with two modes:
- Form mode: The server sends a structured form definition, and the client renders it as a UI for the user to fill out.
- URL mode: The server provides a URL that the client opens for the user (useful for OAuth flows, external configuration pages, etc.)
typescript// Server-side: requesting user input via elicitation const result = await server.createElicitation({ mode: "form", message: "Please provide your database credentials", form: { fields: [ { name: "host", description: "Database hostname", required: true }, { name: "port", description: "Port number", required: true }, { name: "password", description: "Password", required: true } ] } }); if (result.action === "accept") { // User submitted the form const { host, port, password } = result.content; }
Elicitation is negotiated during initialization — the client declares elicitation: { form: {}, url: {} } in its capabilities. Servers should check for this capability before attempting to elicit input.
Tasks: Long-Running Operation Tracking
The tasks capability enables tracking of long-running operations. When a client or server supports tasks, any request can be augmented with a task identifier that allows:
- Progress tracking: The receiver creates a task and sends progress updates.
- Cancellation: The requestor can cancel a running task via
tasks/cancel. - Listing: Active tasks can be listed via
tasks/list.
This is particularly useful for MCP servers that perform expensive operations like large dataset exports, code analysis across repositories, or multi-step workflows.
Tasks are negotiated during initialization via capabilities.tasks. Both clients and servers can support tasks — check the peer's capabilities before augmenting requests with task identifiers.
Common Pitfalls
- Assuming sampling is always available — Not all hosts support sampling. Check the client capabilities during initialization. If sampling is not available, your server should gracefully degrade to non-AI fallbacks.
- Using sampling for trivial tasks — Sampling invokes a full LLM completion, which adds latency and cost. Do not use it for tasks that can be solved with string manipulation or simple logic.
- Not handling sampling rejection — The host can reject a sampling request. Your tool handler must handle this case gracefully, returning a meaningful error message rather than crashing.
Best Practices
- Mark every tool with accurate annotations — Even if your host does not use annotations today, future hosts will. Accurate annotations are a contract about your tool's behavior.
- Use sampling for intelligence, not for computation — Sampling is ideal for summarization, classification, and natural language generation. Use regular code for data processing, calculations, and transformations.
- Design graceful degradation — If sampling is unavailable, your tool should still work. Return raw data instead of an AI summary, or provide a template-based response instead of an LLM-generated one.
Summary
- Sampling (
sampling/createMessage) lets servers request LLM completions from the host, inverting the normal flow. - Use sampling for summarization, classification, and generating human-readable output from raw data.
- Tool annotations (
title,readOnlyHint,destructiveHint,idempotentHint,openWorldHint) tell hosts how tools behave so they can enforce confirmation workflows. - The host has full control over sampling: it can approve, reject, modify, or rate-limit requests.
- Build confirmation workflows by combining sampling (to explain consequences) with annotations (to require user approval).
- Elicitation lets servers request direct user input via forms or URLs, complementing sampling's LLM-focused approach.
- The tasks capability enables tracking, cancellation, and listing of long-running operations across the MCP protocol.
Code Examples
// Server requests an LLM completion from the host
const result = await server.server.createMessage({
messages: [
{
role: "user",
content: {
type: "text",
text: "Summarize these error logs..."
}
}
],
maxTokens: 500
});
// Tool with destructive annotation
server.tool("delete_file", "Delete a file",
{ path: z.string() },
async ({ path }) => { /* ... */ },
{ annotations: { title: "Delete File", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } }
);