MCP Elicitation: Asking the User for Input from Inside a Server

Some tools cannot complete without asking the user a question. “Which account should I debit?” “What is the date range for this report?” “Are you sure you want to delete all 200 records?” These are not questions an LLM should guess at. They require explicit, structured input from the human in the loop. Elicitation is MCP’s mechanism for this – it lets a server, while handling a request, ask the user for information through the client’s UI, wait for the answer, and then continue. It is a synchronous human-in-the-loop pattern baked into the protocol.

MCP elicitation flow showing server pausing tool execution to request user input through client UI dark diagram
Elicitation: the server pauses tool execution, asks the user a structured question, and resumes with the answer.

How Elicitation Works

When a server needs user input, it sends an elicitation/create request to the client. The request includes a message (explaining what information is needed) and a JSON schema describing the expected response format. The client presents this to the user in whatever UI is appropriate – a dialog box, a prompt, a form – and returns the user’s structured response. The server receives the answer and continues processing.

Elicitation is a client capability – the server can only use it if the client declared elicitation support during initialisation. If the client does not support elicitation, the server must handle the lack of user input gracefully (skip the step, use defaults, or return an error with a clear explanation).

// Client: declare elicitation support
const client = new Client(
  { name: 'my-host', version: '1.0.0' },
  {
    capabilities: {
      elicitation: {},  // Required for server-side elicitation
    },
  }
);

// Client: implement the elicitation handler
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';

client.setRequestHandler(ElicitRequestSchema, async (request) => {
  const { message, requestedSchema } = request.params;

  // Show UI to user - implementation is host-specific
  const userResponse = await showElicitationDialog(message, requestedSchema);

  if (userResponse === null) {
    // User dismissed/cancelled
    return { action: 'cancel' };
  }

  return {
    action: 'accept',
    content: userResponse,  // Must match requestedSchema
  };
});
MCP elicitation schema showing JSON schema for user response with string number boolean enum field types
Elicitation schemas: flat JSON schemas that define the exact structure of the user’s expected answer.

Server-Side Elicitation Usage

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

const server = new McpServer({ name: 'payment-server', version: '1.0.0' });

server.tool(
  'process_payment',
  'Processes a payment transaction with user confirmation',
  {
    amount: z.number().positive().describe('Amount in USD'),
    recipient: z.string().describe('Recipient name or email'),
  },
  async ({ amount, recipient }, { server: serverInstance }) => {
    // Elicit confirmation before processing
    const confirmation = await serverInstance.elicitInput({
      message: `You are about to send $${amount.toFixed(2)} to ${recipient}. Please confirm the payment account:`,
      requestedSchema: {
        type: 'object',
        properties: {
          account_id: {
            type: 'string',
            description: 'Your account ID to debit (format: ACC-XXXXXXXX)',
          },
          confirmed: {
            type: 'boolean',
            description: 'Confirm you want to proceed with this payment',
          },
        },
        required: ['account_id', 'confirmed'],
      },
    });

    if (confirmation.action === 'cancel') {
      return { content: [{ type: 'text', text: 'Payment cancelled by user.' }] };
    }

    if (!confirmation.content.confirmed) {
      return { content: [{ type: 'text', text: 'Payment declined - user did not confirm.' }] };
    }

    const result = await processPayment({
      amount,
      recipient,
      accountId: confirmation.content.account_id,
    });

    return { content: [{ type: 'text', text: `Payment ${result.transactionId} processed successfully.` }] };
  }
);

“Elicitation allows servers to request additional information from users during tool execution. This enables interactive workflows where user input is needed to complete tasks, while maintaining a clear separation between the AI model and the human oversight layer.” – MCP Specification, Elicitation

Elicitation Schema Constraints

The schema for an elicitation request is deliberately restricted compared to full JSON Schema. This is intentional – the schema must be renderable by any client UI, which means it cannot be arbitrarily complex. The spec defines a “flat” schema: a single object with primitive properties (string, number, boolean, or enum). No nested objects, no arrays, no $ref references.

// VALID elicitation schema - flat, primitive properties only
{
  type: 'object',
  properties: {
    name: { type: 'string', description: 'Your full name' },
    age: { type: 'number', description: 'Your age in years' },
    agree_to_terms: { type: 'boolean', description: 'Do you agree to the terms?' },
    plan: { type: 'string', enum: ['basic', 'pro', 'enterprise'], description: 'Choose a plan' },
  },
  required: ['name', 'agree_to_terms'],
}

// INVALID: Nested objects not allowed in elicitation schemas
{
  type: 'object',
  properties: {
    address: {
      type: 'object',  // This will fail - nested objects not permitted
      properties: { street: { type: 'string' } },
    },
  },
}

Failure Modes with Elicitation

Case 1: Not Handling cancel Action

Users can always cancel an elicitation. If your tool handler does not check for the cancel action, it will try to proceed with undefined data and crash or produce garbage output.

// BAD: No cancel check
const response = await serverInstance.elicitInput({ ... });
await processData(response.content.value); // Will throw if action was 'cancel'

// GOOD: Always check the action
const response = await serverInstance.elicitInput({ ... });
if (response.action !== 'accept') {
  return { content: [{ type: 'text', text: 'Action cancelled.' }] };
}
await processData(response.content.value);

Case 2: Using Elicitation When a Tool Parameter Would Suffice

Elicitation is for input the server cannot know at design time – confirmation of a specific action, a password, runtime context. If the information can be a tool argument, make it a tool argument. Elicitation adds a round-trip to the user and breaks the automated flow.

// WRONG: Using elicitation for something that should be an arg
server.tool('delete_record', '...', { id: z.string() }, async ({ id }, ctx) => {
  const confirm = await ctx.server.elicitInput({ message: 'Confirm deletion?', ... });
  // Should this really need an interactive prompt? Or is --confirm a better pattern?
});

// BETTER: Use tool annotations + let the host handle confirmation
server.tool(
  'delete_record',
  'Permanently deletes a record',
  { id: z.string(), confirm: z.boolean().describe('Set true to confirm permanent deletion') },
  { annotations: { destructive: true } },
  async ({ id, confirm }) => {
    if (!confirm) return { isError: true, content: [{ type: 'text', text: 'Set confirm=true to proceed.' }] };
    await db.delete(id);
    return { content: [{ type: 'text', text: `Deleted ${id}` }] };
  }
);

What to Check Right Now

  • Map your interactive flows – identify any workflow in your application that requires user input mid-execution. These are elicitation candidates.
  • Keep schemas flat – validate your elicitation schemas against the spec constraints: flat object, primitive values only, no nested objects or arrays.
  • Always handle cancel and decline – every elicitation can result in cancel (user dismissed) or decline (user responded negatively). Handle all three outcomes.
  • Check client support first – before calling elicitInput, check client.getClientCapabilities()?.elicitation. If the client does not support it, fall back to tool-argument-based confirmation.

nJoy 😉

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.