Guide · MCP Protocol

MCP server elicitation

Most MCP tools collect all the information they need from the tool call arguments. But sometimes a tool starts executing and discovers it needs something it couldn't have asked for up front — a password, a confirmation, a choice between options, or a value only the user can provide. MCP's elicitation capability exists for exactly this case: a running tool can pause, send an elicitation/create request to the client, and wait for the user to respond before continuing. The non-obvious parts are capability negotiation (not all clients support elicitation), designing elicitation schemas so hosts render them correctly, and handling the three possible response actions — accept, decline, and cancel — without leaving the tool in a broken state.

TL;DR

Elicitation lets a tool call pause mid-execution and prompt the user for input. Declare elicitation: true in your server capabilities during initialization. In a tool handler, call server.requestElicitation({ message, requestedSchema }) — the host renders a form using the JSON Schema and returns the user's response. Handle all three actions: accept (user filled the form), decline (user explicitly refused), and cancel (user dismissed without deciding). Check whether the connected client supports elicitation before calling it; if not, either proceed with what you have or return a clear error. Never treat elicitation as a substitute for well-designed tool parameters — use it only for information that is genuinely impossible to include in the initial call arguments.

What elicitation is and what it's for

Standard MCP tool calls follow a one-shot pattern: the LLM sends tools/call with arguments, the server executes the tool, and the server returns a result. All information the tool needs must be present in the arguments at call time.

Elicitation adds a mid-call round trip. A tool handler can suspend execution, send a structured request to the host application, and receive the user's response before producing a result. This is different from tool approval (confirming an action before it runs) — elicitation collects information, not permission.

Valid use cases for elicitation:

Use cases that should not use elicitation:

Capability negotiation

Elicitation is an optional capability. Before calling requestElicitation, verify that the connected client supports it. The MCP SDK exposes this via the client capabilities object negotiated during initialize.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
}, {
  capabilities: {
    tools: {},
    elicitation: {},  // declare that this server uses elicitation
  },
});

// In a tool handler, check before calling
server.tool('sync_files', { ... }, async (args, extra) => {
  const clientCapabilities = extra.clientCapabilities;

  if (!clientCapabilities?.elicitation) {
    // Client doesn't support elicitation — fall back or fail gracefully
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          error: 'This tool requires elicitation support. Use a client that supports MCP elicitation (e.g. Claude Desktop 1.6+) or provide the --two-factor-code argument directly.',
        }),
      }],
      isError: true,
    };
  }

  // Safe to elicit
  const result = await extra.requestElicitation({
    message: 'Enter your two-factor authentication code:',
    requestedSchema: {
      type: 'object',
      properties: {
        code: {
          type: 'string',
          title: 'Two-Factor Code',
          description: 'The 6-digit code from your authenticator app',
          pattern: '^[0-9]{6}$',
        },
      },
      required: ['code'],
    },
  });

  if (result.action !== 'accept') {
    return {
      content: [{ type: 'text', text: 'Sync cancelled — two-factor code not provided.' }],
      isError: false,
    };
  }

  const code = result.content.code as string;
  // proceed with sync
});

Elicitation schema design

The requestedSchema field takes a JSON Schema object. The host application renders a UI from this schema. Not all JSON Schema features are supported — stick to the subset that maps cleanly to form fields:

JSON Schema typeTypical host renderingUseful keywords
stringSingle-line text inputtitle, description, minLength, maxLength, pattern, enum, format
string with format: "password"Password input (masked)minLength
string with enumDropdown / radio buttonsenumNames (unofficial but common)
booleanCheckbox / toggletitle, description
integer or numberNumeric inputminimum, maximum
string with format: "date"Date picker

Keep schemas flat. Nested objects are valid JSON Schema but most host UIs render them poorly. If you need multiple fields, use a flat object with multiple top-level properties.

// Good: flat schema, clear field titles
const schema = {
  type: 'object',
  properties: {
    recipient: {
      type: 'string',
      title: 'Recipient email',
      format: 'email',
      description: 'The email address to send the report to',
    },
    format: {
      type: 'string',
      title: 'Export format',
      enum: ['pdf', 'csv', 'xlsx'],
      description: 'Choose the file format for the exported report',
    },
    include_drafts: {
      type: 'boolean',
      title: 'Include drafts',
      description: 'Include unpublished draft entries in the export',
    },
  },
  required: ['recipient', 'format'],
};

// Bad: nested schema — few hosts render this correctly
const badSchema = {
  type: 'object',
  properties: {
    destination: {
      type: 'object',
      properties: {
        email: { type: 'string' },
        name: { type: 'string' },
      },
    },
  },
};

Handling the three response actions

requestElicitation resolves to an object with an action field that is one of three values. Handle all three — do not assume the action will always be accept.

ActionMeaningHandler behavior
acceptUser submitted the formRead result.content and continue executing
declineUser explicitly clicked "Decline" or "No"Return a clear message explaining the tool cannot proceed without the input; do not retry
cancelUser dismissed the dialog without decidingTreat the same as decline — the operation is aborted; do not leave work in a partial state
const result = await extra.requestElicitation({
  message: 'Which project should the task be added to?',
  requestedSchema: {
    type: 'object',
    properties: {
      project_id: {
        type: 'string',
        title: 'Project',
        enum: projectIds,
        description: 'Select the destination project',
      },
    },
    required: ['project_id'],
  },
});

switch (result.action) {
  case 'accept': {
    const projectId = result.content.project_id as string;
    await createTask(projectId, args.title);
    return { content: [{ type: 'text', text: `Task created in project ${projectId}.` }] };
  }
  case 'decline':
    return { content: [{ type: 'text', text: 'Task creation cancelled — no project selected.' }] };
  case 'cancel':
    return { content: [{ type: 'text', text: 'Task creation cancelled.' }] };
}

Retry loops for invalid input

JSON Schema validation in the host UI prevents structurally invalid input (wrong type, pattern mismatch). But semantic validation — checking that the entered ID actually exists, that the password is correct, that the date is in the future — has to happen in the tool handler. When semantic validation fails, you can elicit again.

const MAX_ATTEMPTS = 3;
let attempts = 0;
let lastError: string | null = null;

while (attempts < MAX_ATTEMPTS) {
  const message = lastError
    ? `${lastError} Please try again (attempt ${attempts + 1} of ${MAX_ATTEMPTS}):`
    : 'Enter your API key:';

  const result = await extra.requestElicitation({
    message,
    requestedSchema: {
      type: 'object',
      properties: {
        api_key: {
          type: 'string',
          title: 'API Key',
          format: 'password',
          minLength: 32,
        },
      },
      required: ['api_key'],
    },
  });

  if (result.action !== 'accept') {
    return { content: [{ type: 'text', text: 'Authentication cancelled.' }] };
  }

  const key = result.content.api_key as string;
  const valid = await validateApiKey(key);

  if (valid) {
    // proceed
    break;
  }

  lastError = 'That API key was not accepted.';
  attempts++;
}

if (attempts >= MAX_ATTEMPTS) {
  return {
    content: [{ type: 'text', text: 'Authentication failed after 3 attempts.' }],
    isError: true,
  };
}

Cap retry attempts. Unlimited retry loops frustrate users and can be abused. Three attempts is a reasonable default for credential entry; for simple choices, one is usually enough.

Elicitation vs prompt parameters

MCP has two other mechanisms for gathering user input: prompt parameters (defined in a PromptMessage via the Prompts API) and tool arguments. It's worth clarifying when each applies:

MechanismTimingWho provides the valueBest for
Tool argumentsBefore the tool callLLM (derived from conversation)Parameters the LLM can determine from context
Prompt parametersWhen a prompt is invokedHost application (often the user)Structured inputs to a named prompt template
ElicitationDuring a running tool callUser (via host UI)Information the LLM cannot determine; arises from tool execution state

A common mistake is using elicitation to collect information that should be a tool argument — for example, always eliciting a "confirm?" prompt even when the LLM already knows the user wants to proceed. The LLM can pass confirmed: true as a tool argument when the user's intent is clear. Reserve elicitation for cases where the tool handler genuinely cannot proceed without user intervention that cannot be anticipated at call time.

Frequently asked questions

What happens if I call requestElicitation and the client doesn't support it?

The SDK will throw an error or return a rejection, depending on the implementation. The safe pattern is to always check extra.clientCapabilities?.elicitation before calling requestElicitation. If the capability is absent, fall back: either accept that the tool cannot complete (return an informative error), or redesign the tool so the information can come from arguments instead.

Can I elicit multiple times in a single tool call?

Yes — elicitation is just a request-response round trip. A single tool handler can call requestElicitation multiple times sequentially. However, multiple elicitation prompts create friction: users get hit with several popups in a row. If you need several pieces of information, combine them into a single flat schema with multiple fields rather than using separate sequential elicitation calls.

Does elicitation work over stdio transport?

Yes. Elicitation messages are part of the MCP JSON-RPC protocol, which runs over both SSE and stdio transports. The transport has no bearing on whether elicitation is supported — that's determined by the client application's capabilities. Claude Desktop 1.6+ supports elicitation over both stdio and SSE. Check whether your target client application supports it before designing your tool around it.

Can I pass elicitation results back to the LLM in the tool output?

Yes, and it's often useful. If the user selected a project ID via elicitation, include the project name (not just the ID) in the tool result so the LLM can reference it in its reply to the user. The tool result is the LLM's only view of what happened during execution — everything worth reporting to the user must be in the returned content.

How do I monitor tools that use elicitation?

Elicitation adds variable latency — tool execution time now includes human response time, which can range from seconds to minutes. Uptime probes for elicitation-using tools should use a high timeout threshold (30–120 seconds) and should call the tool with pre-filled arguments that bypass the elicitation path. AliveMCP lets you configure per-monitor timeout thresholds so slow-but-correct elicitation tools don't trigger false-positive downtime alerts.

Further reading

Know when your MCP server is down — before users do

AliveMCP probes your server's MCP endpoint every minute, detects protocol errors and transport failures, and pages you before users notice.

Start monitoring free