Skip to content
Skip to main content
A brass service bell beside a blank paper request slip on a concrete surface, illustrating an MCP server asking a human through elicitation instead of borrowing the client's model
8 min readBy Carlos Aragon

MCP Sampling Is Deprecated. Use Elicitation Instead.

Sampling was deprecated in MCP specification revision 2026-07-28under SEP-2577, along with Roots and Logging. It doesn't vanish — under the feature lifecycle policy it stays in the spec until the first revision released on or after 2027-07-28, then becomes eligible for removal. The registry's migration path is one line: integrate directly with LLM provider APIs. Elicitation was notdeprecated, which makes it the last sanctioned way for a server to pause a tool call and get something it's missing.

What Actually Got Deprecated

The 2026-07-28revision is the first one released under MCP's new feature lifecycle policy, and it came with a deprecated features registry. Four things landed on it at once. If you maintain a server, this is the whole list you care about:

FeatureMigration pathEligible for removal
SamplingCall LLM provider APIs directly from your server.2027-07-28 or later
RootsPass directories and files via tool parameters, resource URIs, or server config.2027-07-28 or later
LoggingWrite to stderr on stdio; use OpenTelemetry for real observability.2027-07-28 or later
Dynamic Client RegistrationClient ID Metadata Documents.2027-07-28 or later

Roots and Sampling and Logging all came from SEP-2577. Read them together and the intent is obvious: the protocol is shedding everything where the serverreached back into the client to borrow a capability. Roots borrowed the client's idea of the filesystem. Logging borrowed its log sink. Sampling borrowed its model — and its bill.

Deprecated is not removed:

Nothing breaks on 2026-07-28. A deprecated feature stays in the spec for at least twelve months, and eligible for removalstill means a maintainer has to choose to remove it during a release. You have a year plus change. What you don't have is a reason to start anything new on it.

Why Sampling Was Never Really Shippable

The pitch was genuinely good. Your server needs a summary, a classification, a bit of reasoning — instead of holding an API key and eating the cost, you send sampling/createMessage up to the client, the client runs it on whatever model the user already pays for, and hands you the text back. No key distribution, no per-tenant billing, no model lock-in. I wanted it to work.

It didn't, for a reason that has nothing to do with the idea and everything to do with the contract. Read what the spec says a client is allowed to do with your request:

  • systemPrompt — the client MAY modify or ignore it, without telling you.
  • temperature, stopSequences, metadata — the client MAY modify or ignore them.
  • modelPreferences hints — advisory only; the client makes the final selection and may map your Claude hint onto a Gemini model.
  • includeContext — the client MAY constrain it, and thisServer/allServers were themselves deprecated back in 2025-11-25.

The only thing the client MUST respect is maxTokens. So you were being asked to build a product feature on a function call where the model, the system prompt and the sampling parameters can all be silently swapped out. That isn't an integration, it's a wish. Add the human-in-the-loop review the spec asks for — user approves the prompt, user approves the response — and a single “quick classification” inside your tool turns into two modal dialogs and an unbounded wait.

Then there's the adoption problem. Client support for sampling stayed thin for two years straight. Every MCP server I've shipped this year carries its own provider key, not because I love paying for inference, but because a server that only works in the one client that implemented sampling is a server nobody installs. If you already made that call, the deprecation costs you nothing. That's not hindsight bragging — it's the same reason I ended up reaching for a CLI over MCP more often than the hype suggested.

Elicitation Survived, and It Grew a Second Mode

Sampling asks the model. Elicitation asks the human. That one sentence is the whole distinction, and it explains why only one of them got deprecated: asking the human is something no provider API can do for you.

Elicitation now has two modes, and picking the wrong one is a spec violation, not a style choice:

ModeUse it forWho sees the data
formMissing parameters, confirmations, disambiguation, ordinary profile data.The client, and therefore the model's context.
urlCredentials, third-party OAuth, payments — anything secret.Only your server. The client sees just the URL.

The rule is stated as a hard MUST: servers MUST NOT use form mode to request passwords, API keys, access tokens or payment credentials, and MUST use URL mode for those. Names, emails and usernames are fine in a form. Anything that grants access or authorizes a transaction is not, and the reason is architectural rather than prudish: a form field's value lands in the client, which means it can land in logs, in the transcript, and in the model's context.

Form schemas are deliberately boring. Flat object, primitive properties only — string, number/integer, boolean, enum — with minLength, minimum, defaults, and the four supported string formats (email, uri, date, date-time). No nested objects, no arrays of objects. If your elicitation needs a nested schema, you're trying to render an app inside a dialog — that's what MCP Apps are for.

{
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Which environment should I deploy to?",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "environment": {
          "type": "string",
          "title": "Environment",
          "enum": ["staging", "production"],
          "default": "staging"
        },
        "confirm": {
          "type": "boolean",
          "title": "I understand this is irreversible",
          "default": false
        }
      },
      "required": ["environment", "confirm"]
    }
  }
}

The Delivery Change That Will Break Your Tool Handlers

This is the part that gets skipped in every “what is elicitation” explainer, and it's the part that actually costs you a refactor. In the older 2025-06-18 spec, elicitation/create was a request your server sent up the open connection, mid-handler, and awaited. In 2026-07-28 it is delivered inside an InputRequiredResult — you return it — and the client then retries the original tool call with the answer attached in inputResponses.

So the shape of a handler goes from “block in the middle” to “return, then get re-entered.” Anything you were holding in a local variable across that await has to survive somewhere else:

// Old shape: block mid-handler and await the user.
async function deploy(args) {
  const answer = await ctx.elicit({ ... });   // <- gone
  return doDeploy(answer.environment);
}

// New shape: return an input request, get called again with the answer.
async function deploy(args, inputResponses) {
  const answer = inputResponses?.[0];

  if (!answer) {
    return inputRequired([
      { method: "elicitation/create", params: { mode: "form", ... } },
    ]);
  }

  if (answer.action !== "accept") {
    // decline = user said no. cancel = user dismissed. They are NOT the same.
    return answer.action === "decline"
      ? { content: [{ type: "text", text: "Deploy cancelled by user." }] }
      : { content: [{ type: "text", text: "No response - ask again later." }] };
  }

  return doDeploy(answer.content.environment);
}

Two more things moved with it. Client capabilities are now declared in _meta.io.modelcontextprotocol/clientCapabilities on every request rather than once at initialization, and the elicitation capability now names its modes — { "form": {}, "url": {} }. An empty object means form-only, for backwards compatibility. Your server MUST NOT send a mode the client hasn't declared, so check before you send a URL request or you'll strand the user.

And handle all three actions. Accept carries content. Decline is an explicit no — offer an alternative. Cancelmeans the user hit Escape or the dialog fell over — you can reasonably ask again later. I've reviewed servers that treat all three as “falsy, therefore fail,” and the result is an agent that keeps re-prompting a user who already said no.

The URL-Mode Phishing Trap

URL mode is the right answer for credentials, and it ships with an attack the spec spells out in full. It's worth reading slowly, because it bites servers written by people who did everything else correctly:

  • Alice, a legitimate user of your server, triggers a tool that needs third-party authorization.
  • Your server generates the connect URL and returns it as a URL mode elicitation.
  • Instead of clicking it, Alice sends the link to Bob — another user of the same server.
  • Bob opens it and completes the OAuth flow, believing he's connecting his own account.
  • Your server binds Bob's third-party tokens to Alice's identity. Alice now has Bob's account.

The mitigation is that your server MUST verify the person who opensthe URL is the person the elicitation was generated for. The pattern the spec suggests: don't send the user straight to the third party. Send them to a connect route you own, check the browser session there, compare the session's sub claim against the subject from the MCP authorization server, and only then redirect into the OAuth flow.

Three related rules that are easy to trip over. Never put credentials or PII in the elicitation URL. Never hand over a URL that is already pre-authenticated to a protected resource — a malicious client could replay it to impersonate the user. And never use URL mode to authorize the client to your own server; that's ordinary MCP authorization, and conflating the two is how you end up with token passthrough, which the spec forbids outright.

If that sounds like a lot of surface area for one dialog box — it is. It's the same lesson as tool poisoning: every place MCP lets a server influence what the user or the model sees is a place someone will eventually abuse.

Migrating a Server Off Sampling, Step by Step

I did this on a server last week and it took under an hour, mostly because the honest answer to step two is usually “this never needed a model.”

  • Grep for sampling/createMessage and list every call site. Write down what each one is for in one sentence.
  • Split the list in two: calls that needed a generation, and calls that needed a human decision. In my case four of six were disguised confirmations.
  • Turn the human decisions into form mode elicitation with a flat primitive schema. They get faster and cheaper, because no inference happens at all.
  • Give the real generations a provider key, a base URL and a model in config, and call the API directly. Inference is your cost now, so cap it — token budgets, a per-call ceiling, and a kill switch.
  • Move anything secret to URL mode, bound to a verified identity. Never a form field.
  • Restructure the handlers to be re-entrant, because the client now retries the whole call with the answer.

The cost question is the one people flinch at. Sampling made inference free for the server author; direct calls don't. But the calls that survive the split are few and small, and you now control the model, so you can put the cheap one on a classification that never needed a frontier model. That, plus watching what your tool definitions do to the context window, is where the actual money is — I measured the context tax MCP servers charge you and it dwarfed the inference in most of my setups.

One last note on Roots and Logging, since they went in the same SEP. Roots is a five-minute fix: take the directory as a tool parameter or read it from config. Logging is the better trade — writing to stderr and exporting OpenTelemetry spans gives you traces the MCP logging feature never could.

What to Take Away

  • Sampling, Roots and Logging were deprecated together in spec 2026-07-28 under SEP-2577. Earliest removal is a revision released on or after 2027-07-28.
  • Don't start anything new on sampling. The migration path in the registry is a single line: call LLM provider APIs directly.
  • Elicitation is not deprecated. Sampling asks the model; elicitation asks the human — and only one of those has an alternative.
  • Form mode is for ordinary structured data: flat object, primitives only, four string formats, defaults allowed.
  • URL mode is mandatory for passwords, API keys, tokens and payment details. Form fields land in the client and therefore in the model's context.
  • Elicitation now arrives inside an InputRequiredResult and the client retries the original tool call. Handlers must be re-entrant, not blocking.
  • Handle accept, decline and cancel separately. Treating all three as failure produces an agent that nags a user who already declined.
  • URL mode has a real phishing attack. Verify that whoever opens the URL is the user the elicitation was issued for, before you bind any tokens.

Running an MCP Server That Needs to Survive 2027?

I build and audit production MCP servers — migrations off deprecated features, elicitation flows that handle every response action, URL-mode auth bound to a verified identity, and the token accounting to keep it affordable. If your server still calls back into the client for things the spec is taking away, let's fix it before the removal window.

Related Posts

AI Agents

MCP Apps: Interactive UI Inside Your AI Client

MCP Apps are the first official Model Context Protocol extension (shipped Jan 26, 2026): an MCP tool can now return a real interface — a dashboard, form, chart, or multi-step wizard — that the client renders in a sandboxed iframe right inside the chat, instead of plain text. Three parts make it work: a ui:// resource (bundled HTML/JS), a tool linked to it via _meta.ui.resourceUri, and an App class that speaks two-way JSON-RPC over postMessage so the UI can receive the tool result, call server tools, and push the user's selection back into the model's context. It's a cross-client standard — Claude, ChatGPT, VS Code, and Goose already render the same UI resource. Reach for an App only when the user needs to see or manipulate something; plain text tools still win for short answers. Bonus: rendering data in a UI instead of narrating 500 rows back through the model can cut token cost, not add it.

AI Agents

MCP Server Security: How to Stop Tool Poisoning

Tool poisoning is when an MCP server hides instructions inside its own tool descriptions — text your agent reads as commands and you almost never see. The model obeys it because, inside the context window, a description and a system prompt are the same kind of thing, which is why no system prompt fixes this. The four controls that hold are structural: approve individual tools instead of whole servers, pin exact versions and diff the tool descriptions in CI so a rug pull is a failed build, keep secrets out of the model's context entirely, and run local servers in a container with no network access.

AI Agents

How to Authenticate a Remote MCP Server

A remote MCP server is a public HTTP endpoint that hands an AI model your tools, so it needs real auth. The spec's answer is OAuth 2.1: return 401 with a WWW-Authenticate header pointing at your protected resource metadata, let the client discover your authorization server and run authorization code with PKCE, then validate that your own canonical URL appears in the token's audience claim. That last step is the one most implementations skip — and skipping it turns your server into a confused deputy that accepts tokens minted for somebody else. Only about 8.5% of public MCP servers implement the flow at all, and roughly half hard-code credentials in config files. If you're running a private, single-tenant server with clients you own, a reverse proxy enforcing service tokens is a real trust boundary that ships in an afternoon.