Skip to main content
Elicitation enables your MCP server tools to request additional information from users during execution. This creates interactive workflows where tools can dynamically gather structured data or direct users to external URLs for sensitive operations.

Overview

When a tool needs user input, it can use ctx.elicit() to send a request to the client. The client presents this request to the user and returns their response. This enables dynamic, interactive tool behavior that adapts based on user input. Two Modes Available:
  • Form Mode: Collect structured data with JSON schema validation (for non-sensitive information)
  • URL Mode: Direct users to external URLs (MUST be used for sensitive data like credentials, OAuth)

Basic Usage

Use the elicit() method on the context object within tool callbacks. The API automatically detects the mode from your parameters:
import { createMCPServer, text } from 'mcp-use/server';
import { z } from 'zod';

const server = createMCPServer('my-server', {
  version: '1.0.0',
});

server.tool({
  name: 'collect-user-info',
  description: 'Collect user information',
}, async (params, ctx) => {
    // Simplified API: ctx.elicit(message, zodSchema)
    // Mode is automatically inferred from the Zod schema
    const result = await ctx.elicit(
      'Please provide your contact details',
      z.object({
        name: z.string().default('Anonymous'),
        email: z.string().email(),
      })
    );

    // result.data is automatically typed as { name: string, email: string }
    if (result.action === 'accept')
      return text(`Thank you, ${result.data.name}! We'll contact you at ${result.data.email}`);
    else if (result.action === 'decline')
      return text('No information provided');
    else
      return text('Operation cancelled');
});

Form Mode - Structured Data Collection

Form mode allows you to collect structured data from users with Zod schema validation. Use this for non-sensitive information only.

Simplified API

const result = await ctx.elicit(message, zodSchema);
// Mode is automatically inferred as "form" from the Zod schema parameter
// result.data is automatically typed based on the Zod schema

Zod Schema Support

Use Zod schemas to define the structure and validation rules for user input. Benefits:
  • Type Safety: Return type is automatically inferred from your schema
  • Server-Side Validation: Data is validated against the Zod schema before being returned (mcp-use feature)
  • Validation: Built-in Zod validators (.min(), .max(), .email(), etc.)
  • Descriptions: Use .describe() for field labels
  • Defaults: Use .default() for default values
  • Optional fields: Use .optional() for optional properties

Server-Side Validation

Automatic Validation with mcp-use: When using the mcp-use server libraryโ€™s simplified API with Zod schemas (ctx.elicit(message, zodSchema)), the server automatically validates the returned data against your schema. This is a convenience feature provided by mcp-use. Invalid data is rejected with clear error messages before reaching your tool logic.
The validation flow:
  1. Schema Conversion: Zod schema is converted to JSON Schema and sent to the client
  2. User Input: Client collects data from the user
  3. Client Validation: Client validates against JSON Schema (optional)
  4. Data Return: Client returns data to server
  5. Server Validation: Server validates data against the original Zod schema
  6. Type Safety: Validated data is returned with correct TypeScript types

What Gets Validated

The server validates:
  • Data Types: Strings, numbers, booleans must match schema
  • Ranges: Min/max values for numbers
  • String Constraints: MinLength, maxLength, patterns
  • Formats: Email, URL, date formats
  • Required Fields: Missing required fields are caught
  • Enums: Values must be from allowed options
  • Custom Validators: Any Zod refinements and transforms

Validation Error Handling

When validation fails, the error is caught and can be handled:
server.tool({
  name: 'create-account',
  description: 'Create account with validation'
}, async (params, ctx) => {
    try {
      const result = await ctx.elicit(
        'Create your account',
        z.object({
          username: z.string().min(3).max(20),
          email: z.string().email(),
          age: z.number().min(18).max(120),
        })
      );

      if (result.action === 'accept') {
        // Data is guaranteed to be valid here
        return text(`Account created for ${result.data.username}`);
      }

      return text('Account creation cancelled');
    } catch (error) {
      // Validation error or other failure
      return error(`Validation failed: ${error.message}`);
    }
});

Validation Examples

Valid Data:
// User provides: { username: "john_doe", email: "john@example.com", age: 25 }
// โœ… Passes validation - all fields valid
Invalid Age:
// User provides: { username: "john_doe", email: "john@example.com", age: 200 }
// โŒ Fails: "Too big: expected number to be <=120"
Invalid Email:
// User provides: { username: "john_doe", email: "not-an-email", age: 25 }
// โŒ Fails: "Invalid email address"
Missing Required Field:
// User provides: { username: "john_doe", age: 25 }
// โŒ Fails: "Invalid input: expected string, received undefined" (email missing)
Wrong Type:
// User provides: { username: "john_doe", email: "john@example.com", age: "twenty-five" }
// โŒ Fails: "Invalid input: expected number, received string"

Default Values

Fields with .default() are automatically filled if missing:
const result = await ctx.elicit(
  'Enter preferences',
  z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
  })
);

// If user provides: {}
// result.data will be: { theme: 'light', notifications: true }

Example: Simple Contact Form

import { z } from 'zod';
import { text } from 'mcp-use/server';

server.tool({
  name: 'register-user',
  description: 'Register a new user',
}, async (params, ctx) => {
    // Simplified API with Zod schema
    const result = await ctx.elicit(
      'Please complete your registration',
      z.object({
        name: z.string().min(2).describe('Your full name'),
        email: z.string().email().describe('Your email address'),
        age: z.number().min(18).max(120).describe('Your age'),
        newsletter: z.boolean().default(false).describe('Subscribe to newsletter'),
      })
    );

    // result.data is typed as:
    // { name: string, email: string, age: number, newsletter: boolean }
    if (result.action === 'accept') {
      const user = result.data;
      // TypeScript knows the exact shape of user data
      return text(`Welcome, ${user.name}! Registration successful.`);
    }

    return text('Registration cancelled');
  });

Example: With Enums and Arrays

import { z } from 'zod';
import { text, error } from 'mcp-use/server';

server.tool({
  name: 'create-task',
  description: 'Create a task with user input',
}, async (params, ctx) => {
    const result = await ctx.elicit(
      'Enter task details',
      z.object({
        title: z.string().min(1).describe('Task title'),
        priority: z
          .enum(['low', 'medium', 'high', 'urgent'])
          .default('medium')
          .describe('Priority level'),
        tags: z
          .array(z.enum(['bug', 'feature', 'docs', 'refactor']))
          .optional()
          .describe('Task tags'),
      })
    );

    // result.data is typed as:
    // { title: string, priority: 'low' | 'medium' | 'high' | 'urgent', tags?: ('bug' | 'feature' | 'docs' | 'refactor')[] }
    if (result.action === 'accept') {
      const task = result.data;
      return text(`Created ${task.priority} priority task: ${task.title}`);
    }

    return text('Task creation cancelled');
  });

URL Mode - External Navigation

URL mode directs users to external URLs for sensitive operations. This mode MUST be used for:
  • Authentication credentials
  • API keys and tokens
  • OAuth authorization flows
  • Payment information
  • Any sensitive personal data
Security Requirement: Never use form mode for sensitive data. URL mode ensures credentials and sensitive information do not pass through the MCP client, maintaining proper security boundaries per the MCP specification.

Simplified API

const result = await ctx.elicit(message, urlString);
// Mode is automatically inferred as "url" from the URL string parameter
// elicitationId is automatically generated

Example: OAuth Authorization

server.tool({
  name: 'connect-github',
  description: 'Authorize GitHub access',
}, async (params, ctx) => {
    // Generate OAuth URL (in a real app, this would come from your OAuth provider)
    const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&state=${generateState()}`;

    // Simplified API: ctx.elicit(message, url)
    // Mode and elicitationId are handled automatically
    const result = await ctx.elicit(
      'Please authorize GitHub access to continue',
      authUrl
    );

    if (result.action === 'accept') {
      // User completed authorization
      // In a real implementation, you would:
      // 1. Receive the callback with authorization code
      // 2. Exchange code for access token
      // 3. Store tokens securely on the server
      return text('โœ… GitHub authorization successful! You can now use GitHub tools.');
    } else if (result.action === 'decline') {
      return error('โŒ GitHub authorization declined. Some features will be unavailable.');
    } else {
      return error('Authorization cancelled');
    }
  });

Example: Service Connection

import { text } from 'mcp-use/server';
server.tool({
  name: 'setup-api-credentials',
  description: 'Set up API credentials for a service',
}, async ({ serviceName }, ctx) => {
    // Generate a secure credential collection URL
    const credentialUrl = `https://your-server.com/setup/${serviceName}?session=${generateSecureToken()}`;

    // Simplified URL mode API
    const result = await ctx.elicit(
      `Configure your ${serviceName} API credentials securely`,
      credentialUrl
    );

    if (result.action === 'accept') {
      return text(`โœ… ${serviceName} credentials configured successfully`);
    }

    return text(`${serviceName} setup not completed`);
  });

Response Handling

The elicit() method returns an ElicitResult object:
interface ElicitResult {
  action: 'accept' | 'decline' | 'cancel';
  data?: any;  // Only present for 'accept' action in form mode
}

Response Actions

ActionDescriptionWhen to Use
acceptUser provided valid input or completed the actionCheck result.data for form mode input
declineUser explicitly declined the requestUser chose not to provide information
cancelUser cancelled without making a choiceUser dismissed or closed the request

Handling All Response Types

Always handle all three response actions appropriately:
server.tool({
  name: 'example-tool',
  description: 'Example showing response handling',
}, async (params, ctx) => {
    const result = await ctx.elicit('Do you want to proceed?', z.object({ confirm: z.boolean().default(false) }));

    // Handle all three cases
    switch (result.action) {
      case 'accept':
        // User provided data
        if (result.data.confirm) {
          return text('โœ… Proceeding with action');
        }
        return text('โŒ Action not confirmed');

      case 'decline':
        // User explicitly declined
        return text('๐Ÿšซ User declined to provide confirmation');

      case 'cancel':
        // User cancelled
        return text('โš ๏ธ Operation cancelled by user');

      default:
        // Should never happen, but good practice
        return error('โ“ Unknown response');
    }
  });

Timeout Configuration

By default, elicit() waits indefinitely for user response (no timeout). You can optionally specify a timeout:
import { z } from 'zod';

server.tool({
  name: 'time-sensitive-input',
  description: 'Collect input with timeout',
}, async (params, ctx) => {
  try {
    // Wait indefinitely (default)
    const result1 = await ctx.elicit(
      'Please provide information',
      z.object({ data: z.string() })
    );

    // With 60-second timeout
    const result2 = await ctx.elicit(
      'Quick! Provide information',
      z.object({ data: z.string() }),
      { timeout: 60000 } // 1 minute timeout
    );

    // URL mode with timeout
    const result3 = await ctx.elicit(
      'Authorize (2 minute limit)',
      'https://example.com/oauth',
      { timeout: 120000 } // 2 minute timeout
    );

    if (result2.action === 'accept') {
      return text(`Received: ${result2.data.data}`);
    }

    return text('No response');
  } catch (error) {
    // Timeout or validation error
    return error(`Request failed: ${error.message}`);
  }
});
Default Behavior: Like ctx.sample(), elicitation has no timeout by default and waits indefinitely for user response. This prevents premature failures for users who may take time to complete forms or authorizations.

Error Handling

Wrap elicitation calls in try-catch blocks to handle errors gracefully:
server.tool({
  name: 'safe-elicitation',
  description: 'Tool with error handling',
}, async (params, ctx) => {
    try {
      const result = await ctx.elicit('Please provide information', z.object({ data: z.string() }));

      if (result.action === 'accept') {
        return text(`Received: ${result.data.data}`);
      }

      return text('No data provided');
    } catch (error: unknown) {
      // Client doesn't support elicitation or request failed
      return error(`Error: ${error instanceof Error ? error.message : String(error)}. Please ensure your client supports elicitation.`);
    }
  });

Security Best Practices

Critical Security Rules:
  1. Never collect sensitive data via form mode - Always use URL mode for passwords, API keys, tokens, or personal information
  2. Validate on the server - Donโ€™t trust client-side validation alone
  3. Use HTTPS for URL mode - All URL mode elicitations should use secure HTTPS URLs
  4. Bind to user identity - Associate elicitation requests with authenticated user sessions
  5. Verify completion - For URL mode, verify the action was completed through a secure callback

DO NOT Do This โŒ

// WRONG: Collecting sensitive data via form mode
server.tool({
  name: 'bad-auth-example',
}, async (params, ctx) => {
    // โŒ NEVER DO THIS!
    const result = await ctx.elicit('Enter your credentials', z.object({ password: z.string(), apiKey: z.string() }));
    // This violates MCP security requirements
  });

DO This Instead โœ…

// CORRECT: Using URL mode for sensitive data
server.tool({
  name: 'good-auth-example',
}, async (params, ctx) => {
    // โœ… Correct approach
    const result = await ctx.elicit('Please authenticate securely', 'https://your-secure-server.com/auth');

    if (result.action === 'accept') {
      // Verify completion server-side
      // Tokens are stored on your server, never pass through client
    }
  });

Complete Example Server

Hereโ€™s a complete example showing both form and URL mode with the simplified API:
import { createMCPServer } from 'mcp-use/server';
import { z } from 'zod';
import { text } from 'mcp-use/server';

const server = createMCPServer('elicitation-demo', {
  version: '1.0.0',
  description: 'Server demonstrating elicitation patterns',
});

// Form mode: Collect preferences with Zod schema
server.tool({
  name: 'set-preferences',
  description: 'Configure user preferences',
}, async (params, ctx) => {
    // Simplified API: ctx.elicit(message, zodSchema)
    const result = await ctx.elicit(
      'Configure your preferences',
      z.object({
        theme: z.enum(['light', 'dark', 'auto']).default('auto').describe('UI theme'),
        notifications: z.boolean().default(true).describe('Enable notifications'),
        language: z.enum(['en', 'es', 'fr', 'de']).default('en').describe('Language'),
      })
    );

    // result.data is typed: { theme: 'light' | 'dark' | 'auto', notifications: boolean, language: 'en' | 'es' | 'fr' | 'de' }
    if (result.action === 'accept') {
      const prefs = result.data;
      return text(`Preferences saved: ${prefs.theme} theme, notifications ${prefs.notifications ? 'enabled' : 'disabled'}`);
    }

    return text('Preferences not updated');
  });

// URL mode: OAuth authorization
server.tool({
  name: 'authorize-service',
  description: 'Authorize external service',
}, async ({ service }, ctx) => {
    const authUrl = generateOAuthUrl(service);

    // Simplified API: ctx.elicit(message, url)
    const result = await ctx.elicit(`Authorize ${service} to access your data`, authUrl);

    if (result.action === 'accept') {
      return text(`โœ… ${service} authorized successfully`);
    }

    return text(`Authorization for ${service} not completed`);
  });

await server.listen(3000);

Working Example

Check out the complete working example with form mode, URL mode, and conformance tests: ๐Ÿ“ examples/server/elicitation/ This example includes:
  • Form mode with JSON schema validation
  • URL mode for OAuth-like flows
  • Conformance test tool
  • Working test client
To run the example:
cd libraries/typescript/packages/mcp-use/examples/server/elicitation
pnpm install
pnpm dev
Server runs on http://localhost:3002 with MCP Inspector available.

Next Steps