Elicitation allows MCP tools to request additional information from users during their execution. Servers can collect structured data (form mode) or direct users to external URLs (URL mode) for sensitive operations.
Overview
When a server tool needs user input, it sends an elicitation request to the client. The client presents this request to the user and returns their response. This enables interactive workflows where tools can dynamically gather information as needed.
Elicitation supports two modes:
- 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 flows)
Server API: Servers use a simplified API like ctx.elicit(message, zodSchema) or ctx.elicit(message, url). The mode is automatically detected and the request is sent to the client with the appropriate JSON schema or URL.Server-Side Validation with mcp-use: When using the mcp-use server library with Zod schemas (simplified API), returned data is automatically validated server-side before reaching your tool logic. This is a convenience feature provided by mcp-use, ensuring type safety and data integrity.
Configuration
With MCPClient
Provide an elicitationCallback function when initializing the MCPClient:
import { MCPClient } from 'mcp-use';
import type {
ElicitRequestFormParams,
ElicitRequestURLParams,
ElicitResult,
} from '@modelcontextprotocol/sdk/types.js';
async function elicitationCallback(
params: ElicitRequestFormParams | ElicitRequestURLParams
): Promise<ElicitResult> {
if (params.mode === 'url') {
// URL mode: Direct user to external URL
console.log(`Please visit: ${params.url}`);
console.log(`Reason: ${params.message}`);
// Show URL to user (open in browser, display in UI, etc.)
// In a real app, you might open a popup or new tab
// Wait for user confirmation
const userConsent = await promptUser('Did you complete the authorization?');
return {
action: userConsent ? 'accept' : 'decline',
};
} else {
// Form mode: Collect structured data
console.log(`Server requests: ${params.message}`);
const schema = params.requestedSchema;
const userData: Record<string, any> = {};
// Collect data based on schema properties
if (schema.type === 'object' && schema.properties) {
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
const value = await promptUser(
`Enter ${fieldSchema.title || fieldName}:`,
fieldSchema.default
);
userData[fieldName] = value;
}
}
return {
action: 'accept',
data: userData,
};
}
}
const client = new MCPClient(
config,
{ elicitationCallback }
);
With React Hook
Use the onElicitation prop in the useMcp hook:
import { useMcp } from 'mcp-use/react';
import type {
ElicitRequestFormParams,
ElicitRequestURLParams,
ElicitResult,
} from '@modelcontextprotocol/sdk/types.js';
function MyComponent() {
const { tools, callTool, state } = useMcp({
url: 'http://localhost:3000/mcp',
onElicitation: async (params) => {
if (params.mode === 'url') {
// URL mode: Open external URL
const confirmed = window.confirm(
`${params.message}\n\nOpen ${params.url}?`
);
if (confirmed) {
window.open(params.url, '_blank');
// Wait for user to complete external action
const completed = window.confirm('Did you complete the action?');
return {
action: completed ? 'accept' : 'decline',
};
}
return { action: 'decline' };
} else {
// Form mode: Show form to collect data
const formData = await showElicitationForm(
params.message,
params.requestedSchema
);
return {
action: formData ? 'accept' : 'cancel',
data: formData,
};
}
},
});
// ... rest of component
}
Form mode collects structured data from users with JSON schema validation.
Validation Flow: The client receives a JSON Schema to guide user input. When data is returned, the server validates it against the original Zod schema (if using simplified API) or JSON Schema (if using verbose API). This provides defense-in-depth validation.
Request Parameters
| Field | Type | Description |
|---|
| mode | "form" (optional) | Specifies form mode (can be omitted for backwards compatibility) |
| message | string | Human-readable prompt explaining what information is needed |
| requestedSchema | object | JSON Schema defining the expected response structure |
Client-Side vs Server-Side Validation
Client-Side (Optional):
- You can validate data before sending to improve UX
- Shows errors immediately without round-trip
- Not required - server validates anyway
Server-Side (Automatic):
- Server always validates returned data
- Ensures data integrity regardless of client behavior
- Protects against malicious or buggy clients
- Returns clear validation error messages
Client-Side Validation (Optional but Recommended)
While the server always validates data, implementing client-side validation improves UX by catching errors before the round-trip:
async function handleFormElicitation(
params: ElicitRequestFormParams
): Promise<ElicitResult> {
const schema = params.requestedSchema;
if (schema.type === 'object' && schema.properties) {
const formData: Record<string, any> = {};
// Build form based on schema
for (const [fieldName, fieldDef] of Object.entries(schema.properties)) {
const value = await showInputField({
name: fieldName,
title: fieldDef.title || fieldName,
description: fieldDef.description,
type: fieldDef.type, // 'string', 'number', 'boolean', etc.
default: fieldDef.default,
required: schema.required?.includes(fieldName),
// Optional: Add client-side validation
min: fieldDef.minimum || fieldDef.minLength,
max: fieldDef.maximum || fieldDef.maxLength,
pattern: fieldDef.pattern,
format: fieldDef.format,
});
formData[fieldName] = value;
}
// Optional: Validate before sending
// This improves UX but server will validate anyway
const validationErrors = validateAgainstSchema(formData, schema);
if (validationErrors.length > 0) {
// Show errors to user, let them fix
await showValidationErrors(validationErrors);
// Retry or cancel
}
return {
action: 'accept',
data: formData,
};
}
return { action: 'cancel' };
}
Best Practice: Implement client-side validation for better UX, but remember that the server always validates as the final authority. Never rely solely on client-side validation.
import { useState } from 'react';
function ElicitationForm({ params, onSubmit, onCancel }) {
const [formData, setFormData] = useState({});
const schema = params.requestedSchema;
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
action: 'accept',
data: formData,
});
};
return (
<div className="elicitation-modal">
<h3>{params.message}</h3>
<form onSubmit={handleSubmit}>
{Object.entries(schema.properties || {}).map(([name, field]) => (
<div key={name}>
<label>{field.title || name}</label>
{field.description && <p>{field.description}</p>}
<input
type={field.type === 'number' ? 'number' : 'text'}
defaultValue={field.default}
onChange={(e) => setFormData({
...formData,
[name]: field.type === 'number'
? parseFloat(e.target.value)
: e.target.value
})}
required={schema.required?.includes(name)}
/>
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={() => onCancel({ action: 'cancel' })}>
Cancel
</button>
</form>
</div>
);
}
URL Mode Elicitation
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.
Request Parameters
| Field | Type | Description |
|---|
| mode | "url" | Specifies URL mode (required) |
| message | string | Human-readable explanation of why the user needs to visit the URL |
| url | string | The URL to direct the user to |
| elicitationId | string | Unique identifier for tracking this elicitation (auto-generated by server) |
Example: OAuth Authorization
async function handleUrlElicitation(
params: ElicitRequestURLParams
): Promise<ElicitResult> {
// Display the URL to the user
const userMessage = `
${params.message}
Please visit this URL to continue:
${params.url}
`;
console.log(userMessage);
// In a browser environment, open in new tab
if (typeof window !== 'undefined') {
window.open(params.url, '_blank', 'noopener,noreferrer');
}
// Wait for user confirmation
const completed = await confirmWithUser(
'Have you completed the authorization?'
);
if (completed) {
return { action: 'accept' };
} else {
return { action: 'decline' };
}
}
Example: React OAuth Flow
function OAuthElicitation({ params, onComplete }) {
const [loading, setLoading] = useState(false);
const handleAuthorize = () => {
// Open OAuth URL in popup
const width = 600;
const height = 700;
const left = (window.innerWidth - width) / 2;
const top = (window.innerHeight - height) / 2;
const popup = window.open(
params.url,
'OAuth Authorization',
`width=${width},height=${height},left=${left},top=${top}`
);
setLoading(true);
// Poll for popup closure
const checkPopup = setInterval(() => {
if (popup?.closed) {
clearInterval(checkPopup);
setLoading(false);
// User completed or closed the popup
const success = window.confirm('Did you complete the authorization?');
onComplete({
action: success ? 'accept' : 'decline',
});
}
}, 500);
};
return (
<div className="oauth-modal">
<h3>{params.message}</h3>
<p>You will be redirected to complete the authorization.</p>
<button onClick={handleAuthorize} disabled={loading}>
{loading ? 'Waiting for authorization...' : 'Authorize'}
</button>
<button onClick={() => onComplete({ action: 'decline' })}>
Cancel
</button>
</div>
);
}
Response Structure
The elicitation callback must return an ElicitResult object:
interface ElicitResult {
action: 'accept' | 'decline' | 'cancel';
data?: any; // Only for 'accept' action in form mode
}
Response Actions
| Action | When to Use | Data Field |
|---|
| accept | User provided valid input | Required for form mode, optional for URL mode |
| decline | User explicitly declined to provide information | Omit |
| cancel | User dismissed the request without making a choice | Omit |
Response Examples
Accept (Form Mode):
return {
action: 'accept',
data: {
name: 'John Doe',
email: 'john@example.com',
age: 30,
},
};
Accept (URL Mode):
return {
action: 'accept',
// No data field needed - authorization completed externally
};
Decline:
return {
action: 'decline',
// User chose not to provide the requested information
};
Cancel:
return {
action: 'cancel',
// User closed the dialog or dismissed the request
};
Complete Example: React App with Elicitation
import { useMcp } from 'mcp-use/react';
import { useState } from 'react';
function App() {
const [elicitationRequest, setElicitationRequest] = useState(null);
const { tools, callTool } = useMcp({
url: 'http://localhost:3000/mcp',
onElicitation: async (params) => {
// Show elicitation UI and wait for user response
return new Promise((resolve) => {
setElicitationRequest({
params,
resolve,
});
});
},
});
const handleElicitationResponse = (result) => {
if (elicitationRequest) {
elicitationRequest.resolve(result);
setElicitationRequest(null);
}
};
return (
<div>
<h1>MCP Application</h1>
{/* Your main app UI */}
<ToolsList tools={tools} onCallTool={callTool} />
{/* Elicitation modal */}
{elicitationRequest && (
<ElicitationModal
params={elicitationRequest.params}
onResponse={handleElicitationResponse}
/>
)}
</div>
);
}
function ElicitationModal({ params, onResponse }) {
if (params.mode === 'url') {
return (
<OAuthElicitation
params={params}
onComplete={onResponse}
/>
);
} else {
return (
<ElicitationForm
params={params}
onSubmit={onResponse}
onCancel={() => onResponse({ action: 'cancel' })}
/>
);
}
}
Error Handling
If no elicitation callback is provided but a tool requests user input, the tool call will fail:
// Without elicitation callback
const client = new MCPClient(config);
// No elicitationCallback provided
// Tool that requires elicitation will fail
const session = await client.createSession('server-name');
try {
await session.callTool('collect-user-info', {});
} catch (error) {
console.error('Elicitation not supported:', error);
// MCP error: Client does not support elicitation
}
To handle this gracefully:
const client = new MCPClient(config, {
elicitationCallback: async (params) => {
console.warn('Elicitation requested but not implemented');
// Always decline if not implemented
return { action: 'decline' };
},
});
Security Best Practices
Critical Security Rules:
- Always use URL mode for sensitive data - Never collect passwords, API keys, or credentials through form mode
- Validate URLs - Before opening URL mode elicitations, verify the domain is expected
- User consent - Always show the URL to users and get explicit consent before navigating
- Secure context - Open URL mode elicitations in a secure browser context (new window/tab with noopener)
- No auto-submit - Never automatically submit form data without user review
URL Validation Example
const ALLOWED_DOMAINS = ['auth.example.com', 'oauth.github.com'];
async function safeElicitationCallback(params) {
if (params.mode === 'url') {
const url = new URL(params.url);
// Validate domain
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
console.warn(`Untrusted domain: ${url.hostname}`);
return {
action: 'decline',
};
}
// Show URL and get consent
const consent = await getUserConsent(
`Allow navigation to ${url.hostname}?`
);
if (!consent) {
return { action: 'decline' };
}
// Proceed with authorization
window.open(params.url, '_blank', 'noopener,noreferrer');
// ... handle completion
}
// ... handle form mode
}
Next Steps