@Elicitation decorator to create forms, multi-step wizards, and conditional prompts.
Quick Start
import { Tool } from '@leanmcp/core';
import { Elicitation } from '@leanmcp/elicitation';
@Tool({ description: 'Create Slack channel' })
@Elicitation({
title: 'Create Channel',
fields: [
{ name: 'channelName', label: 'Channel Name', type: 'text', required: true },
{ name: 'isPrivate', label: 'Private Channel', type: 'boolean', defaultValue: false }
]
})
async createChannel(args: { channelName?: string; isPrivate?: boolean }) {
// Method only executes after user provides required fields
console.log('Creating channel:', args.channelName);
return { success: true };
}
Form Fields
Field Types
Supported field types:type FieldType =
| 'text' // Single-line text input
| 'textarea' // Multi-line text input
| 'number' // Numeric input
| 'boolean' // Checkbox
| 'select' // Dropdown (single choice)
| 'multiselect' // Multi-select dropdown
| 'date' // Date picker
| 'email' // Email input with validation
| 'url'; // URL input with validation
packages/elicitation/src/types.ts:25
Field Configuration
interface ElicitationField {
name: string; // Field identifier
label: string; // Display label
type: FieldType; // Input type
description?: string; // Help text
required?: boolean; // Is field required?
defaultValue?: any; // Default value
options?: Array<{ // For select/multiselect
label: string;
value: any;
}>;
validation?: FieldValidation; // Validation rules
placeholder?: string; // Input placeholder
helpText?: string; // Additional help text
}
packages/elicitation/src/types.ts:22
Text Fields
@Elicitation({
title: 'Send Message',
fields: [
{
name: 'message',
label: 'Message',
type: 'text',
required: true,
placeholder: 'Enter your message',
helpText: 'This will be sent to all subscribers'
}
]
})
async sendMessage(args: { message?: string }) {
// ...
}
Select Fields
@Elicitation({
title: 'Deploy Application',
fields: [
{
name: 'environment',
label: 'Environment',
type: 'select',
required: true,
options: [
{ label: 'Development', value: 'dev' },
{ label: 'Staging', value: 'staging' },
{ label: 'Production', value: 'prod' }
]
}
]
})
async deploy(args: { environment?: string }) {
// ...
}
Number Fields
@Elicitation({
fields: [
{
name: 'replicas',
label: 'Number of Replicas',
type: 'number',
defaultValue: 3,
validation: {
min: 1,
max: 10,
errorMessage: 'Must be between 1 and 10'
}
}
]
})
async scale(args: { replicas?: number }) {
// ...
}
Validation
Built-in Validation
interface FieldValidation {
min?: number; // Minimum value (for numbers)
max?: number; // Maximum value (for numbers)
minLength?: number; // Minimum length (for strings)
maxLength?: number; // Maximum length (for strings)
pattern?: string; // Regex pattern
customValidator?: (value: any) => boolean | string;
errorMessage?: string; // Custom error message
}
packages/elicitation/src/types.ts:47
Validation Examples
import { validation } from '@leanmcp/elicitation';
@Elicitation({
fields: [
{
name: 'username',
label: 'Username',
type: 'text',
required: true,
validation: validation()
.minLength(3)
.maxLength(20)
.pattern('^[a-zA-Z0-9_]+$')
.errorMessage('Username must be 3-20 alphanumeric characters')
.build()
},
{
name: 'age',
label: 'Age',
type: 'number',
validation: validation()
.min(18)
.max(120)
.errorMessage('Must be between 18 and 120')
.build()
},
{
name: 'email',
label: 'Email',
type: 'email',
validation: validation()
.customValidator((value) => {
return value.endsWith('@company.com') || 'Must use company email';
})
.build()
}
]
})
async createUser(args: any) {
// ...
}
packages/elicitation/src/builders/form-builder.ts:226
Conditional Elicitation
Only show the form when certain conditions are met:@Tool({ description: 'Send message' })
@Elicitation({
condition: (args) => !args.channelId, // Only elicit if missing
title: 'Select Channel',
fields: [
{
name: 'channelId',
label: 'Channel',
type: 'select',
required: true,
options: [
{ label: '#general', value: 'C123' },
{ label: '#random', value: 'C456' }
]
}
]
})
async sendMessage(args: { channelId?: string; text: string }) {
// If channelId is provided, skip elicitation
// If missing, show channel selector
}
packages/elicitation/src/decorators.ts:48
Multi-Step Elicitation
Create wizard-like flows with multiple steps:import { ElicitationStep } from '@leanmcp/elicitation';
@Tool({ description: 'Deploy application' })
@Elicitation({
strategy: 'multi-step',
builder: () => [
{
title: 'Step 1: Select Environment',
description: 'Choose deployment target',
fields: [
{
name: 'environment',
label: 'Environment',
type: 'select',
required: true,
options: [
{ label: 'Development', value: 'dev' },
{ label: 'Production', value: 'prod' }
]
}
]
},
{
title: 'Step 2: Configure Resources',
fields: [
{ name: 'replicas', label: 'Replicas', type: 'number', defaultValue: 3 },
{ name: 'memory', label: 'Memory (GB)', type: 'number', defaultValue: 2 }
],
condition: (previousValues) => previousValues.environment === 'prod'
},
{
title: 'Step 3: Confirm',
fields: [
{ name: 'confirm', label: 'Deploy now?', type: 'boolean', required: true }
]
}
]
})
async deployApp(args: any) {
// Receives all values from all steps
console.log('Environment:', args.environment);
console.log('Replicas:', args.replicas);
console.log('Confirmed:', args.confirm);
}
packages/elicitation/src/decorators.ts:61
Form Builder API
Use the fluent builder for complex forms:import { ElicitationFormBuilder } from '@leanmcp/elicitation';
@Elicitation({
builder: () => new ElicitationFormBuilder()
.title('Create Slack Channel')
.description('Configure your new channel')
.addTextField('channelName', 'Channel Name', {
required: true,
placeholder: '#general',
validation: validation()
.pattern('^[a-z0-9-_]+$')
.maxLength(80)
.build()
})
.addBooleanField('isPrivate', 'Private Channel', {
defaultValue: false
})
.addSelectField('team', 'Team', [
{ label: 'Engineering', value: 'eng' },
{ label: 'Marketing', value: 'mkt' },
{ label: 'Sales', value: 'sales' }
])
.addTextAreaField('description', 'Description', {
placeholder: 'What is this channel for?',
validation: validation().maxLength(500).build()
})
.build()
})
async createChannel(args: any) {
// ...
}
packages/elicitation/src/builders/form-builder.ts:18
Available Builder Methods
class ElicitationFormBuilder {
title(title: string): this
description(description: string): this
condition(condition: (args: any) => boolean): this
addTextField(name, label, options?): this
addTextAreaField(name, label, options?): this
addNumberField(name, label, options?): this
addBooleanField(name, label, options?): this
addSelectField(name, label, options, fieldOptions?): this
addMultiSelectField(name, label, options, fieldOptions?): this
addEmailField(name, label, options?): this
addUrlField(name, label, options?): this
addDateField(name, label, options?): this
addCustomField(field: ElicitationField): this
build(): ElicitationConfig
}
Dynamic Forms
Build forms dynamically based on context:@Elicitation({
builder: (context) => {
const fields = [
{ name: 'message', label: 'Message', type: 'text', required: true }
];
// Add channel selector if not provided
if (!context.args.channelId) {
fields.unshift({
name: 'channelId',
label: 'Channel',
type: 'select',
required: true,
options: getChannelOptions() // Dynamic options
});
}
return {
title: 'Send Message',
fields
};
}
})
async sendMessage(args: any) {
// ...
}
Error Handling
import { ElicitationError } from '@leanmcp/elicitation';
try {
await elicitUserInput();
} catch (error) {
if (error instanceof ElicitationError) {
console.error('Elicitation error:', error.code);
console.error('Details:', error.details);
}
}
packages/elicitation/src/types.ts:116
Best Practices
1. Use Clear Labels and Help Text
// Good
{
name: 'apiKey',
label: 'OpenAI API Key',
type: 'text',
helpText: 'Found at https://platform.openai.com/api-keys',
placeholder: 'sk-...',
required: true
}
// Bad
{
name: 'key',
label: 'Key',
type: 'text'
}
2. Provide Sensible Defaults
{
name: 'replicas',
label: 'Number of Replicas',
type: 'number',
defaultValue: 3, // Good default
validation: validation().min(1).max(10).build()
}
3. Validate User Input
Always validate user input to prevent errors:{
name: 'email',
label: 'Email Address',
type: 'email',
validation: validation()
.pattern('^[^@]+@[^@]+\\.[^@]+$')
.errorMessage('Please enter a valid email')
.build()
}
4. Use Conditional Logic
Only ask for what you need:@Elicitation({
condition: (args) => !args.userId,
fields: [{ name: 'userId', label: 'User ID', type: 'text', required: true }]
})
async getUser(args: { userId?: string }) {
// Only elicit if userId is missing
}
Next Steps
Authentication
Secure your elicitation flows
Multi-Tenancy
Per-user form data