MCP Framework
Tools

Elicitation

Request user input during tool execution with form-based and URL-based elicitation

Elicitation

Elicitation allows your tools to request input from the user mid-execution. Instead of requiring all information upfront, a tool can ask the user for additional details as needed — like a form popup or a redirect to an external URL.

MCP Spec Feature

Elicitation is defined in the MCP specification (2025-06-18 for form mode, 2025-11-25 for URL mode). The client must declare elicitation support in its capabilities — not all clients support it.

Two Modes

ModePurposeData visible to client?Use for
FormStructured input collectionYesNames, emails, preferences, confirmations
URLOut-of-band sensitive interactionNoPasswords, API keys, OAuth flows, payments

Quick Start

Form Mode

Call this.elicit() inside your tool's execute() method to collect structured user input:

import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";

const schema = z.object({
  task: z.string().describe("The task to perform"),
});

class SetupTool extends MCPTool {
  name = "setup";
  description = "Set up a new project with user preferences";
  schema = schema;

  async execute(input: MCPInput<this>) {
    // Ask the user for their details
    const result = await this.elicit("Please provide your project details", {
      name: { type: "string", description: "Project name", minLength: 1 },
      language: {
        type: "string",
        description: "Programming language",
        enum: ["typescript", "python", "go", "rust"],
      },
      enableTests: {
        type: "boolean",
        description: "Enable test scaffolding?",
        default: true,
      },
    });

    // Handle the three possible responses
    if (result.action === "accept") {
      return {
        project: result.content?.name,
        language: result.content?.language,
        tests: result.content?.enableTests,
      };
    }

    if (result.action === "decline") {
      return "User declined to provide project details";
    }

    // action === "cancel" — user dismissed the dialog
    return "Setup cancelled";
  }
}

export default SetupTool;

URL Mode

Call this.elicitUrl() to redirect the user to a URL for sensitive interactions:

import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";

const schema = z.object({
  provider: z.string().describe("OAuth provider name"),
});

class ConnectServiceTool extends MCPTool {
  name = "connect_service";
  description = "Connect to a third-party service via OAuth";
  schema = schema;

  async execute(input: MCPInput<this>) {
    const elicitationId = `oauth-${input.provider}-${Date.now()}`;

    const result = await this.elicitUrl(
      `Please authorize access to your ${input.provider} account`,
      `https://${input.provider}.example.com/oauth/authorize?state=${elicitationId}`,
      elicitationId
    );

    if (result.action === "accept") {
      // User agreed to open the URL — authorization flow started
      return { status: "authorization_started", provider: input.provider };
    }

    return { status: "authorization_declined", provider: input.provider };
  }
}

export default ConnectServiceTool;

Response Handling

Every elicitation returns an ElicitResult with one of three actions:

ActionMeaningcontent field
"accept"User submitted the form / agreed to open URLForm mode: contains submitted data. URL mode: omitted
"decline"User explicitly refusedOmitted
"cancel"User dismissed without choosing (closed dialog, pressed Escape)Omitted

Always handle all three actions. A common pattern:

const result = await this.elicit("Enter details", { /* schema */ });

switch (result.action) {
  case "accept":
    // Use result.content
    return processData(result.content);
  case "decline":
    return "User declined. Proceeding without additional info.";
  case "cancel":
    return "Operation cancelled.";
}

Field Types

Form mode supports flat objects with primitive fields only. No nested objects or complex structures.

await this.elicit("Enter your info", {
  // Basic string
  name: { type: "string", description: "Your full name" },

  // With constraints
  username: {
    type: "string",
    description: "Username",
    minLength: 3,
    maxLength: 20,
  },

  // With format validation
  email: { type: "string", description: "Email", format: "email" },
  website: { type: "string", description: "Website", format: "uri" },
  birthday: { type: "string", description: "Birthday", format: "date" },

  // With default
  role: { type: "string", description: "Role", default: "viewer" },

  // Optional field
  bio: { type: "string", description: "Bio", optional: true },
});

Supported formats: email, uri, date, date-time

await this.elicit("Configure limits", {
  // Basic number
  count: { type: "number", description: "Number of items" },

  // Integer only
  quantity: { type: "integer", description: "Quantity (whole numbers)" },

  // With range
  rating: {
    type: "number",
    description: "Rating from 1 to 5",
    minimum: 1,
    maximum: 5,
  },

  // With default
  timeout: {
    type: "number",
    description: "Timeout in seconds",
    default: 30,
  },

  // Optional
  priority: {
    type: "integer",
    description: "Priority level",
    optional: true,
  },
});
await this.elicit("Preferences", {
  enableNotifications: {
    type: "boolean",
    description: "Enable email notifications?",
    default: true,
  },
  acceptTerms: {
    type: "boolean",
    description: "Accept terms and conditions?",
  },
});

Single-select with plain values:

await this.elicit("Choose", {
  color: {
    type: "string",
    description: "Pick a color",
    enum: ["red", "green", "blue"],
  },
});

Single-select with display titles:

await this.elicit("Choose", {
  priority: {
    type: "string",
    description: "Priority level",
    oneOf: [
      { const: "p0", title: "Critical" },
      { const: "p1", title: "High" },
      { const: "p2", title: "Medium" },
      { const: "p3", title: "Low" },
    ],
    default: "p2",
  },
});

Multi-select with plain values:

await this.elicit("Select tags", {
  tags: {
    type: "array",
    description: "Choose up to 3 tags",
    minItems: 1,
    maxItems: 3,
    items: { type: "string", enum: ["bug", "feature", "docs", "test"] },
  },
});

Multi-select with display titles:

await this.elicit("Select features", {
  features: {
    type: "array",
    description: "Features to enable",
    items: {
      anyOf: [
        { const: "auth", title: "Authentication" },
        { const: "logs", title: "Logging" },
        { const: "metrics", title: "Metrics" },
      ],
    },
  },
});

Required vs Optional Fields

By default, all fields are required. Add optional: true to make a field optional:

await this.elicit("Contact info", {
  name: { type: "string", description: "Full name" },           // required
  email: { type: "string", description: "Email", format: "email" }, // required
  phone: { type: "string", description: "Phone", optional: true },  // optional
});

The optional flag is a framework convenience — it maps to JSON Schema's required array. It is not sent to the client.

Multi-Step Elicitation

You can call elicit() and elicitUrl() multiple times within a single execute() call:

async execute(input: MCPInput<this>) {
  // Step 1: Collect basic info via form
  const basicInfo = await this.elicit("Enter basic info", {
    name: { type: "string", description: "Project name" },
    type: {
      type: "string",
      description: "Project type",
      enum: ["web", "api", "cli"],
    },
  });

  if (basicInfo.action !== "accept") {
    return "Setup cancelled at step 1";
  }

  // Step 2: Connect external service via URL
  const auth = await this.elicitUrl(
    "Connect your GitHub account to import settings",
    "https://github.example.com/oauth/authorize",
    `github-auth-${Date.now()}`
  );

  if (auth.action !== "accept") {
    return { name: basicInfo.content?.name, github: false };
  }

  // Step 3: Final confirmation
  const confirm = await this.elicit("Confirm setup", {
    proceed: {
      type: "boolean",
      description: `Create project "${basicInfo.content?.name}"?`,
      default: true,
    },
  });

  if (confirm.action === "accept" && confirm.content?.proceed) {
    return {
      created: true,
      name: basicInfo.content?.name,
      type: basicInfo.content?.type,
      github: true,
    };
  }

  return "Setup cancelled at confirmation step";
}

Error Handling

If the client does not support elicitation, elicit() and elicitUrl() will throw an error. Handle this gracefully:

async execute(input: MCPInput<this>) {
  try {
    const result = await this.elicit("Enter your name", {
      name: { type: "string", description: "Name" },
    });
    return `Hello, ${result.content?.name}!`;
  } catch (error) {
    // Client doesn't support elicitation — fall back
    return `Hello! (Tip: use a client that supports elicitation for a better experience)`;
  }
}

No Server = Error

elicit() and elicitUrl() can only be called from within a tool's execute() method. Calling them outside of tool execution (e.g., at construction time) will throw an error.

Security

Sensitive Data

Never use form mode (elicit()) for passwords, API keys, secrets, or any sensitive data. Form data passes through the MCP client and may be visible to the LLM. Use URL mode (elicitUrl()) instead — it redirects the user to your own secure page where data is entered directly.

  • Form mode: Data is visible to the client/LLM. Use for non-sensitive input only.
  • URL mode: Data stays between the user and your server. The client only knows the URL was opened.
  • Always use HTTPS URLs in production.
  • Validate user identity server-side when using URL mode for authentication flows.

API Reference

elicit(message, schema, options?)

Request structured input from the user.

ParameterTypeDescription
messagestringHuman-readable message explaining why input is needed
schemaRecord<string, ElicitationFieldSchema>Field definitions
optionsRequestOptionsOptional timeout, abort signal, etc.

Returns: Promise<ElicitResult> with action and optional content.

elicitUrl(message, url, elicitationId, options?)

Request the user to visit a URL for out-of-band interaction.

ParameterTypeDescription
messagestringHuman-readable message explaining why the URL visit is needed
urlstringThe URL the user should navigate to
elicitationIdstringUnique identifier for this elicitation
optionsRequestOptionsOptional timeout, abort signal, etc.

Returns: Promise<ElicitResult> with action only (no content for URL mode).

Next Steps