@leanmcp/elicitation
v0.1.0
Published
Elicitation support for LeanMCP - structured user input collection
Downloads
21
Maintainers
Readme
@leanmcp/elicitation
Structured user input collection for LeanMCP tools using the MCP elicitation protocol. The @Elicitation decorator automatically intercepts tool calls to request missing required fields from users before execution.
Features
- @Elicitation decorator - Declarative way to collect missing user inputs
- Method wrapping - Automatically intercepts calls and returns elicitation requests
- Multiple strategies - Form, multi-step, and conversational elicitation
- Fluent builder API - Programmatic form creation with type safety
- Built-in validation - Email, URL, pattern matching, custom validators
- Conditional elicitation - Only ask for inputs when needed
- Type-safe - Full TypeScript support with type inference
Installation
npm install @leanmcp/elicitation @leanmcp/coreQuick Start
1. Simple Form Elicitation
import { Tool } from "@leanmcp/core";
import { Elicitation } from "@leanmcp/elicitation";
class SlackService {
@Tool({ description: "Create a new Slack channel" })
@Elicitation({
title: "Create Channel",
description: "Please provide channel details",
fields: [
{
name: "channelName",
label: "Channel Name",
type: "text",
required: true,
validation: {
pattern: "^[a-z0-9-]+$",
errorMessage: "Must be lowercase alphanumeric with hyphens"
}
},
{
name: "isPrivate",
label: "Private Channel",
type: "boolean",
defaultValue: false
}
]
})
async createChannel(args: { channelName: string; isPrivate: boolean }) {
// Implementation
return { success: true, channelName: args.channelName };
}
}2. Conditional Elicitation
Only ask for inputs when they're missing:
@Tool({ description: "Send message to Slack" })
@Elicitation({
condition: (args) => !args.channelId,
title: "Select Channel",
fields: [
{
name: "channelId",
label: "Channel",
type: "select",
required: true,
options: [
{ label: "#general", value: "C12345" },
{ label: "#random", value: "C67890" }
]
}
]
})
async sendMessage(args: { channelId?: string; message: string }) {
// Only elicits if channelId is missing
}3. Fluent Builder API
More programmatic approach:
import { ElicitationFormBuilder, validation } from "@leanmcp/elicitation";
@Tool({ description: "Create user account" })
@Elicitation({
builder: () => new ElicitationFormBuilder()
.title("User Registration")
.description("Create a new user account")
.addEmailField("email", "Email Address", { required: true })
.addTextField("username", "Username", {
required: true,
validation: validation()
.minLength(3)
.maxLength(20)
.pattern("^[a-zA-Z0-9_]+$")
.build()
})
.addSelectField("role", "Role", [
{ label: "Admin", value: "admin" },
{ label: "User", value: "user" }
])
.build()
})
async createUser(args: any) {
// Implementation
}4. Multi-Step Elicitation
Break input collection into multiple steps:
@Tool({ description: "Deploy application" })
@Elicitation({
strategy: "multi-step",
builder: () => [
{
title: "Step 1: Environment",
fields: [
{
name: "environment",
label: "Environment",
type: "select",
required: true,
options: [
{ label: "Production", value: "prod" },
{ label: "Staging", value: "staging" }
]
}
]
},
{
title: "Step 2: Configuration",
fields: [
{
name: "replicas",
label: "Replicas",
type: "number",
defaultValue: 3
}
],
condition: (prev) => prev.environment === "prod"
}
]
})
async deployApp(args: any) {
// Implementation
}Field Types
Text Fields
{
name: "description",
label: "Description",
type: "text",
placeholder: "Enter description...",
validation: {
minLength: 10,
maxLength: 500
}
}Textarea
{
name: "content",
label: "Content",
type: "textarea",
placeholder: "Enter long text..."
}Number
{
name: "age",
label: "Age",
type: "number",
validation: {
min: 18,
max: 120
}
}Boolean (Checkbox)
{
name: "agree",
label: "I agree to terms",
type: "boolean",
defaultValue: false
}Select (Dropdown)
{
name: "country",
label: "Country",
type: "select",
options: [
{ label: "United States", value: "US" },
{ label: "Canada", value: "CA" }
]
}Multi-Select
{
name: "tags",
label: "Tags",
type: "multiselect",
options: [
{ label: "JavaScript", value: "js" },
{ label: "TypeScript", value: "ts" },
{ label: "Python", value: "py" }
]
}{
name: "email",
label: "Email",
type: "email",
required: true
}URL
{
name: "website",
label: "Website",
type: "url",
placeholder: "https://example.com"
}Date
{
name: "birthdate",
label: "Birth Date",
type: "date"
}Validation
Built-in Validators
{
name: "username",
label: "Username",
type: "text",
validation: {
minLength: 3,
maxLength: 20,
pattern: "^[a-zA-Z0-9_]+$",
errorMessage: "Username must be 3-20 alphanumeric characters"
}
}Custom Validators
{
name: "password",
label: "Password",
type: "text",
validation: {
customValidator: (value) => {
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
if (!hasUpper || !hasLower || !hasNumber) {
return "Password must contain uppercase, lowercase, and numbers";
}
return true; // Valid
}
}
}Using ValidationBuilder
import { validation } from "@leanmcp/elicitation";
validation()
.minLength(8)
.maxLength(100)
.pattern("^[a-zA-Z0-9]+$")
.customValidator((value) => value !== "admin")
.errorMessage("Invalid input")
.build()How It Works
- Client calls tool with missing required fields
- Decorator intercepts the method call before execution
- Elicitation check determines if required fields are missing
- Elicitation request returned if fields are missing
- Client displays form to collect user input
- Client calls tool again with complete arguments
- Method executes normally with all required fields
Key Benefits:
- Automatic interception - No need to modify
@leanmcp/core - Clean separation - Elicitation logic separate from business logic
- MCP compliant - Follows MCP elicitation protocol
- Type-safe - Full TypeScript support
Strategies
Form Strategy (Default)
Collect all fields at once:
@Elicitation({
strategy: "form", // or omit, form is default
title: "User Information",
fields: [/* ... */]
})Multi-Step Strategy
Break input collection into sequential steps:
@Elicitation({
strategy: "multi-step",
builder: () => [
{
title: "Step 1: Basic Info",
fields: [/* step 1 fields */]
},
{
title: "Step 2: Details",
fields: [/* step 2 fields */],
condition: (prev) => prev.needsDetails === true
}
]
})Elicitation Flow
Request/Response Cycle
First Call (Missing Fields):
// Request
{
"method": "tools/call",
"params": {
"name": "createChannel",
"arguments": {}
}
}
// Response (Elicitation Request)
{
"content": [{
"type": "text",
"text": "{\n \"type\": \"elicitation\",\n \"title\": \"Create Channel\",\n \"fields\": [...]\n}"
}]
}Second Call (Complete Fields):
// Request
{
"method": "tools/call",
"params": {
"name": "createChannel",
"arguments": {
"channelName": "my-channel",
"isPrivate": false
}
}
}
// Response (Tool Result)
{
"content": [{
"type": "text",
"text": "{\"success\": true, \"channelId\": \"C123\"}"
}]
}API Reference
ElicitationConfig
interface ElicitationConfig {
strategy?: 'form' | 'multi-step';
title?: string;
description?: string;
fields?: ElicitationField[];
condition?: (args: any) => boolean;
builder?: (context: ElicitationContext) => ElicitationRequest | ElicitationStep[];
}ElicitationField
interface ElicitationField {
name: string;
label: string;
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'date' | 'email' | 'url' | 'textarea';
description?: string;
required?: boolean;
defaultValue?: any;
options?: Array<{ label: string; value: any }>;
validation?: FieldValidation;
placeholder?: string;
helpText?: string;
}FieldValidation
interface FieldValidation {
min?: number;
max?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
customValidator?: (value: any) => boolean | string;
errorMessage?: string;
}Complete Example
See examples/slack-with-elicitation for a complete working example.
import { createHTTPServer, MCPServer, Tool } from "@leanmcp/core";
import { Elicitation, ElicitationFormBuilder, validation } from "@leanmcp/elicitation";
class SlackService {
@Tool({ description: "Create a new Slack channel" })
@Elicitation({
title: "Create Channel",
description: "Please provide channel details",
fields: [
{
name: "channelName",
label: "Channel Name",
type: "text",
required: true,
validation: {
pattern: "^[a-z0-9-]+$",
errorMessage: "Must be lowercase alphanumeric with hyphens"
}
},
{
name: "isPrivate",
label: "Private Channel",
type: "boolean",
defaultValue: false
}
]
})
async createChannel(args: { channelName: string; isPrivate: boolean }) {
return {
success: true,
channelId: `C${Date.now()}`,
channelName: args.channelName
};
}
}
// Start server
const serverFactory = () => {
const server = new MCPServer({ name: "slack-server", version: "1.0.0" });
server.registerService(new SlackService());
return server.getServer();
};
await createHTTPServer(serverFactory, { port: 3000 });Error Handling
try {
const result = await service.createChannel({ channelName: "test" });
console.log(result);
} catch (error) {
console.error('Tool execution failed:', error);
}If elicitation is needed, the method returns an ElicitationRequest object instead of throwing an error.
Best Practices
- Use conditional elicitation - Only ask when truly needed
- Provide sensible defaults - Reduce user input burden
- Clear field labels - Make fields self-explanatory
- Validate early - Catch errors before submission
- Group related fields - Use multi-step for complex forms
- Test thoroughly - Test both elicitation and execution paths
- Use builder for complex forms - Fluent API is more maintainable
- Add help text - Guide users with helpful descriptions
Related Packages
- @leanmcp/core - Core MCP server functionality
- @leanmcp/auth - Authentication for MCP tools
- @leanmcp/utils - Utility functions
Links
License
MIT
