Elicitation

By the Syncfusion DevTutorials team · Last updated

How MCP servers ask users for input: elicitation/create in form and URL mode, client capabilities, flat form schemas, host and server validation, accept, decline and cancel, and keeping secrets out of forms.

At a hotel front desk, the clerk sometimes slides a short card across the counter: "Please write your car's plate number." It is a small form with a few blanks. When it is time to pay, you use the card machine instead of writing your card number on that form. Each process is designed for different information.

Elicitation is how an MCP server asks the user for information. For simple, non-sensitive answers, the host can show a form. For sensitive steps, such as signing in or paying, URL mode sends the user to a page outside the chat; the host should not collect those secrets in the form.

The main idea: a server asks with elicitation/create inside an MRTR round. In form mode, it sends a simple schema for the host to display. In URL mode, it sends a link for a separate page. The user can accept, decline or cancel; these actions do not mean the same thing.

A Real-World Example

Brightgrid has two tools that need the customer:

  • book_return needs a reason, an email, the number of boxes and a pickup date. That is a form: four simple fields.
  • connect_store_account needs the customer to sign in to their store account. Passwords must never pass through the chat, so the server sends a link to the store's own sign-in page.

In both cases, the host identifies the connected server making the request, explains what is being asked, and lets the customer say no.

In simple words: use forms for simple, non-sensitive facts and URL mode for sensitive steps. The host must keep the user in control.

What You Will Learn

In this lesson, you will learn:

  1. How a server asks for input, and when it may.
  2. The field types a form can use, and their rules.
  3. How the host shows the form and checks the answers.
  4. What accept, decline and cancel mean.
  5. When to use URL mode instead of a form.

The MCP Lab shows both modes. book_return asks with forms. connect_store_account asks you to open a page. The page is not real, so nothing opens, but you can see exactly what the server sends.

In form mode, the server sends a schema and the host shows a form; the answers return in the next round. In URL mode, the server sends a link and the user completes the sensitive step on a separate page.

Asking for Input

How It Travels

In the 2026-07-28 spec, elicitation always travels through MRTR. The server returns input_required, and one of the inputRequests has the method elicitation/create. Its params have:

  • mode: "form" or "url".
  • message: what to ask, in plain words.
  • For forms: requestedSchema, the fields.
  • For URL mode: url, the page to open.

Only If the Client Can

The client reports what it supports in its capabilities on each request:

  • elicitation: { form: {} }: the host can show forms.
  • elicitation: { url: {} }: the host can open pages.
  • An empty elicitation: {} means forms only.

A server must not ask for a mode the client does not support. If forms are unavailable, the tool can explain what information it needs in a normal result instead.

Check what the client can do

// A server may only ask if the client said it can show forms (or open URLs).
function canAsk(clientCapabilities, mode) {
  const e = clientCapabilities.elicitation;
  if (!e) {
    return false;
  }
  if (mode === "url") {
    return "url" in e;
  }
  return "form" in e || Object.keys(e).length === 0; // an empty object means form mode
}

const clients = {
  "MCP Lab": { elicitation: { form: {}, url: {} } },
  "Old CLI": {},
  "Form-only app": { elicitation: { form: {} } },
};
for (const [name, caps] of Object.entries(clients)) {
  console.log(
    name.padEnd(14),
    "form:",
    canAsk(caps, "form") ? "yes" : "no ",
    " url:",
    canAsk(caps, "url") ? "yes" : "no",
  );
}

Expected output:

MCP Lab        form: yes  url: yes
Old CLI        form: no   url: no
Form-only app  form: yes  url: no

Form Fields

Before a host can show a form, the server needs a way to describe what information it wants. Instead of designing a custom form for every tool, elicitation uses a schema to tell the host which fields to display.

Simple, Flat Schemas

A form schema is a small JSON Schema object. To keep forms easy to display, it can contain only flat fields of simple types, not nested objects. Each field can be:

TypeExtrasHost shows
stringminLength, maxLength, format: email, uri, date, date-timeA text, email or date box
number, integerminimum, maximumA number box
booleanA checkbox
string with enumA list of allowed valuesA drop-down list

Each field can have a title and a description. The required list says which fields must be filled.

The next example maps these schema fields to likely host controls. Hosts may display the same field types differently.

From schema to form controls

// A requestedSchema, and the form control a host would show for each field.
const requestedSchema = {
  type: "object",
  properties: {
    reason: {
      type: "string",
      title: "Reason",
      enum: ["Broken", "Wrong item", "No longer needed"],
    },
    email: { type: "string", title: "Email for the label", format: "email" },
    boxes: { type: "integer", title: "Number of boxes", minimum: 1, maximum: 3 },
    pickup_date: { type: "string", title: "Pickup date", format: "date" },
    gift_wrap: { type: "boolean", title: "Gift wrap the replacement" },
  },
  required: ["reason", "email", "boxes", "pickup_date"],
};

function control(f) {
  if (f.enum) {
    return "drop-down list";
  }
  if (f.type === "boolean") {
    return "checkbox";
  }
  if (f.type === "integer" || f.type === "number") {
    return `number box (${f.minimum}-${f.maximum})`;
  }
  if (f.format === "date") {
    return "date picker";
  }
  if (f.format === "email") {
    return "email box";
  }
  return "text box";
}

for (const [key, f] of Object.entries(requestedSchema.properties)) {
  const star = requestedSchema.required.includes(key) ? "*" : " ";
  console.log(`${star} ${f.title.padEnd(26)} ${control(f)}`);
}

Expected output:

* Reason                     drop-down list
* Email for the label        email box
* Number of boxes            number box (1-3)
* Pickup date                date picker
  Gift wrap the replacement  checkbox

The Host's Job

When a server requests information, the user needs to understand who is asking and what will happen before sending any data. The host is responsible for making that interaction clear.

Show the Form Clearly

A good host:

  • Says which server is asking.
  • Shows the server's message and every field with its title.
  • Lets the user review the answers before sending.
  • Always offers Decline and Cancel.

Check Before Sending

The host checks each field against its rules: required fields filled, emails that look like emails, numbers inside their range. The lab's form does this. Try a number of boxes like 7, or an email without @. A host-side check gives quick feedback, but the server still needs to validate submitted answers.

The Server Checks Again

The server must check answers again because it cannot trust the client. In the SDK used in this course, acceptedContent(responses, key, schema) returns an accepted answer only when it matches the schema. The rule functions below are simplified examples: real email and date validation should check the full value, not only its shape.

Server-side checks

// The server checks the answer again. It never trusts the form.
const rules = {
  reason: (v) =>
    ["Broken", "Wrong item", "No longer needed"].includes(v) || "not a listed reason",
  email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || "not an email address",
  boxes: (v) => (Number.isInteger(v) && v >= 1 && v <= 3) || "must be 1 to 3",
  pickup_date: (v) => /^\d{4}-\d{2}-\d{2}$/.test(v) || "must be YYYY-MM-DD",
};

function check(content) {
  const problems = [];
  for (const [key, rule] of Object.entries(rules)) {
    if (!(key in content)) {
      problems.push(`${key}: missing`);
      continue;
    }
    const ok = rule(content[key]);
    if (ok !== true) {
      problems.push(`${key}: ${ok}`);
    }
  }
  return problems.length ? problems.join("; ") : "ok";
}

console.log(
  check({
    reason: "Broken",
    email: "asha@example.com",
    boxes: 2,
    pickup_date: "2026-10-12",
  }),
);
console.log(
  check({ reason: "Bored", email: "asha", boxes: 7, pickup_date: "12/10/2026" }),
);

Expected output:

ok
reason: not a listed reason; email: not an email address; boxes: must be 1 to 3; pickup_date: must be YYYY-MM-DD

The Three Answers

Accept, Decline, Cancel

The user may provide the requested information, refuse the request, or close the form without choosing. The server needs to handle these outcomes differently.

An elicitation response uses one of three actions:

  • accept: the user sent the form. For forms, content holds the answers.
  • decline: the user said no on purpose.
  • cancel: the user closed the form without choosing.

Handle each answer

// The three answers a user can give, and what a server should do with each.
function handle(response) {
  switch (response.action) {
    case "accept":
      return "use the content: " + JSON.stringify(response.content);
    case "decline":
      return "the user said no: do not proceed with the requested action";
    case "cancel":
      return "the user closed the form: stop; you may offer to ask again later";
    default:
      return "unknown answer: treat it like cancel";
  }
}

for (const r of [
  { action: "accept", content: { ok: true } },
  { action: "decline" },
  { action: "cancel" },
]) {
  console.log(r.action.padEnd(8), "->", handle(r));
}

Expected output:

accept   -> use the content: {"ok":true}
decline  -> the user said no: do not proceed with the requested action
cancel   -> the user closed the form: stop; you may offer to ask again later

Treat decline and cancel kindly. Explain what will or will not happen next. Do not proceed with an action the user declined, and do not ask again immediately.

URL Mode

Not every interaction can safely happen inside a host-generated form. Some actions involve secrets or sensitive information that should stay on a dedicated website.

When to Use It

Some information should not be collected in a form, where it could enter the host, model, or logs:

  • passwords and sign-in steps,
  • card numbers and payments,
  • API keys and other secrets.

For these, the server can use URL mode to send the user to a separate page, such as the store's sign-in page. The user completes the sensitive step there. The response to the server is only accept, decline or cancel; it carries no form data. URL mode does not make a page trustworthy by itself, so the host should show where the link goes and let the user choose whether to open it.

The next code uses field names as a simple teaching heuristic. It is not a security control: a real app must classify sensitive information by what it contains, not only by the field name.

Form or URL?

// Form mode or URL mode? Sensitive data never goes through a form.
const SENSITIVE = /password|card|cvv|pin|ssn|secret|token|api.?key/i;

function chooseMode(fields) {
  const risky = fields.filter((f) => SENSITIVE.test(f));
  return risky.length
    ? `URL mode: send the user to a trusted page (sensitive: ${risky.join(", ")})`
    : "form mode: ask in the host's form";
}

console.log(chooseMode(["reason", "pickup_date"]));
console.log(chooseMode(["card_number", "cvv"]));
console.log(chooseMode(["store_password"]));

Expected output:

form mode: ask in the host's form
URL mode: send the user to a trusted page (sensitive: card_number, cvv)
URL mode: send the user to a trusted page (sensitive: store_password)

A link from a server can still lead to a fake sign-in page. The host should show the full address, especially the site name, and must not open it without the user's consent. URL mode does not verify the destination; users should check that the site is the one they expect.

Common mistakes

  • Asking without the capability. If the client didn't declare elicitation, don't send it.
  • Nested schemas. Forms take flat fields of simple types only.
  • Secrets in forms. Keep passwords, card numbers and keys out of forms. A URL flow should send the user only to a destination the host makes clear.
  • Ignoring decline. Decline means no. Stop, and say nothing was done.
  • Unclear messages. "Input needed" says nothing. Say what you need and why, in one sentence.

Exercises

Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.

What to Remember

  • Elicitation is an elicitation/create request inside an MRTR round.
  • Only ask if the client declared elicitation (form, url, or empty for forms).
  • Form mode: a flat schema of strings (with formats), numbers, booleans and enums.
  • The host identifies the connected server, checks fields and offers decline and cancel. The server checks the answers again.
  • Answers are accept (with content for forms), decline or cancel.
  • URL mode can be used for sign-in, payment and secrets. Its response carries an action, not form data.

Next: Roots, Sampling and Logging, three older features that are now deprecated, and what to use instead.