Skip to content
Chapter 1 of 8 · SamplingThe sampling/createMessage request

15 min read

The sampling/createMessage request

On this page

Sampling is the Model Context Protocol's name for a server asking the client to run a model completion for it and hand the generated text back. The word carries the machine-learning sense of drawing tokens out of a model rather than any statistical sense of drawing from a population, and the method that carries it is sampling/createMessage. It exists because a great many MCP servers need a language model in order to do their own job — turning a list of commit subjects into a changelog, condensing a forty-page contract before returning it, sorting a support message into one of 240 categories — and a great many of those servers have no model of their own to call. A server that runs as a local process on the user's machine, launched by the client and spoken to over standard input and output, has no API key, no provider account, and on some networks no route out to a model API at all, while the client that launched it has all three. Sampling lets such a server borrow the client's model instead of every user being shipped a credential to configure.

The step under "has no API key" is worth spelling out, because it is the whole economic case and it is easy to accept as a slogan. A model API is reached by presenting a credential that identifies an account to bill, so possessing the credential and paying the bill are the same thing. A stdio server is a process the client starts on the user's own machine, which leaves exactly two ways for it to hold such a credential: the user obtains one and pastes it into a config file, or the server's author embeds one and pays for everybody's usage. The first puts a provider account and a payment method in front of every person who installs the integration, which is why integrations that require it have small install bases. The second makes the integration's running cost rise with its popularity, which is why almost nobody ships it. Sampling moves the credential to the other side of the boundary, and that — not better output, not cheaper output — is what it buys.

Where an engineer meets it is on a connection they already have. Your application is already a model client, in that it holds the provider key, has a catalogue of models and is running a conversation, and it has also become an MCP client by connecting to a handful of servers named in a config file. During the opening handshake the client tells each server whether it will answer sampling requests, and once it has said yes, any of those servers may send sampling/createMessage at any point on the connection — most often part-way through serving a tool call.

Reading the frame as a set of claims about who decides

Here is one, captured from a third-party release-notes server that reads a repository and writes changelog entries. It has an id and a method and no result, which makes it a request rather than a response, and it is travelling from the server towards the client:

json
{
  "jsonrpc": "2.0",
  "id": 47,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Rewrite these 214 commit subjects as a changelog grouped by area.\n- fix(auth): reject expired refresh tokens\n- feat(billing): proration on mid-cycle plan change\n- chore(ci): bump runner image\n…"
        }
      }
    ],
    "systemPrompt": "You are a release-notes editor. Be terse and factual.",
    "includeContext": "thisServer",
    "modelPreferences": {
      "hints": [{ "name": "claude-sonnet-4-5" }],
      "intelligencePriority": 0.9,
      "costPriority": 0.2,
      "speedPriority": 0.3
    },
    "maxTokens": 4096,
    "temperature": 0.2
  }
}

Read the params as a set of claims about who decides what. The messages array and the systemPrompt were written by the server, at runtime, out of material the client never sees before the frame arrives — here about 5,800 tokens of commit subjects, priced against the client's provider account because the client is the party holding the API key. maxTokens is the server's request for how much output it would like. temperature, stopSequences and metadata are the server's sampling settings. includeContext asks the client to attach conversation context that the server does not itself possess. modelPreferences expresses which model the server would prefer, in two forms: hints, an ordered list of model name substrings the client may match loosely against whatever it has available, and three priority values between 0 and 1 for cost, speed and intelligence.

Those claims are not equally consequential, because only one of them scales the bill in proportion to itself. Two hundred and fourteen commit subjects rendered as roughly 5,800 tokens comes to about twenty-seven tokens apiece, which is what a one-line subject weighs, and the number of subjects included was the server's choice. Nothing in the frame lets the client negotiate that number down. By the time the request arrives the prompt is already assembled, so the client's options are to answer it, to refuse it, or to answer it on a cheaper model. maxTokens bounds the other half of the bill, and the server chose that number too. The party that sizes the prompt and the party that pays for it are different parties, and no field in params connects them.

The client's reply carries the completion back:

json
{
  "jsonrpc": "2.0",
  "id": 47,
  "result": {
    "role": "assistant",
    "content": {
      "type": "text",
      "text": "## Authentication\n- Expired refresh tokens are now rejected at the edge…"
    },
    "model": "claude-haiku-4-5",
    "stopReason": "endTurn"
  }
}

Two things reverse relative to a tool call. The direction of the request reverses: the server is the caller and the client is the callee, on a connection the client opened and pays to keep open. The authorship of the prompt reverses: with a tool call, the prompt-shaped thing is the arguments object, which your side composes against a JSON Schema the server published in advance; with sampling, the entire prompt is the server's, composed at the moment of the request, against nothing published in advance.

The choice of model does not reverse. modelPreferences is advisory in the specification's own terms, the client makes the final selection, and the model field in the result names what actually ran. In the frame above the server asked for a frontier model with intelligence priority at 0.9 and the client answered on a small, cheap one; that is a compliant exchange, and the field that reports it exists precisely so the server can notice and decide what to do. A server that treats a hint as a guarantee has misread the contract. A client that silently answers on a model the user has not approved for that server has a different problem, which chapter 7, on where a refusal surfaces, is about.

Sequence: User → Client (holds the API key) (generate the changelog); Client (holds the API key) → Release-notes server (tools/call generate_changelog); Release-notes server → Client (holds the API key) (sampling/createMessage (server-authored prompt)); Client (holds the API key) → User (approval prompt); User → Client (holds the API key) (allow); Client (holds the API key) → Model provider (messages request, client's key, client's model); Model provider → Client (holds the API key) (completion + usage); Client (holds the API key) → Release-notes server (result { content, model, stopReason }); Release-notes server → Client (holds the API key) (tools/call result); Client (holds the API key) → User (changelog).

The shape of that diagram is the whole idea. There is a nested call inside the tool call, it runs on your account, and its payload was written by the party you are calling. Every other method on that connection has the client as the initiating party; this one does not.

How a hint becomes a model

modelPreferences is the part of the request that reads most like a demand and binds least. The hints array holds objects with a name field, and the specification says the client should treat each name as a substring to be matched loosely against the models it has available, in order, falling through to the next hint when nothing matches. A server that writes claude-sonnet-4-5 is expressing a preference that a client running only OpenAI models can honour by mapping the family and the tier onto something comparable, or can ignore entirely. The three priority values are floats between 0 and 1 for costPriority, speedPriority and intelligencePriority, and they express relative weight rather than a threshold.

A client implementing this is writing an ordinary scoring function over a catalogue it controls:

ts
function selectModel(prefs: ModelPreferences, policy: ServerPolicy): string {
  const allowed = CATALOGUE.filter((m) => policy.allowedModels.includes(m.id));

  for (const hint of prefs.hints ?? []) {
    const match = allowed.find((m) => m.id.includes(hint.name));
    if (match) return match.id;                    // an allowed hint wins outright
  }

  const w = {
    cost: prefs.costPriority ?? 0.5,
    speed: prefs.speedPriority ?? 0.5,
    iq: prefs.intelligencePriority ?? 0.5,
  };
  return allowed
    .map((m) => ({
      id: m.id,
      score: w.cost * m.cheapness + w.speed * m.speed + w.iq * m.capability,
    }))
    .sort((a, b) => b.score - a.score)[0].id;
}

The policy.allowedModels filter is why the release-notes server asked for a frontier model and got Haiku: the client's policy for third-party servers listed two small models, the hint matched neither, and the scoring ran over what was left. That behaviour is compliant, and the model field in the result exists so the server can find out. A server that composes a prompt relying on frontier-model instruction-following and never reads the model field will produce quietly worse output on every client that answers with something cheaper, and will attribute the regression to the prompt.

Two properties of the scoring line decide what a server can communicate through the priorities at all, and both are visible in the arithmetic. The first is that nothing normalises the weights: a model's score is the cost weight times its cheapness, plus the speed weight times its speed, plus the intelligence weight times its capability, and that sum is not divided by anything. A score is therefore a number with no units, and comparing one request's scores against another request's is meaningless.

The second property follows from the first. Multiplying all three weights by the same positive factor multiplies every model's score by that factor, because each of the three terms picks up the factor and a common factor comes straight back out of the sum; and multiplying every number in a list by the same positive constant leaves their order unchanged, so the client ranks the catalogue exactly as it did before. What a server transmits is the ratios between its three priorities and nothing else. An intelligence priority of 0.9 against a cost priority of 0.2 says intelligence counts between four and five times as much as price, and a server that raised all three values to signal urgency would have said precisely what it said before it raised them.

Declaring the capability is the whole grant

The permission to send that frame at all is granted in initialize, in the capabilities object the client sends:

json
{
  "jsonrpc": "2.0", "id": 0, "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "clientInfo": { "name": "support-agent", "version": "2.4.0" },
    "capabilities": {
      "sampling": {},
      "roots": { "listChanged": true }
    }
  }
}

"sampling": {} is the entire declaration. An empty object is the whole grant, and there is no field inside it for a budget, a model allowlist, a rate limit or a context scope, because the protocol deliberately puts none of that on the wire — those are the client's business, and building them is what chapters 3 through 5, on limits, denials and records, are about. A client that omits the key is telling the server that sampling/createMessage will not be served, and a server that sends one anyway gets the standard JSON-RPC method-not-found error:

json
{ "jsonrpc": "2.0", "id": 47, "error": { "code": -32601, "message": "Method not found" } }

The declaration is per connection rather than per application, which is the lever most client implementations fail to use. Because initialize is sent separately down each server connection, a client can advertise sampling to the two servers a user has explicitly approved for it and omit the key for the other six, and the six will never send a request in the first place. That is cheaper than denying at request time and it removes an entire class of decision from the runtime path.

The chain under that last claim is short enough to check link by link, and it is worth checking, because "we can always say no later" is the reasonable-sounding alternative. A server learns what the client supports from the initialize result and from nowhere else on the connection. A well-written server SDK gates its own sampling helper on that declaration, so with the key absent the frame is never composed. A frame never composed is a request never received, which is in turn a consent dialog never shown, a rate-limiter slot never taken, a prompt never written to a log and a policy branch never executed. Refusing at request time reaches the same verdict several steps further along, after the request has crossed the transport and entered code that has to be written, tested and kept correct as the policy changes. The two paths differ in outcome only for a server that ignores the declaration and sends anyway, and that server receives the method-not-found error above, which is the answer you wanted in the first place.

Tool approval did not cover this

The tempting policy is to auto-approve sampling for any server the user has already approved for tools, on the reasoning that a server trusted enough to run operations against your systems is trusted enough to produce text. Text is the least dangerous thing an integration can produce, the argument goes, so sampling is strictly the smaller grant.

What that argument misses is that the two approvals are approvals of different kinds of object. Approving a server's tools means approving an enumerable set of named operations, each with a schema you can read, fixed at connection time and visible in tools/list before anything runs. Approving sampling means approving an open-ended text-generation facility against your model, your budget, and whatever context your client decides to attach — with the prompt composed at runtime and nothing to review in advance, because the thing being approved does not exist yet at the moment of approval.

tools/callsampling/createMessage
Who sends the requestclientserver
Who authored the payloadyour side, against a published schemathe server, at runtime
What you can review before consentingthe tool list and each schemanothing; the prompt arrives with the request
Size of the surfaceenumerable: n tools, n schemasunbounded: any text, any number of times
Whose resources are consumedthe server'syour provider account
Where the output goesinto your model's contextback to the server
What a per-call review meansreading arguments you composedreading text a third party wrote

The last row is where the two diverge most sharply in practice. A per-call approval dialog for a tool shows the user something their own agent decided to do. A per-call approval dialog for sampling shows the user a wall of text a third party wrote, arriving at machine speed, and asks them to judge it — which is a control, but a weak one, and one that degrades the more it fires.

The belief that makes auto-approval tempting is worth writing out in the form a reader would state it: text cannot do anything, so a facility that only produces text must be the smaller grant. The first half is true of the text and false of the exchange that produces it. Producing the text spends money on an account the server does not hold, against a prompt nobody reviewed, over whatever context the client assembled — three consequences, none of which is text. The second half fails on a separate point, which is that the size of a grant is not the size of its output. Tool approval enumerates: n named operations with n readable schemas, and n is a number visible in tools/list before anything runs. Sampling enumerates nothing, so there is no n on the other side of the comparison, and "smaller" has no meaning between a list and an open-ended facility.

None of this makes sampling illegitimate. There are servers for which it is the only sensible design, and chapter 8, which measures three ways of getting the same completion, works through what those look like. The argument against blanket auto-approval is narrower: the grant is a different grant, so it needs its own decision, and inheriting it from tool approval means the user consented to something they were never shown.

Sampling, elicitation, and a server's own model call

Sampling is not the server calling a model provider. The server never sees your API key, never learns your account identity, and cannot select the model — everything it gets is mediated. That mediation is real and it is what makes a policy possible at all. It is also what makes the exchange more dangerous than a server making its own call, because a server making its own call spends its own money and sees only what it already has.

Sampling is not elicitation. elicitation/create, which travels in the same direction on the same connection, asks the client to put a structured question to the user — a schema, a form, a human typing an answer — and returns what the human supplied. Sampling asks the client to put an unstructured question to the model and returns what the model generated. The confusion is common because both are server-initiated, and the distinction that matters is which resource is being spent: a human's attention in one case, tokens and context in the other.

Sampling is also not a way to give the server your conversation. The includeContext field asks, and the client decides; a client that attaches nothing at all under any value of that field is still a conforming client. The specification is careful here and worth reading directly — its sampling section defines the message shape and the advisory status of preferences, and then hands the policy to the implementer with a recommendation for human approval and rate limiting rather than a mandate.

What sampling does not solve is worth stating plainly too. It does not give a server better output than it could get by calling a model itself; the model is the same model, and often a smaller one than the server asked for. It does not save the server from writing prompts. Its only genuine economics are the keylessness described above, which matters enormously for a local stdio server on a laptop and matters not at all for a hosted SaaS integration that already talks to three APIs.

The frame is therefore best read as three separate questions with three separate answers. Who may send it is decided once, per connection, by whether you put "sampling": {} in the capabilities you declare — and that is the only part of the arrangement the protocol carries. What the prompt says is the server's to decide, every time, with nothing published in advance for you to review. What model runs is yours, which is why the result reports it, and a server that ignores the report will misread its own output quality.

That leaves the question the release-notes frame raised and did not answer. Its includeContext was set to thisServer, which sounds narrow, and the client's default behaviour for that field had never been examined. What a client attaches when it honours that field decides whether the exchange is a moderately expensive convenience or a one-hop data path out of the application, and the two look identical in the frame log.

Sampling — read the current version at https://daviddominguez.dev/learn/en/sampling-2/the-sampling-createmessage-request

Back to top