What you need to know

Most people who build a Model Context Protocol server stop at the first two primitives. They expose a handful of tools the model can call, maybe a few resources it can read, and ship it. That gets you a long way — an agent that can query your database, file a ticket or fetch a document. But it leaves the server in a purely reactive posture: it waits to be called, does the one thing, and returns. It cannot reason. It cannot ask. If you have built your first MCP server with FastMCP and wondered how to make it do something more collaborative, this guide is the next step.

The two primitives that change the game are sampling and elicitation, and the quickest way to hold them in your head is by who the server is talking to. With sampling, a server pauses mid-task and asks the client's language model to generate a completion — it borrows the model's reasoning without ever holding its own API key. With elicitation, a server pauses mid-task and asks the human user for a piece of structured input or a confirmation. One direction reaches the machine; the other reaches the person. Together they turn a static tool provider into a server that can think and check in.

This matters most for anything consequential. A read-only server can afford to be a black box. A server that migrates a schema, moves money or changes production configuration needs a human in the loop — and sampling plus elicitation are precisely the mechanisms the protocol gives you to build that loop properly. As of mid-2026, the MCP specification defines both as first-class client capabilities, with a strong emphasis on human oversight baked into each. This guide walks through both flows with code, then builds a worked human-in-the-loop example and closes on the security discipline that keeps it safe. If you are the kind of engineer who ships this sort of thing, it is worth putting your work somewhere the right people can see it — a free Verified Builder profile takes two minutes and puts your projects in front of teams hiring across India and the UK.

Sampling and elicitation: the two directions

The cleanest mental model treats an MCP server as sitting between two counterparties. On one side is the client's model — the language model the host application (Claude Desktop, an IDE, a bespoke agent) is driving. On the other side is the human user sitting in front of that host. Ordinary tools and resources flow one way: the model calls into the server. Sampling and elicitation let the server call back out — and the destination is the whole distinction.

Sampling is a server-to-model request. Midway through handling a tool call, the server decides it needs the model to reason about something — to summarise a document it just fetched, classify a record, or draft a candidate response. Rather than embed its own LLM and its own billing, the server sends a sampling/createMessage request back to the client and asks the client's model to produce the completion. The client runs it, subject to human review, and returns the result. The server never sees an API key; the compute and the cost sit with the host. This is what lets a lightweight server punch above its weight — it can be intelligent without being a model provider.

Elicitation is a server-to-user request. Sometimes the thing the server needs is not reasoning but a decision or a fact that only the human holds — a missing parameter, a choice between options, an explicit go-ahead on something destructive. The server sends an elicitation request carrying a JSON schema that describes the fields it wants, the client renders that schema as a form, the user fills it in or declines, and the structured answer comes back. It is the protocol's supported way for a server to say "I need you to tell me something" without hard-coding a prompt into the model's context and hoping it asks.

Both are optional client capabilities. A server must be written to degrade gracefully when the client does not support them — not every host implements sampling or elicitation, and a well-behaved server checks capabilities during initialisation and has a fallback. But where they are supported, they are what separate a genuinely agentic server from a glorified function library. The rest of this guide is about wiring them up without cutting the human out of the loop.

Sampling Elicitation
Who initiates The server, mid-task The server, mid-task
Direction Server → client's language model Server → human user
What comes back A model-generated completion Structured input matching a JSON schema (or a decline)
Typical use Summarise, classify, draft, analyse — reasoning the server needs Confirm a destructive action, supply a missing field, choose an option
Security posture Human should be able to review/edit the prompt and review the response Must never request secrets/PII inline; URL-mode for credentials and payment

Building a sampling flow

A sampling request is the server asking the client to run its model. The message the server sends is a sampling/createMessage request: a set of messages, a hint about which model class you would prefer, sampling parameters such as a token limit, and optionally a system prompt. Crucially, the server does not get to reach past the client and hit the model directly — the request goes to the client, which is where the human oversight lives.

Here is a sampling request issued from inside a tool handler. Imagine a documentation server that has just fetched a long release note and wants a two-line summary to attach to a changelog entry. Rather than ship its own model, it asks the client's.

// Inside an MCP server tool handler.
// The server borrows the client's model to summarise text it fetched.

const result = await server.createMessage({
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: `Summarise the following release note in two sentences,
plain and factual, no marketing tone:\n\n${releaseNote}`
      }
    }
  ],
  modelPreferences: {
    // Hints, not hard requirements — the client picks the actual model.
    hints: [{ name: "claude-3-5-haiku" }],
    intelligencePriority: 0.3,
    speedPriority: 0.8
  },
  systemPrompt: "You are a precise technical summariser.",
  maxTokens: 200
});

// result.content is the completion the client's model produced,
// AFTER any human review the client applied.
const summary = result.content.type === "text" ? result.content.text : "";

The important thing to internalise is what happens between your createMessage call and the result landing back in your handler. The client is not obliged to run your prompt blindly. The specification is clear that human oversight is the intended default: there should always be a human able to review and deny a sampling request, clients should let users see and edit the prompt your server sent before it reaches the model, and users should be able to review the model's response before it is delivered back to you. Your server does not control any of that — and it should not want to. Write your prompts as if a person will read every one of them, because in a well-built client, one can.

That has a design consequence. Because a human may be watching, keep the prompts you send legible and free of anything you would not want a user to see attributed to your server. Do not stuff secrets into a sampling prompt to smuggle them past the model. Do not phrase a request so that a hurried user rubber-stamps something they would refuse if it were spelled out. The oversight is a feature; build to it rather than around it. For teams thinking about how tool calls and their arguments should be shaped so a human can reason about them, our guide on designing tools for AI agents covers the schema-and-error discipline that makes reviewable prompts easier to write.

Pro tip

Use modelPreferences as hints, never assumptions. A sampling request may be served by a small fast model or a large one depending on the client and the user's settings, so write prompts that do not depend on a specific model's quirks. Set speedPriority high and intelligencePriority low for cheap mechanical tasks like summarising or tagging — you rarely need frontier reasoning to compress a diff, and the user pays for the tokens.

Building an elicitation flow

Elicitation is the other half of the loop: the server asking the human. The request carries a message for the user and a requestedSchema — a JSON schema describing the fields you want back. The client is responsible for turning that schema into a form, collecting the input, and returning it as structured data along with an action that tells you what the user did: they accepted and gave you data, they declined, or they cancelled outright. Your server must handle all three.

Here is an elicitation request from a deployment server that has everything it needs except a target environment and a confirmation. Rather than guess, it asks.

// Inside an MCP server tool handler.
// The server asks the user for structured input before proceeding.

const response = await server.elicitInput({
  message: "Confirm the deployment target and window before I proceed.",
  requestedSchema: {
    type: "object",
    properties: {
      environment: {
        type: "string",
        enum: ["staging", "production"],
        description: "Which environment to deploy to"
      },
      confirmed: {
        type: "boolean",
        description: "I understand this will restart live services"
      }
    },
    required: ["environment", "confirmed"]
  }
});

if (response.action === "accept" && response.content?.confirmed) {
  await deploy(response.content.environment);
} else {
  // "decline" or "cancel" — do NOT proceed. Return a clean, non-destructive result.
  return { content: [{ type: "text", text: "Deployment cancelled by user." }] };
}

A few rules keep elicitation well-behaved. Keep the schema flat and primitive — strings, numbers, booleans, enums and short arrays render cleanly as a form; deeply nested objects do not, and a client may not support them. Always treat decline and cancel as first-class outcomes that leave the world unchanged; a server that proceeds anyway when the user backs out has defeated the entire point. And be sparing: an elicitation interrupts the human, so ask only when you genuinely need something they alone can give.

There is one category of input elicitation deliberately does not handle inline, and it matters. When the thing you need is a credential, an OAuth consent or a payment — anything that must be entered into a trusted system rather than typed into a chat form — the pattern is URL-mode elicitation. Instead of asking for the secret through the schema, the server returns a link the user opens outside the client: a bank's OAuth page, a payment provider's checkout, an identity provider's consent screen. The secret is entered directly into the system that owns it and never touches the model, the transport or your server. A Bengaluru fintech wiring an MCP server to a payments API would use URL-mode to bounce the user to the provider's hosted consent flow rather than ever eliciting a card number, and that is not a nicety — as the next sections make clear, inline elicitation for secrets is prohibited.

From a verified Builder

"The mistake I see most often is treating elicitation as a way to grab whatever the server is missing, including things it has no business asking for. It is a consent primitive, not a data-harvesting one. If the field is a password or a card number, you are holding it wrong — send the user out to the system that owns that secret and let them enter it there. Keep the humans in the loop, keep the secrets out of the transport."

— Rishi Kora, Verified Builder · London, United Kingdom

Every article here is written by a Verified Builder. Want your name on the next one?

AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.

Become a Verified Builder →

The human-in-the-loop approval pattern

Sampling and elicitation are most powerful in combination, and the canonical example is a server that has to do something risky. Consider a database-migration MCP server — the sort of thing a platform team might build so an agent can apply schema changes safely. On its own, letting an agent run migrations is alarming. Wire in both primitives and it becomes a controlled, auditable flow with a person at the gate.

Walk the sequence. An engineer asks the agent to add a column and backfill it. The agent calls the migration server's tool with the proposed change. Now the server does something a plain tool server cannot: it uses sampling to ask the client's model for an impact analysis. It sends the proposed DDL and the current schema back through createMessage and asks the model to reason about locking behaviour, affected tables, and whether the change is reversible. The server did not need its own model to do this — it borrowed the client's, under human review.

The model comes back with an assessment. If the change is low-risk — a nullable column on a small table — the server may proceed and simply report what it did. But if the analysis flags the change as high-risk — a non-concurrent index on a hundred-million-row table that will lock writes — the server uses elicitation to get an explicit human go-ahead before touching anything. It sends the user the model's risk summary and a schema asking for a typed confirmation, and only proceeds on an accept.

// Migration server: sample for analysis, elicit for approval on high risk.

// 1. Borrow the client's model to assess the proposed change.
const analysis = await server.createMessage({
  messages: [{
    role: "user",
    content: { type: "text", text:
      `Assess this migration for lock risk and reversibility.
Return a JSON object {"risk":"low|high","reason":"..."}.

Current schema:\n${schema}\n\nProposed change:\n${ddl}` }
  }],
  maxTokens: 400
});

const verdict = JSON.parse(extractText(analysis));

// 2. On high risk, require an explicit human decision before proceeding.
if (verdict.risk === "high") {
  const ok = await server.elicitInput({
    message: `High-risk migration. ${verdict.reason}\nApprove to proceed?`,
    requestedSchema: {
      type: "object",
      properties: {
        approve: { type: "boolean", description: "Yes, run this migration now" }
      },
      required: ["approve"]
    }
  });
  if (ok.action !== "accept" || !ok.content?.approve) {
    return { content: [{ type: "text", text: "Migration held — not approved." }] };
  }
}

await applyMigration(ddl);

This is the shape of a mature agentic server: reason with sampling, decide with elicitation, act only on approval. The same skeleton generalises well beyond migrations. A London health startup building an MCP server that drafts patient-facing letters might sample the model to generate a draft, then elicit clinician sign-off before anything is sent — the machine drafts, the human approves, and the approval is a structured, logged event rather than a hopeful assumption buried in a prompt. If your server executes work by running generated code rather than fixed tools, the same review gates apply; our guide on code execution with MCP and code mode covers where those gates belong when the agent is writing the code it runs.

Security and consent

Both primitives hand a server new reach, and reach is exactly what an attacker wants. The discipline that keeps sampling and elicitation safe comes down to a few non-negotiables, and the specification states them plainly.

First, elicitation must not be used to extract sensitive information. Passwords, full card numbers, API keys, national identity numbers and similar secrets are out of bounds for the ordinary schema-driven form. This is not a style preference — a server that elicits a password has put a credential into the model's client and the MCP transport, exactly where it should never be. Anything that qualifies as a secret or as personal data you have no legitimate need to hold should go through URL-mode, entered directly into the system that owns it. A Bengaluru fintech and a London health startup live under DPDP and UK GDPR respectively, and "the form asked for it" is not a lawful basis for collecting a person's sensitive data through a chatbot.

Second, keep humans in the loop and treat prompts as reviewable. The oversight the specification builds into sampling — review, edit, deny — is a security control, not a courtesy. Design your server so that every sampling prompt is something you would be content for a user to read, and never rely on a prompt slipping past unexamined. The same goes for the responses you get back: treat model output returned via sampling as untrusted input to your server, validate it, and never pipe it straight into a shell, a query or a filesystem path.

Third, remember that a server that can sample is a server that can be steered by whatever ends up in its context — which makes prompt injection a live concern. If your migration server fetches a release note that secretly instructs the model to mark every change as low-risk, your sampling call has just been hijacked. The mitigations are the same layered defences that protect any agent: our guide on defending AI agents against prompt injection lays out the defence-in-depth posture — treat retrieved content as hostile, keep the human confirmation gate for anything destructive, and never let a sampled verdict be the only thing standing between an agent and production.

Watch out

Do not use elicitation as a soft channel for data you could not otherwise justify collecting. A form that asks "just confirm your date of birth and PAN to continue" is a data-protection incident waiting to happen, whatever the server's intent. If a flow needs a credential, a consent or a payment, use URL-mode elicitation and send the user to the trusted system. If it needs personal data, ask whether you have a lawful basis to hold it at all before you ask for it through an MCP form.

When to use which, and where to stop

The decision between sampling and elicitation is almost always answered by one question: does the answer live in the model's reasoning or in the human's head? If a competent model could work it out from the context — a summary, a classification, a draft, a risk assessment — sample it. If it requires a fact only the user knows, a choice only they can make, or a permission only they can grant, elicit it. Reasoning is sampled; decisions and consent are elicited. The migration example uses both precisely because it needs both: the model's reasoning to assess risk, the human's consent to act on it.

There is also a question of when to reach for these primitives at all. Not every server needs them. If your server is genuinely read-only — it fetches, it queries, it never changes anything consequential — plain tools and resources are enough, and adding sampling or elicitation is complexity you will not use. Reach for sampling when a tool would be meaningfully better if it could reason mid-task without you shipping a model. Reach for elicitation when a tool does something you would not want it to do without a human's explicit say-so. And build both to degrade gracefully: check the client's capabilities at initialisation, and have a sensible fallback for hosts that support neither.

The through-line is that these two primitives exist to put a person back into an automated loop, not to take them out of it. A server that samples keeps the human able to review the reasoning; a server that elicits makes the human's decision an explicit, structured, logged event. Used well, they turn a server that merely acts into one that reasons, checks in, and waits for the nod — which is exactly the posture you want for anything that touches a schema, a patient record or a payment. As of mid-2026 the MCP specification defines both as capabilities a client opts into, with human oversight written into the design of each. Build to that design, keep the secrets out of the transport, and keep the human at the gate.