.do
McpTools

The elicit Tool

Human-in-the-loop workflows via the elicit tool

The elicit Tool

The elicit tool implements the MCP elicitation protocol for human-in-the-loop workflows, enabling AI agents to request structured input from users when needed.

Concept

AI agents often need human guidance for:

  • Approval decisions - "Should I deploy this to production?"
  • Ambiguity resolution - "Which customer did you mean?"
  • Data collection - "What's the customer's shipping address?"
  • Preference selection - "Which design do you prefer?"

The elicit tool bridges AI agents to human users through multiple UI platforms.

Tool Definition

{
  "name": "elicit",
  "description": "Request structured input from user via Human Functions",
  "inputSchema": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string",
        "description": "Message explaining what information is needed"
      },
      "schema": {
        "type": "object",
        "description": "JSON Schema for requested data (flat object only)"
      },
      "uiType": {
        "type": "string",
        "enum": ["slack", "discord", "teams", "web"],
        "description": "UI platform to use (default: slack)"
      }
    },
    "required": ["message", "schema"]
  }
}

How It Works

sequenceDiagram AI Agent->>MCP Server: elicit tool call MCP Server->>Human Worker: Create Human Function Human Worker->>User: Display UI (Slack/Discord/Teams/Web) User->>Human Worker: Submit response Human Worker->>MCP Server: Return structured data MCP Server->>AI Agent: Tool result
  1. AI Agent requests input via elicit tool
  2. MCP Server converts request to Human Function
  3. Human Worker sends UI to user on specified platform
  4. User responds with structured data or declines
  5. MCP Server polls for completion and returns result
  6. AI Agent continues workflow with user input

Schema Restrictions

MCP elicitation supports only flat objects with primitive properties:

✅ Valid Schemas

// Boolean decision
{
  type: "object",
  properties: {
    approved: { type: "boolean", description: "Approval decision" },
    reason: { type: "string", description: "Reason for decision" }
  },
  required: ["approved"]
}

// Multiple choice with enum
{
  type: "object",
  properties: {
    priority: {
      type: "string",
      enum: ["low", "medium", "high"],
      description: "Issue priority"
    }
  }
}

// Number input with constraints
{
  type: "object",
  properties: {
    rating: {
      type: "number",
      minimum: 1,
      maximum: 5,
      description: "Rating from 1-5"
    }
  }
}

// Email validation
{
  type: "object",
  properties: {
    email: {
      type: "string",
      format: "email",
      description: "Contact email"
    }
  }
}

❌ Invalid Schemas

// Nested objects not allowed
{
  type: "object",
  properties: {
    user: {
      type: "object",  // ❌ Nested object
      properties: {
        name: { type: "string" }
      }
    }
  }
}

// Arrays not allowed
{
  type: "object",
  properties: {
    tags: {
      type: "array",  // ❌ Array
      items: { type: "string" }
    }
  }
}

User Response Actions

Users can respond in three ways:

1. Accept - Provide Requested Data

// Tool call
{
  "name": "elicit",
  "arguments": {
    "message": "Do you approve this deployment?",
    "schema": {
      "type": "object",
      "properties": {
        "approved": { "type": "boolean" },
        "reason": { "type": "string" }
      }
    }
  }
}

// User accepts
{
  "content": [
    {
      "type": "text",
      "text": "User provided: {\"approved\":true,\"reason\":\"Looks good to deploy\"}"
    }
  ]
}

2. Decline - Refuse to Provide Data

// User declines
{
  "content": [
    {
      "type": "text",
      "text": "User declined: Not comfortable sharing this information"
    }
  ],
  "isError": true
}

3. Cancel - Cancel the Request

// User cancels (timeout or explicit cancel)
{
  "content": [
    {
      "type": "text",
      "text": "User canceled: Request timed out"
    }
  ],
  "isError": true
}

UI Platforms

Slack (Current)

Uses jsx-slack for interactive BlockKit forms:

await elicit({
  message: 'Approve this expense report?',
  schema: {
    type: 'object',
    properties: {
      approved: { type: 'boolean' },
      notes: { type: 'string' },
    },
  },
  uiType: 'slack',
})

Slack displays:

  • Message as header
  • Form fields based on schema
  • Approve/Decline buttons
  • Cancel button

Discord (Planned)

Will use Discord embeds and components:

await elicit({
  message: 'Select a deployment environment',
  schema: {
    type: 'object',
    properties: {
      environment: {
        type: 'string',
        enum: ['development', 'staging', 'production'],
      },
    },
  },
  uiType: 'discord',
})

Teams (Planned)

Will use Adaptive Cards:

await elicit({
  message: 'Review this purchase order',
  schema: {
    type: 'object',
    properties: {
      approved: { type: 'boolean' },
      comments: { type: 'string' },
    },
  },
  uiType: 'teams',
})

Web (Planned)

Will use React forms at humans.do:

await elicit({
  message: 'Provide your shipping address',
  schema: {
    type: 'object',
    properties: {
      street: { type: 'string' },
      city: { type: 'string' },
      zip: { type: 'string' },
    },
  },
  uiType: 'web',
})

Timeouts

  • Anonymous users: 3 minutes
  • Authenticated users: 5 minutes
  • Polling interval: 5 seconds

After timeout, the request is automatically canceled:

{
  "content": [
    {
      "type": "text",
      "text": "User canceled: Request timed out after 3 minutes"
    }
  ],
  "isError": true
}

Use Cases

Approval Workflows

// Deploy to production approval
const approval = await elicit({
  message: 'Approve deployment to production?',
  schema: {
    type: 'object',
    properties: {
      approved: { type: 'boolean' },
      reason: { type: 'string' },
    },
    required: ['approved'],
  },
})

if (approval.approved) {
  await deploy('production')
} else {
  console.log('Deployment declined:', approval.reason)
}

Data Collection

// Collect customer feedback
const feedback = await elicit({
  message: 'Please rate your experience',
  schema: {
    type: 'object',
    properties: {
      rating: {
        type: 'number',
        minimum: 1,
        maximum: 5,
        description: 'Rating from 1-5 stars',
      },
      comment: {
        type: 'string',
        description: 'Additional feedback',
      },
    },
    required: ['rating'],
  },
})

await db.create('Feedback', {
  rating: feedback.rating,
  comment: feedback.comment,
  timestamp: new Date(),
})

Ambiguity Resolution

// Multiple customers with same name
const customers = await db.list('Customer', { name: 'John Smith' })

if (customers.length > 1) {
  const selection = await elicit({
    message: 'Multiple customers found. Which one?',
    schema: {
      type: 'object',
      properties: {
        customerId: {
          type: 'string',
          enum: customers.map((c) => c.id),
          description: 'Select customer ID',
        },
      },
      required: ['customerId'],
    },
  })

  const customer = await db.get('Customer', selection.customerId)
}

Form Validation

// Collect and validate contact info
const contact = await elicit({
  message: 'Please verify your contact information',
  schema: {
    type: 'object',
    properties: {
      email: {
        type: 'string',
        format: 'email',
        description: 'Email address',
      },
      phone: {
        type: 'string',
        description: 'Phone number',
      },
    },
    required: ['email'],
  },
})

await db.update('Customer', customerId, {
  email: contact.email,
  phone: contact.phone,
})

Preference Selection

// Choose notification preferences
const prefs = await elicit({
  message: 'How would you like to be notified?',
  schema: {
    type: 'object',
    properties: {
      emailNotifications: {
        type: 'boolean',
        description: 'Receive email notifications',
      },
      smsNotifications: {
        type: 'boolean',
        description: 'Receive SMS notifications',
      },
      frequency: {
        type: 'string',
        enum: ['immediate', 'daily', 'weekly'],
        description: 'Notification frequency',
      },
    },
  },
})

await db.update('UserPreferences', userId, prefs)

Error Handling

Handle User Decline

try {
  const approval = await elicit({
    message: 'Approve this action?',
    schema: {
      type: 'object',
      properties: {
        approved: { type: 'boolean' },
      },
    },
  })

  if (approval.approved) {
    // Proceed with action
  }
} catch (error) {
  if (error.message.includes('declined')) {
    console.log('User declined the request')
    // Handle decline gracefully
  } else if (error.message.includes('canceled')) {
    console.log('Request timed out or was canceled')
    // Handle timeout
  }
}

Provide Defaults

// Request input with fallback to defaults
let config
try {
  config = await elicit({
    message: 'Configure deployment settings (or use defaults)',
    schema: {
      type: 'object',
      properties: {
        environment: {
          type: 'string',
          enum: ['staging', 'production'],
        },
        replicas: {
          type: 'number',
          minimum: 1,
          maximum: 10,
        },
      },
    },
  })
} catch (error) {
  // Use defaults if user doesn't respond
  config = {
    environment: 'staging',
    replicas: 2,
  }
}

await deploy(config)

Best Practices

1. Clear Messages

// ❌ Vague
message: 'Approve?'

// ✅ Clear
message: 'Approve deployment of v2.1.0 to production? This will affect 1000+ users.'

2. Required vs Optional

// Mark truly required fields
{
  type: "object",
  properties: {
    approved: { type: "boolean" },
    reason: { type: "string" }  // Optional
  },
  required: ["approved"]  // Only approval is required
}

3. Use Enums for Choices

// ❌ Free text
priority: { type: "string" }

// ✅ Constrained choices
priority: {
  type: "string",
  enum: ["low", "medium", "high"]
}

4. Provide Descriptions

{
  type: "object",
  properties: {
    budget: {
      type: "number",
      description: "Maximum budget in USD",  // Help users understand
      minimum: 0,
      maximum: 10000
    }
  }
}

Integration with do Tool

Combine elicit with do for powerful workflows:

// Get user input, then execute operations
const orderDetails = await elicit({
  message: 'Provide order details',
  schema: {
    type: 'object',
    properties: {
      productId: { type: 'string' },
      quantity: { type: 'number' },
    },
  },
})

// Use input to create order
const product = await db.get('Product', orderDetails.productId)
const order = await db.create('Order', {
  customer: user.current(),
  items: [
    {
      product: product,
      quantity: orderDetails.quantity,
    },
  ],
  total: product.price * orderDetails.quantity,
})

// Confirm with user
await send($.Email.send, {
  to: user.current().email,
  subject: 'Order Confirmation',
  body: `Your order #${order.id} has been created`,
})

Next Steps