Sampling, Roots & Elicitation

Difficulty

Sampling is the one MCP primitive that flips the normal request direction. Instead of the client always initiating, the server asks the client for something.

Why a server would want this

Imagine an MCP server that processes a large log file and needs a concise natural-language summary before returning it as a tool result. Dumping raw logs back to the model to summarize itself burns context on the client side. Giving the server its own separate LLM API key means extra cost, extra credential management, and ties it to one specific model provider. Instead, the server can just ask: "hey, client, can your LLM summarize this for me?"

// Server -> Client
{"method": "sampling/createMessage", "params": {
  "messages": [{"role": "user", "content": {"type": "text", "text": "Summarize these logs: ..."}}],
  "modelPreferences": {"intelligencePriority": 0.3, "speedPriority": 0.8},
  "maxTokens": 200
}}

// Client -> Server (after the user approves, and the client's LLM generates a completion)
{"result": {
  "role": "assistant",
  "content": {"type": "text", "text": "3 errors occurred, all related to a database timeout."},
  "model": "claude-sonnet-5",
  "stopReason": "endTurn"
}}

Why this design instead of "servers just call an LLM API directly"

  • No separate credentials or billing for the server to manage. It uses whatever model access the client/user already has.
  • Model-agnostic. The server doesn't hardcode a dependency on any specific provider. The client decides which model actually services the request.
  • Keeps a human in the loop. Sampling requests go through the client, so the client — and by extension the user — can review, modify, or reject the request before it's ever sent to a model. That matters, since the server is asking to spend the user's model budget and potentially see sensitive conversation context.

The tradeoff

A server using sampling depends on the client actually supporting and approving sampling requests. It's an optional capability, and a client is free to decline. A well-designed server should have a sensible fallback for clients that don't support sampling — returning the raw content, say, or a simpler non-LLM-based summary.

Related Resources

Sampling's approval step exists because the server asks to use resources — model access, potentially cost, potentially context — that belong to the user, not to the server.

The full request

{"method": "sampling/createMessage", "params": {
  "messages": [
    {"role": "user", "content": {"type": "text", "text": "Draft a one-sentence changelog entry for this diff: ..."}}
  ],
  "systemPrompt": "You are a terse technical writer.",
  "includeContext": "thisServer",
  "modelPreferences": {
    "hints": [{"name": "claude-haiku"}],
    "costPriority": 0.8, "speedPriority": 0.9, "intelligencePriority": 0.2
  },
  "maxTokens": 60
}}

The approval step, conceptually

Server sends sampling/createMessage
        |
        v
Client shows the user: "acme-server wants to generate text using your LLM. Preview: [messages]"
        |
   User approves / edits / rejects
        |
        v (if approved)
Client calls its own configured LLM with the (possibly user-edited) request
        |
        v
Client optionally lets the user review the generated completion before sending it back
        |
        v
Client returns the result to the server as the sampling/createMessage response

Why modelPreferences is hints, not a model name

The request expresses priorities — favor cost, favor speed, favor intelligence — plus optional named hints, rather than mandating an exact model. The client, not the server, actually knows which models are available, what they cost, and what the user prefers. The server describes what it needs. The client decides how best to satisfy that.

Why this matters for trust

Without a human-in-the-loop step, a malicious or buggy server could silently spend a user's model budget. Worse, it could smuggle a manipulative instruction into a sampling request, designed to make the client's LLM produce output favorable to the server's own agenda — an early form of the confused-deputy-style risk this primitive is exposed to. Surfacing the request for approval, and ideally showing exactly what will be sent, is the mitigation the spec expects clients to implement. The precise UI for doing so is left to each host.

Related Resources

Roots answer a specific, narrow question. When a server operates on a filesystem-like resource, what part of it is actually the user's business?

The exchange

// Server -> Client
{"method": "roots/list"}

// Client -> Server
{"result": {"roots": [
  {"uri": "file:///home/user/projects/my-app", "name": "my-app"}
]}}

A filesystem-oriented server receiving this should treat file:///home/user/projects/my-app as its working boundary. Listing, reading, or writing files outside it means operating outside what the user intended to expose, even if the server process technically has OS-level permission to do so.

Notifying about changes

// User switches to a different project folder in the host's UI
{"method": "notifications/roots/list_changed"}
// Server should call roots/list again to get the updated set

Why "hint," not "sandbox"

Roots travel over the same JSON-RPC channel as everything else. A server has no technical mechanism forcing it to respect them — a poorly written or malicious server could simply ignore the declared roots and read files anywhere its OS-level permissions allow. Real filesystem safety for an untrusted server has to come from actual OS-level sandboxing: containerization, a restricted user account, a chroot-like boundary. Roots communicate the user's intent about scope, which a well-behaved server should honor. They aren't a substitute for genuine access control when a server isn't fully trusted.

A concrete use case

A code-editing host with multiple project folders open might expose only the currently active project as a root. Say the user asks the assistant to "find all TODO comments." A filesystem MCP server receiving that root list knows to search within that one project, rather than scanning every folder the host happens to have open. Results stay scoped to what's actually relevant, avoiding an accidental cross-project leak of file contents into an unrelated conversation.

Related Resources

Before elicitation existed, a tool that discovered mid-execution it needed more information had few good options. It could fail with an error and hope the model re-asks the user and re-invokes the tool with better arguments, or it could make an assumption on the user's behalf. Elicitation gives it a direct, structured third option.

Example

// Server -> Client, mid-tool-execution
{"method": "elicitation/create", "params": {
  "message": "Which seat would you like for this flight?",
  "requestedSchema": {
    "type": "object",
    "properties": {
      "seatType": {"type": "string", "enum": ["window", "aisle", "middle"]}
    },
    "required": ["seatType"]
  }
}}

// Client, after prompting the user through its own UI
{"result": {
  "action": "accept",
  "content": {"seatType": "window"}
}}

The action field can also be "decline" — the user was asked but chose not to answer — or "cancel" — the user dismissed the request entirely. This lets the server tell "no answer given" apart from "explicitly said no" versus "walked away."

Why this belongs at the client/user layer, not the model layer

The model could just ask the user a clarifying question directly in the conversation and re-invoke the tool with the missing piece. Often that's exactly what should happen for simple cases. Elicitation is useful specifically when a tool's own execution logic, not the model's reasoning, is what determines a follow-up question is needed. That often happens deep inside a multi-step operation the model isn't tracking step-by-step. Getting a clean, schema-validated answer directly, rather than parsing it back out of a free-form model turn, meaningfully simplifies the server's implementation.

Design guidance

Keep elicited requests narrow and specific — a single, clearly-scoped question with a small schema. Don't use elicitation to front-load an entire complex form's worth of fields the server could reasonably have required as part of the initial tool call. Overusing elicitation for things that could have simply been tool arguments adds unnecessary round trips and interrupts the flow of a task the user expected to run to completion in one shot.

Related Resources

Sampling is one of the places MCP's security model gets tested most directly. A server is asking to trigger LLM generation, and how much conversation context it gets to see for that generation is a real trust decision, not a formality.

The includeContext options

{"method": "sampling/createMessage", "params": {
  "messages": [...],
  "includeContext": "thisServer"
}}
ValueMeaning
"none"The sampling request only sees the messages explicitly included in the request itself
"thisServer"Additionally include context this specific server has itself contributed (its own prior tool results, resources)
"allServers"Additionally include context from every connected server, not just this one

Why the client enforces this, not the server

A server requesting "allServers" doesn't mean the client has to grant it. The client is the trust boundary. It decides, per its own policy and potentially with user confirmation, how much of the broader conversation a given server is actually entitled to see for a sampling call. This matters because a session might have five connected servers — a filesystem server, a banking API server, a personal notes server. It would be a serious privacy leak if any one of them could casually request "give me everything" and receive banking details or personal notes it had nothing to do with, just by asking.

The threat this defends against

Imagine a compromised or intentionally malicious third-party MCP server. If sampling requests automatically included the full conversation with no gating, that server could exfiltrate sensitive information from other servers' interactions. It would just issue a sampling request and read whatever context came back, even though its actual declared purpose has nothing to do with that other data. Scoping context by default, and treating broader access as something the client actively grants rather than something a server can simply take, is a direct mitigation for that scenario.

Practical takeaway

When designing a server that uses sampling, request the narrowest includeContext value that actually satisfies your use case. Requesting "allServers" when "thisServer" (or even "none") would do just as well is unnecessary. It's also a signal, to any host that inspects such requests, that the server may be over-reaching.

Related Resources