# Elicitation

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Client Interaction
Source: https://www.devtutorials.ai/learn/mcp/client-interaction/elicitation

> 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.

## 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.

```js runnable title="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:

```text
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:

| Type | Extras | Host shows |
| --- | --- | --- |
| `string` | `minLength`, `maxLength`, `format`: `email`, `uri`, `date`, `date-time` | A text, email or date box |
| `number`, `integer` | `minimum`, `maximum` | A number box |
| `boolean` | | A checkbox |
| `string` with `enum` | A list of allowed values | A 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.
```js runnable title="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:

```text
* 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.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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.
```js runnable title="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:

```text
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.
