---
title: "Elicitation"
description: "Request user input during tool execution"
icon: "check"
---

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

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

```typescript
import { MCPServer, text } from 'mcp-use/server';
import { z } from 'zod';

const server = new MCPServer({
  name: '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

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

```typescript
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
```

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

<Note>
**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.
</Note>

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:

```typescript
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**:
```typescript
// User provides: { username: "john_doe", email: "john@example.com", age: 25 }
// ✅ Passes validation - all fields valid
```

**Invalid Age**:
```typescript
// User provides: { username: "john_doe", email: "john@example.com", age: 200 }
// ❌ Fails: "Too big: expected number to be <=120"
```

**Invalid Email**:
```typescript
// User provides: { username: "john_doe", email: "not-an-email", age: 25 }
// ❌ Fails: "Invalid email address"
```

**Missing Required Field**:
```typescript
// User provides: { username: "john_doe", age: 25 }
// ❌ Fails: "Invalid input: expected string, received undefined" (email missing)
```

**Wrong Type**:
```typescript
// 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:

```typescript
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 }
```

<Note>
Default values (SEP-1034) and enum schema improvements (SEP-1330) are complementary. Use `.default()` for straightforward Zod-based forms, and use the verbose JSON Schema API when you need titled enum variants or multi-select enum arrays.
</Note>

#### SEP-1330 Enum Schema Patterns

When you need advanced enum shapes like titled enums (`oneOf`/`anyOf`) or legacy `enumNames`, use the verbose API:

```typescript
import {
  enumSchema,
  legacyEnum,
  titledEnum,
  titledMultiEnum,
  untitledEnum,
  untitledMultiEnum,
} from 'mcp-use/server';

const result = await ctx.elicit({
  message: 'Choose your options',
  requestedSchema: enumSchema({
    untitledSingle: untitledEnum(['option1', 'option2', 'option3']),
    titledSingle: titledEnum([
      { value: 'value1', title: 'First Option' },
      { value: 'value2', title: 'Second Option' },
      { value: 'value3', title: 'Third Option' },
    ]),
    legacyEnum: legacyEnum([
      { value: 'opt1', name: 'Option One' },
      { value: 'opt2', name: 'Option Two' },
      { value: 'opt3', name: 'Option Three' },
    ]),
    untitledMulti: untitledMultiEnum(['option1', 'option2', 'option3']),
    titledMulti: titledMultiEnum([
      { value: 'value1', title: 'First Choice' },
      { value: 'value2', title: 'Second Choice' },
      { value: 'value3', title: 'Third Choice' },
    ]),
  }),
});

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

Supported SEP-1330 variants:

- Untitled single-select: `type: "string" + enum`
- Titled single-select: `type: "string" + oneOf[{ const, title }]`
- Legacy titled enum: `type: "string" + enum + enumNames`
- Untitled multi-select: `type: "array" + items.enum`
- Titled multi-select: `type: "array" + items.anyOf[{ const, title }]`

#### Example: Simple Contact Form

```typescript
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');
  });
```

## URL Mode

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

<Warning>
**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.
</Warning>

```typescript
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

```typescript
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');
    }
  });
```


## Response Handling

The `elicit()` method returns an `ElicitResult` object:

```typescript
interface ElicitResult {
  action: 'accept' | 'decline' | 'cancel';
  data?: any;  // Only present for 'accept' action in form mode
}
```

### Response Actions

| Action | Description | When to Use |
|--------|-------------|-------------|
| **accept** | User provided valid input or completed the action | Check `result.data` for form mode input |
| **decline** | User explicitly declined the request | User chose not to provide information |
| **cancel** | User cancelled without making a choice | User dismissed or closed the request |

Always handle all three response actions appropriately:

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

```typescript
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}`);
  }
});
```

<Note>
**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.
</Note>

## Error Handling

Wrap elicitation calls in try-catch blocks to handle errors gracefully:

```typescript
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.`);
    }
  });
```

## Working Example

Check out the complete working example with form mode, URL mode, and conformance tests:

📁 [examples/server/elicitation/](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/elicitation)

This example includes:
- Form mode with JSON schema validation
- URL mode for OAuth-like flows
- Conformance test tool
- SEP-1330 conformance pattern via `test_elicitation_sep1330_enums` in the conformance feature server:
  [examples/server/features/conformance/](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/features/conformance)
- Working test client

To run the example:

```bash
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

- Learn how to [handle elicitation on the client side](/typescript/client/elicitation)
- Explore [sampling](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/sampling) for LLM completions
- Check out [notifications](/typescript/server/notifications) for server-to-client messages
- Review [tools documentation](/typescript/server/tools) for more tool patterns

