DEV Community

Cover image for MCP Sampling: When the Server Gets to Prompt Your Model
Ken Imoto
Ken Imoto

Posted on

MCP Sampling: When the Server Gets to Prompt Your Model

Most of what I'd read about MCP framed the data flow one direction: I ask, the client picks a tool, the server runs it, I get a result. The server was a thing my model called. It did not occur to me that the server could call back.

Then I read the part of the spec that covers Sampling, and the arrow flipped. With Sampling, an MCP server can send a request up to the client that says, in effect, "run an LLM completion for me and hand me the text." The server is not returning data anymore. It is asking my model to think on its behalf, with a prompt the server wrote.

I spent an evening tracing exactly what that request contains, who approves it, and which clients even support it in 2026. This post is what I found: how Sampling works, the trust boundary it crosses, and why a feature designed to make servers smarter is also a clean new way to get into your model's context.

MCP Sampling flow: the server reaches across the trust boundary through the client's human approval gate to your LLM and back, with the approval gate marked as the single line of defense

What Sampling actually is

In a normal MCP exchange, the model is the one with the LLM. The server is dumb plumbing: it exposes tools, runs them, returns text. Sampling inverts that. It lets a server that has no model of its own borrow yours.

Say a server is processing an expense and hits a transaction it can't categorize. Without Sampling it has to fail, guess, or hand the problem back. With Sampling it pauses mid-task and asks the client: "given this description, which account does this belong to?" The client runs that against its LLM and returns the answer, and the server keeps going. (Speakeasy, What is MCP sampling)

That is genuinely useful. It lets a server stay simple and still make judgment calls, instead of every server shipping its own model and API key. The MCP docs pitch it as the thing that makes "agentic" server workflows possible: the server can stop and reason at a step instead of running blind. (Model Context Protocol, Sampling concept)

But re-read that sentence. A third party's server got to write a prompt and run it through the model I authenticated, on context I own. That is a different relationship than "I called your tool."

The request shape

The method is sampling/createMessage, and the request the server sends back up to the client looks roughly like this. (MCP spec draft, client/sampling)

{
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      { "role": "user",
        "content": { "type": "text", "text": "Categorize this transaction: ..." } }
    ],
    "modelPreferences": {
      "hints": [{ "name": "claude-3-sonnet" }],
      "costPriority": 0.3,
      "intelligencePriority": 0.8,
      "speedPriority": 0.5
    },
    "systemPrompt": "You are a helpful bookkeeping assistant.",
    "includeContext": "thisServer",
    "maxTokens": 100
  }
}
Enter fullscreen mode Exit fullscreen mode

Three fields are doing more than they look:

  • messages and systemPrompt are a prompt written by the server. The server controls what the model is asked to do. The client did not write this; a remote party did.
  • modelPreferences lets the server nudge model selection with hints, costPriority, intelligencePriority, and speedPriority. The client still makes the final pick, but the server gets to lobby for an expensive model. (rust-mcp-schema, CreateMessageRequestParams)
  • includeContext is the one I kept staring at. It accepts "none", "thisServer", or "allServers". It tells the client how much of the conversation to fold into the prompt the server requested. The default is "none", and "thisServer"/"allServers" are soft-deprecated: a compliant client only honors them if it has declared a sampling-context capability. (MCP spec draft, client/sampling)

That includeContext field is where the convenience and the danger sit on the same line.

The approval gate the spec built in

The protocol authors clearly saw this coming, because they did not leave the server's request unsupervised. The spec puts a human in the loop at two points, not one.

  1. Before the call. The client is supposed to show me the prompt the server wants to run, and I can edit it, approve it, or reject it. The server's text does not reach the model unless I let it. (Speakeasy, What is MCP sampling)
  2. Before the result goes back. The client shows me the completion the model produced, and I can approve or block it before it returns to the server. (Model Context Protocol, Sampling concept)

So the design intent is: the server proposes, the model thinks, but a person sits on both the inbound prompt and the outbound answer. The client owns the actual LLM call, picks the real model, and is the gate. That is a reasonable threat model on paper.

The catch is the word supposed to. The spec describes the gate. Whether a given client implements it well, or implements Sampling at all, is a different question.

Who actually supports it in 2026

This is the part that reset my assumptions. Sampling is one of the older MCP primitives by spec, and it is still thinly implemented.

Claude Code acting as an MCP client still does not support Sampling. The feature request (Issue #1785, claude-code) has been open since June 2025 and was still open, at 58 comments, in August 2026. Other clients have moved: opencode's "Add MCP sampling support (createMessage)" request (Issue #11948, opencode) was closed as completed in April 2026, and VS Code's MCP client acts on modelPreferences.

Two things follow from that. First, if you are building a server and leaning on Sampling, your server fails or degrades on the clients most of your users run. Second, the security surface lives in how each client implements the gate. A primitive that is "emerging" and implemented unevenly across clients means the human-in-the-loop guarantee is only as strong as the specific client in front of you. The spec mandating an approval step does not mean the client you're on actually renders one. Which is its own small joke: I'd spent an evening auditing a gate that, on the client I use every day, doesn't exist yet.

The trust boundary, stated plainly

Here is the boundary I had been ignoring. When my model calls a tool, the data flows server-to-me and I treat the result as untrusted output. When a server uses Sampling, the request flows server-to-my-model, and the server is now upstream of my model's reasoning. It crossed from "thing I call" to "thing that prompts me."

In April 2026, Palo Alto's Unit 42 published an analysis of attack vectors specific to Sampling, and it names the failure modes cleanly. Sampling attacks slip past tool-integrity checks and sandboxing because they ride a legitimate protocol feature, not a malformed tool. They group the abuse into three classes: covert tool invocation that performs hidden file and system operations, conversation hijacking that injects instructions persisting across turns, and resource theft that drains your compute quota for the attacker's workloads. (Unit 42, Prompt Injection Attack Vectors Through MCP Sampling)

The mechanism for the second one is worth spelling out, because it's subtle. A malicious server's Sampling prompt instructs the model to append a directive to its next visible response. Because that text lands in the conversation history, the model keeps following it on later turns, long after the Sampling call finished. The same trick exfiltrates data by telling the model to slip extracted information into its next answer to you. (Unit 42, MCP Sampling attack vectors)

And includeContext? That is the cross-server problem. If a client isn't strict about scoping each server's Sampling request to that server's own context, a malicious server can ask for "allServers" and pull in conversation belonging to servers it was never meant to see. In a multi-server session, one untrusted server can read context from the trusted ones it shares the model with. (Unit 42, MCP Sampling attack vectors) The soft-deprecation of "thisServer"/"allServers" is the spec quietly walking that back, but only for clients that respect the capability flag.

A short threat list

If I'm reviewing a server that uses Sampling, or a client that implements it, this is what I check:

  • Prompt injection inbound. The messages/systemPrompt from a server are attacker-controlled text aimed straight at your model. Treat them like any other untrusted prompt.
  • Persistent hijack. A Sampling prompt can plant an instruction in the visible answer that survives into later turns. The damage outlives the one call.
  • Cross-server context leak. includeContext: "allServers" on a loose client hands one server the conversation of every other server in the session.
  • Quota theft (Denial-of-Wallet). A server can fire Sampling requests to burn your tokens and budget on its own work, not yours.
  • Silent or missing gate. If the client doesn't render the request-and-response approvals the spec calls for, the entire safety story collapses to "trust the server."

Takeaways

  • Sampling flips MCP's direction: the server asks your client to run an LLM call (sampling/createMessage) using a prompt the server wrote.
  • The spec puts a human in the loop twice, before the request reaches the model and before the result returns. That gate is the whole defense.
  • includeContext (none/thisServer/allServers) is the cross-server leak risk; thisServer/allServers are soft-deprecated and capability-gated for a reason.
  • Sampling is unevenly implemented in 2026: Claude Code still doesn't support it as a client, while opencode shipped it in April. The safety guarantee is only as real as the client's gate.
  • Unit 42 documents three live abuse classes: covert tool invocation, persistent conversation hijacking, and quota theft. Sampling prompts are untrusted input pointed at your model.

I went deeper on MCP's primitives, trust boundaries, and the OWASP MCP failure modes in my book if you want the long version: MCP Security Practice.

Top comments (0)