teachyou.ai academy
← All posts
MCPModel Context ProtocolAI agentsagent architectureLLM tooling

MCP Sampling and Elicitation: Server-Initiated LLM Calls

Pramod Dutta · Jun 25, 2026 · 18 min read

MCP sampling is the mechanism by which an MCP server asks the client to run an LLM completion on the server's behalf, instead of the server calling a model API directly. It inverts the usual MCP data flow: rather than the client always initiating requests to the server, the server sends a sampling/createMessage request back to the client, and the client's host application (with a human still in the loop) decides whether to run it. Elicitation is the sibling capability: it lets a server pause mid-task and ask the client to collect structured input from the user, via a JSON schema instead of free text.

Both features solve the same underlying problem: an MCP server frequently needs something only the client can provide, either a model to reason with or a human to answer a question, and neither capability existed in the original 2024 version of the protocol. If you have only used MCP for tools/list and tools/call, this article covers the other half of the spec: the requests that flow from server to client.

Why MCP Sampling Exists

In the baseline MCP flow, the client holds the model. A host application such as an IDE assistant or a chat client connects to one or more MCP servers, and those servers expose tools, resources, and prompts. The client's model decides which tool to call, the server executes it, and the result goes back into the model's context. The server itself is typically "dumb": it does not call an LLM, it just executes deterministic code (query a database, hit an API, read a file).

That works fine for lookup-style tools. It breaks down for a category of servers that need judgment mid-execution. Consider a server that:

  • Summarizes a long document before returning it, so it doesn't blow the caller's context window
  • Classifies unstructured user input before deciding which sub-tool to route to
  • Evaluates whether a generated SQL query looks safe before running it
  • Drafts a commit message from a diff as part of a larger git-automation tool

Before sampling existed, a server author had two bad options. Either give the server its own API key and let it call a model directly (now the server needs billing, rate limiting, and a second point of AI-safety review, and its behavior is invisible to the host's usage tracking and guardrails), or push the judgment step back to the client's model as an extra round trip, which adds latency and often loses the local context the server already has loaded.

MCP sampling gives a third option: the server asks the client's model to do the reasoning, through the same connection, with the client staying in control of which model actually runs and whether the human approves the call. The server never sees an API key. The client's existing safety and cost controls apply automatically, because it's still the client's model quota being spent.

The sampling/createMessage Request

Sampling is a server-to-client JSON-RPC request. The server sends sampling/createMessage, and the client is expected to respond with a completion. A minimal request looks like this:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Summarize this changelog in two sentences: ..."
        }
      }
    ],
    "systemPrompt": "You write terse, factual release notes.",
    "includeContext": "thisServer",
    "maxTokens": 200,
    "temperature": 0.3
  }
}

Key fields:

  • messages: the conversation to sample from, using the same role/content shape as the rest of MCP. Content can be text, image, or audio blocks.
  • systemPrompt: optional. The client is allowed to ignore, modify, or wrap this before it hits the actual model, since the client is the one enforcing its own system-level policy.
  • includeContext: tells the client how much of the surrounding MCP context to fold in. none means sample in isolation, thisServer includes context the requesting server has already shared, allServers includes context from every connected server. Most clients treat this as a hint, not a guarantee, because pulling in context from other servers has its own privacy implications.
  • maxTokens: a cap the server sets on the response.
  • temperature, stopSequences, metadata: standard sampling controls, all optional.

The client responds with a CreateMessageResult:

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "role": "assistant",
    "content": {
      "type": "text",
      "text": "v2.3 adds streaming responses and fixes a memory leak in the connection pool."
    },
    "model": "claude-sonnet-5",
    "stopReason": "endTurn"
  }
}

Note that the result tells the server which model actually ran (model) and why it stopped (stopReason). The server asked for a completion; it did not get to pick the exact model. That distinction matters enough that the spec has a dedicated field for expressing a preference instead of a hard requirement.

Model Preferences: Hints, Not Guarantees

Because the client owns model selection, cost, and routing, a server cannot say "run this on model X." It can only express preferences through modelPreferences, and the client is free to override them:

{
  "modelPreferences": {
    "hints": [
      { "name": "claude-haiku" },
      { "name": "claude-sonnet" }
    ],
    "costPriority": 0.8,
    "speedPriority": 0.7,
    "intelligencePriority": 0.2
  }
}

hints is an ordered list of suggested model names or families. The client tries to match them against whatever it actually has available and falls through the list if the first hint isn't supported. A server built for a small, cheap classification task might hint at a lightweight model; a server drafting a technical summary might hint at a stronger one, or provide no hint at all and rely on the priority scores.

costPriority, speedPriority, and intelligencePriority are each 0 to 1 and describe the trade-off the server cares about, without naming a specific model. A high-volume classification server sets costPriority near 1 and intelligencePriority low. A one-shot architecture-review tool does the opposite. This is the mechanism that lets a server express "this call doesn't need to be smart" without hardcoding a model name that might not exist on the client's side, or might be deprecated by the time the server ships.

The client is allowed to ignore preferences entirely, for example if the organization has locked every MCP-originated sampling call to one approved model for audit reasons. Preferences are a request, not a contract.

Human-in-the-Loop Is Not Optional

The spec is explicit that sampling requests should never bypass the user. A compliant client implementation is expected to:

  • Show the user what the server is asking to send to the model, before it's sent
  • Let the user edit or reject the prompt
  • Show the user the completion before it's returned to the server, and let them edit or block it

This exists because sampling is the one place in MCP where a server, which might be a third-party integration with a much smaller trust footprint than the model provider, gets to put words in front of a model and read back what comes out. Without a review step, a malicious or buggy server could use sampling to exfiltrate data (by asking the model to summarize sensitive local context and stuff it into the completion) or to manipulate the user (by asking the model to generate text that looks like it came from the assistant but was actually server-authored). The approval step is what keeps sampling in the same trust boundary as everything else the client shows the user.

In practice, most current host applications implement this as a lightweight confirmation dialog on first use per server, sometimes with a "remember for this session" option, rather than a full review on every single call. That's a client policy decision, not a protocol requirement, but the protocol requires that the option to review exist.

Implementing Sampling in a Python Server

The Python MCP SDK exposes sampling through the request Context object that FastMCP passes into a tool function. Here's a server that uses sampling to draft a commit message from a diff, instead of shipping its own model call:

from mcp.server.fastmcp import FastMCP, Context
from mcp.types import SamplingMessage, TextContent

mcp = FastMCP("git-helper")

@mcp.tool()
async def draft_commit_message(diff: str, ctx: Context) -> str:
    """Draft a commit message for the given diff using the client's model."""
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"Write a one-line git commit message for this diff:\n\n{diff}",
                ),
            )
        ],
        system_prompt="Follow conventional commits. No explanations, just the message.",
        max_tokens=60,
    )
    if isinstance(result.content, TextContent):
        return result.content.text
    return "chore: update files"

The tool itself never calls an LLM API. It calls ctx.session.create_message, which sends sampling/createMessage up to whatever client invoked this server, waits for the human-reviewed response, and gets back plain text. If the client doesn't support sampling, create_message raises, so a production server should check for the capability during initialization (covered below) and degrade to a non-AI fallback, such as returning a templated message, rather than crashing the tool call.

Implementing Sampling in a TypeScript Server

The TypeScript SDK exposes the same request through the server's request-handler context:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "git-helper", version: "1.0.0" });

server.tool(
  "draft_commit_message",
  { diff: z.string() },
  async ({ diff }, extra) => {
    const result = await extra.sendRequest(
      {
        method: "sampling/createMessage",
        params: {
          messages: [
            {
              role: "user",
              content: { type: "text", text: `Write a one-line git commit message for this diff:\n\n${diff}` },
            },
          ],
          systemPrompt: "Follow conventional commits. No explanations, just the message.",
          maxTokens: 60,
        },
      },
      z.any(),
    );

    const text =
      result?.content?.type === "text" ? result.content.text : "chore: update files";

    return { content: [{ type: "text", text }] };
  },
);

Both SDKs follow the same shape: the tool handler gets access to the underlying session or request context, and sampling is just another JSON-RPC round trip through it, distinct from the tool's own return value. The tool's return value goes back to the model that originally called the tool; the sampling request is a side conversation the server has with the client while the tool is still running.

What Elicitation Adds

Sampling gets a server a model. Elicitation gets a server a human answer. It was added to the MCP spec after the initial release specifically to cover the case where a tool discovers, mid-execution, that it needs one more piece of information the caller didn't provide, and the right move is to ask the person, not the model.

Typical uses:

  • A deployment tool that needs to confirm "this will delete the staging database, continue?" before proceeding
  • A file-generation tool that hits a naming collision and needs the user to pick "overwrite," "rename," or "cancel"
  • A form-filling tool that's missing a required field the model didn't supply, like a target email address
  • A booking tool that found three matching flights and needs the user to pick one

Before elicitation, a server's only options were to guess, fail with an error the model would have to relay back to the user in its own words, or require every possible parameter up front even when most calls don't need them. Elicitation lets the tool ask exactly when it needs to, with a real form instead of free text buried in a chat turn.

The elicitation/create Request

Like sampling, elicitation is a server-initiated request. The server sends elicitation/create with a human-readable message and a JSON schema describing the shape of the answer it wants back:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "elicitation/create",
  "params": {
    "message": "Deploying will delete the existing staging database. How do you want to proceed?",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "action": {
          "type": "string",
          "enum": ["overwrite", "cancel"],
          "description": "What to do with the existing staging database"
        }
      },
      "required": ["action"]
    }
  }
}

The schema is deliberately restricted compared to full JSON Schema: it must be a flat object with primitive-typed properties (string, number, boolean, enum), no nested objects, no arrays of objects. This is intentional. Elicitation schemas are meant to render into a simple auto-generated form (a few labeled fields, a dropdown, a checkbox), not an arbitrary structured document. If a server needs something more complex than a flat form, elicitation is the wrong tool, and it should either ask for a file upload path through a resource, or break the request into multiple elicitation calls.

The client's response carries an explicit action field on top of the data, because the user can decline or dismiss the prompt entirely, not just fill it in:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "action": "accept",
    "content": { "action": "overwrite" }
  }
}

action at the response level is one of accept, decline, or cancel. accept means the user submitted the form and content matches the schema. decline means the user explicitly said no. cancel means they dismissed the dialog without answering either way (closed the window, timed out). A server has to handle all three: a tool that only checks for content and assumes it's always present will crash on decline or cancel.

Implementing Elicitation

In the Python SDK, Context exposes elicit, typically backed by a Pydantic model so the schema and the parsed response share one definition:

from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("deploy-helper")

class StagingConflict(BaseModel):
    action: str = Field(description="overwrite or cancel")

@mcp.tool()
async def deploy_to_staging(build_id: str, ctx: Context) -> str:
    existing = check_existing_staging_db()
    if existing:
        result = await ctx.elicit(
            message="Deploying will delete the existing staging database. How do you want to proceed?",
            schema=StagingConflict,
        )
        if result.action == "decline" or result.action == "cancel":
            return "Deploy cancelled by user."
        if result.data.action == "cancel":
            return "Deploy cancelled by user."
        # action == "accept" and data.action == "overwrite"
    run_deploy(build_id)
    return f"Deployed {build_id} to staging."

The equivalent in TypeScript goes through the same sendRequest path used for sampling, just with elicitation/create as the method and a schema instead of messages:

server.tool(
  "deploy_to_staging",
  { buildId: z.string() },
  async ({ buildId }, extra) => {
    if (await checkExistingStagingDb()) {
      const result = await extra.sendRequest(
        {
          method: "elicitation/create",
          params: {
            message: "Deploying will delete the existing staging database. How do you want to proceed?",
            requestedSchema: {
              type: "object",
              properties: {
                action: { type: "string", enum: ["overwrite", "cancel"] },
              },
              required: ["action"],
            },
          },
        },
        z.any(),
      );

      if (result.action !== "accept" || result.content?.action === "cancel") {
        return { content: [{ type: "text", text: "Deploy cancelled by user." }] };
      }
    }

    await runDeploy(buildId);
    return { content: [{ type: "text", text: `Deployed ${buildId} to staging.` }] };
  },
);

Both examples show the same pattern: check whether a form is even needed, ask only when it is, and treat every branch of the response (accept, decline, cancel) as a real code path rather than an afterthought.

Sampling vs. Elicitation vs. Tools: Picking the Right One

These three mechanisms answer different questions, and mixing them up produces a confusing tool:

  • Use a regular tool when the server already has everything it needs and the answer is deterministic: read a file, query an API, run a calculation.
  • Use sampling when the server has all the input it needs but the next step requires judgment, drafting, summarizing, or classification, and you'd rather borrow the client's model than run your own.
  • Use elicitation when the server is missing information or needs a decision only the human can make, and the answer is a small, well-typed form rather than free-form reasoning.

A useful gut check: if you'd ask a colleague to "just decide," that's sampling. If you'd ask them "which one do you want," that's elicitation. If the answer was already fully determined by the input, you didn't need either, you needed a plain tool.

It's also fine to combine them in one tool call. A server might elicit a missing parameter first, then sample a model to turn that parameter into a polished output, then return the final result to the calling model as a normal tool response.

Capability Negotiation

Neither sampling nor elicitation is guaranteed to be available. During the MCP initialize handshake, the client declares which capabilities it supports, and a server that wants to use sampling or elicitation should check for the corresponding capability before relying on it:

{
  "capabilities": {
    "sampling": {},
    "elicitation": {}
  }
}

An empty object as the value simply means "supported," with details of what's inside subject to future spec extension. If a client omits sampling or elicitation from its declared capabilities, a well-behaved server should not call the corresponding method, and should instead fall back gracefully: a summarization tool without sampling might return the raw text with a note that it wasn't summarized; a deploy tool without elicitation might require the conflict-resolution parameter up front instead of asking mid-call.

Support for these two capabilities varies by client and changes as SDKs catch up to spec revisions, so don't assume every MCP host you connect to supports either one. Always gate the call behind the capability check rather than a try/catch that silently swallows the failure, so you can give the user (or the calling model) a clear reason the fallback path was taken.

Security and Cost Considerations

A few things to keep in mind before shipping a server that uses either capability:

  • Sampling spends the client's model quota, not the server's. That's the point, but it also means a runaway loop of sampling calls (for example, a server that samples inside a retry loop with no cap) becomes the client owner's cost problem, invisibly, until someone notices the bill or the rate limit. Always set a hard maxTokens and a retry ceiling.
  • Elicitation schemas should never request secrets. The spec explicitly discourages using elicitation to collect passwords, API keys, or other sensitive credentials, because the rendered form is generic UI, not a vetted credential-entry surface with the protections a browser password field gets.
  • Both requests are visible to the user by design. Don't treat that visibility as an inconvenience to route around; it's the safety mechanism. A server that tries to make its sampling prompts vague or its elicitation messages misleading to slip past review is working against the protocol's threat model, not with it.
  • Log what you send and receive on both paths during development. Since these are server-initiated requests, they're easy to under-test compared to the client-initiated tools/call path, and issues (a schema the client can't render, a prompt that triggers an unexpected refusal) tend to surface as silent failures rather than loud ones.

Debugging Sampling and Elicitation

Because both capabilities flow from server to client, standard "call the tool and watch stdout" debugging misses half the interaction. A few practical habits:

  • Use MCP Inspector, or your client's developer/debug console if it has one, to watch the raw JSON-RPC traffic in both directions, not just the tool call and its final result.
  • Test the capability-negotiation path explicitly by connecting your server to a client that does not support sampling or elicitation, and confirm the fallback branch actually runs instead of throwing.
  • For sampling, log the model and stopReason fields on every response during development. If your prompts assume a strong model and the client is routing you to a small one under a costPriority hint, you'll see truncated or lower-quality output and want to adjust your prompt or preferences accordingly.
  • For elicitation, test all three response actions (accept, decline, cancel), not just the happy path. It's easy to build a demo that only ever exercises accept and ships a tool that throws on decline.

FAQ

What's the difference between MCP sampling and a server calling an LLM API directly? With sampling, the server never holds a model API key or picks the exact model; it sends a sampling/createMessage request to the client, and the client's model runs the completion after a human-in-the-loop review. Calling an API directly means the server needs its own credentials, billing, and safety review, and the call is invisible to the client's usage tracking.

Does every MCP client support sampling and elicitation? No. Both are optional capabilities declared during the initialize handshake. A server should check capabilities.sampling and capabilities.elicitation before relying on either, and provide a non-AI, non-interactive fallback when they're absent.

Can a server force a specific model through sampling? No. A server can express preferences through modelPreferences (name hints plus cost, speed, and intelligence priority scores from 0 to 1), but the client decides which model actually runs and returns the chosen model's name in the response.

Is elicitation the same as a tool asking for more parameters? Not quite. A tool's input schema is fixed when the model calls it. Elicitation happens after the tool call has already started, when the server discovers mid-execution that it needs something the schema didn't capture, such as how to resolve a conflict or which of several matches to use.

Can elicitation collect passwords or API keys? It shouldn't. The elicitation schema is meant for short, well-typed operational choices, not credential entry. Sensitive secrets belong in the client's own configuration or secret-storage flow, not in a server-initiated form.

What happens if the user closes the elicitation dialog without answering? The client returns an action of cancel (as opposed to decline, which means the user explicitly said no). A correct server implementation treats accept, decline, and cancel as three distinct branches rather than assuming the response always contains usable data.

Do sampling and elicitation replace regular MCP tools? No, they complement them. Tools stay the mechanism for deterministic actions and data retrieval. Sampling adds judgment when the server needs a model's reasoning; elicitation adds a way to ask the human a short, structured question mid-task. Most real servers use tools for the bulk of the work and reach for sampling or elicitation only at the specific points where the server itself can't determine the next step alone.