Elicitation
Teaching agents to think in types, not just fill in forms
The Problem: JSON Forms vs. Domain Languages
Most MCP servers follow a familiar pattern: expose domain objects as JSON schemas, let agents fill in forms. This works, but it's backwards:
// What most MCP servers do:
// "Here's a User form. Fill it in."
let user = agent.call_tool;
The agent is stuck in JSON-land, translating between natural language and key-value pairs. No understanding of what a User actually is, no concept of validity beyond "did the JSON match?"
The Vision: Agents That Speak Your Domain
Elicitation flips the script. Instead of forms, you give agents the building blocks of your domain—the types, the constraints, the compositional rules—and let them construct values through conversation:
// What elicitation does:
// "Here's how to construct a valid User. Go."
// Agent now speaks in User-construction steps:
// 1. Select a name (String elicitation)
// 2. Construct a valid Email (format validation built-in)
// 3. Choose an age (0-255, type-guaranteed)
let user = elicit.await?;
The difference? The agent understands the structure. It's not filling a form—it's building a User through a sequence of typed operations.
What Is Elicitation?
Elicitation is a Rust library that turns sampling interactions (calls to LLMs via MCP) into strongly-typed domain values. But it's not just type-safe JSON deserialization—it's a framework for teaching agents to:
- Think compositionally - Build complex types from simpler ones
- Respect constraints - Types encode validity (Email formats, bounded numbers)
- Follow processes - Multi-step construction with step-by-step guidance
- Verify formally - Contracts and composition rules checked at compile time
- Adapt contextually - Swap prompts/styles without changing types
Think of it as a DSL for agent-driven data construction, where the "syntax" is your Rust types and the "semantics" are guaranteed by the compiler.
Tutorial: From Simple Values to Complex Domains
Part 1: The Four Interaction Mechanics
Elicitation provides four fundamental ways agents construct values:
1. Select - Choose from finite options
Used for enums, where the agent picks one variant:
// Agent sees: "Select Priority: Low, Medium, High, Critical"
let priority = elicit.await?;
When to use: Finite choice sets, enum variants, discriminated unions.
2. Affirm - Yes/no decisions
Used for booleans:
// Agent sees: "Affirm: Should this task be urgent? (yes/no)"
let urgent: bool = boolelicit.await?;
When to use: Binary decisions, flags, opt-in/opt-out.
3. Survey - Multi-field construction
Used for structs, where the agent builds each field in sequence:
// Agent follows a 3-step process:
// 1. Provide title (String)
// 2. Select priority (Priority enum)
// 3. Affirm urgency (bool)
let task = elicit.await?;
When to use: Product types, records, multi-field structures.
4. Authorize - Permission policies (future)
For access control and capability-based security.
Why these four? They map to fundamental type constructors: sums (Select), booleans (Affirm), products (Survey), and effects (Authorize). Every Rust type decomposes into these primitives.
Part 2: Compositionality - Types All The Way Down
The power of elicitation is infinite composition. Every type that implements Elicitation can be nested in any other:
// Agent can construct an entire organization structure:
let org = elicit.await?;
This works because:
Vec<T>implementsElicitationifTdoes (recursive elicitation)Option<T>implementsElicitationifTdoes (optional fields)- Your custom structs implement via
#[derive(Elicit)] - Primitives implement it built-in
No depth limit. Nest 10 levels deep, 100 fields wide—it composes.
Part 3: Validity Guarantees
Elicitation isn't just data entry—it's construction with guarantees. Types encode constraints that the agent must respect:
Type-Level Constraints
use Bounded;
)]
u16
); // Must be in range 1024-65535
]
String
); // Must pass validation function
Contract System (Formal Verification)
Elicitation v0.5.0 introduced contracts: type-level proofs that operations maintain invariants.
use ;
// Define propositions (contracts)
;
;
// Function requiring proofs
// Compose workflow with proofs
let email_proof = validate_email?;
let consent_proof = obtain_consent?;
let both_proofs = both;
register_user; // ✓ Compiles
register_user; // ✗ Missing consent proof
Verified with Kani: 183 symbolic execution checks prove the contract system works correctly. Build multi-step agent workflows with mathematical guarantees.
Part 4: Style System - Context-Aware Prompts
Agents need context. The same Email type might be elicited differently in different scenarios:
use ;
// Define custom styles for Email
// Use different prompts based on style
let work_email = elicit_styled.await?;
// Prompt: "Provide work email address (e.g., name@company.com)"
let personal_email = elicit_styled.await?;
// Prompt: "Provide personal email address"
Hot-swapping prompts without changing types. One Email type, multiple presentation contexts. Extensible: define custom styles for any type, including built-ins like String, i32, etc.
Part 5: Generators - Alternate Constructors
Sometimes you need to construct values in different ways. Elicitation provides generators for alternate construction paths.
Real-world example: std::time::Instant has a now() generator:
use Instant;
// Option 1: Agent provides manual timing (default elicitation)
let instant1 = elicit.await?;
// Option 2: Use generator to capture current time
let instant2 = elicit_with_generator.await?;
// Equivalent to: Instant::now()
Why this matters: Some types have natural "smart constructors" that don't require user input:
Instant::now()- Current timestampSystemTime::now()- Current system timeUuid::new_v4()- Random UUID- Factory patterns with defaults
Custom generators:
// Agent can choose:
// 1. from_template: Start with defaults
// 2. from_env: Load from environment variables
// 3. (default): Build each field manually
Use cases:
- Smart constructors (now(), random(), default())
- Environment-based initialization
- Template expansion
- Multi-stage construction
Part 6: Random Generation - Testing & Simulation
For testing, gaming, and simulation, you need random data. The #[derive(Rand)] macro generates contract-aware random values:
use ;
;
// Random dice rolls that respect the contract
let generator = D6random_generator;
let roll = generator.generate; // Always in [1, 6]
Perfect symmetry: If you can elicit it, you can randomly generate it.
Contract-Aware Generation
Contracts map to appropriate sampling strategies:
; // Uniform [1, 100]
; // Positive integers only
; // Even numbers only
; // Positive AND bounded
Automatic Support for All Types
Works with primitives, third-party types, and custom types:
// Primitives
let gen = u32rand_generator;
let n = gen.generate;
// Third-party types
let gen = rand_generator;
let id = gen.generate;
let gen = rand_generator;
let url = gen.generate;
// Collections
let gen = new;
let strings = gen.generate;
// Custom types with contracts
Use Cases
Testing:
// Property-based testing
for _ in 0..1000
Gaming:
// Agent as game master
let encounter = random_generator.generate;
let loot = random_generator.generate;
Simulation:
// Generate realistic test data
let users: =
.map
.collect;
Supported types:
- Primitives: u8-u128, i8-i128, f32, f64, bool, char
- Stdlib: String, PathBuf, Duration, SystemTime
- Third-party: DateTime (chrono), Timestamp (jiff), Uuid, Url
- Custom: Any type with
#[derive(Rand)] - Collections: Vec, HashMap, HashSet (via generators)
Part 7: Trait-Based MCP Tools (v0.6.0+)
For more complex systems, you might have trait-based APIs. Elicitation supports automatic tool generation from traits:
use elicit_trait_tools_router;
// Automatically generate MCP tools from trait methods
Why this matters:
- Expose entire trait-based APIs as MCP tools
- 80-90% less boilerplate (no manual wrapper functions)
- Supports
async_traitfor object safety (trait objects work!) - Compose regular tools with elicitation tools seamlessly
The Complete Picture: Agent-Native Domain Languages
Here's what you get when you use elicitation:
-
Types as Specifications
- Your Rust types define what is valid
- The compiler checks correctness
- Agents see structured operations, not key-value forms
-
Compositionality as Architecture
- Build complex systems from simple pieces
- Nest types arbitrarily deep
- Reuse elicitation logic across your domain
-
Contracts as Guarantees
- Express invariants as type-level proofs
- Compose workflows with verified properties
- Catch logic errors at compile time, not runtime
-
Styles as Adaptation
- Same types, different contexts
- Hot-swap prompts without code changes
- Customize presentation per use case
-
Verification as Confidence
- Formally verified with Kani model checker
- 183 symbolic checks prove correctness
- Zero-cost abstractions (proofs compile away)
The result? Agents that don't just fill forms—they construct valid domain values through typed operations. They speak your domain language, follow your invariants, and produce verified outputs.
Quick Start
Installation
[]
= "0.6"
= "0.14" # Rust MCP SDK
= { = "1", = ["full"] }
Basic Example
use ;
use Client;
async
Run with Claude Desktop or CLI:
# or
Requirements and Constraints
Required Derives
All types using #[derive(Elicit)] must implement three traits:
use JsonSchema;
use ;
use Elicit;
Why each derive is required:
Serialize- Convert Rust values to JSON for MCP responsesDeserialize- Parse agent selections back into Rust typesJsonSchema- Generate JSON schemas for MCP tool definitionsElicit- Generate the elicitation logic (our derive macro)
Optional but recommended:
Debug- For printing/logging during developmentClone- Many async patterns need cloneable values
Field Type Constraints
All field types in your structs must also implement Elicitation:
// ✅ VALID: All fields implement Elicitation
// ❌ INVALID: CustomEmail doesn't implement Elicitation
// ✅ FIX: Derive Elicit for nested types
;
Common Pitfalls
1. Missing JsonSchema on Nested Types
// ❌ BAD: Address missing JsonSchema
// ✅ GOOD: Add JsonSchema to all nested types
2. Generic Types Need Bounds
// ❌ BAD: Missing trait bounds
// ✅ GOOD: Add proper bounds
3. Enums Must Have Serde Attributes
// ❌ BAD: Complex enum variants without serde tags
// ✅ GOOD: Add serde tagging for complex enums
4. PhantomData Needs Skip
// ✅ GOOD: Skip non-serializable fields
use PhantomData;
Trait Tools Requirements
When using #[elicit_trait_tools_router], parameter and result types need the same derives:
// Tool parameter types
// Tool result types
Note: These don't need Elicit derive (they're not elicited, just passed as JSON).
Async Requirements
Traits using #[elicit_trait_tools_router] need proper async signatures:
// Pattern 1: impl Future + Send (zero-cost)
// Pattern 2: async_trait (object-safe)
See ELICIT_TRAIT_TOOLS_ROUTER.md for complete details.
Quick Checklist
Before deriving Elicit:
- Type has
Serialize + Deserialize + JsonSchema - All field types implement
Elicitation - Nested types have all required derives
- Generic types have proper bounds
- Complex enums have serde tagging
- PhantomData fields are marked
#[serde(skip)]
Integrating with rmcp Tool Routers
Elicitation tools compose seamlessly with regular rmcp tools using the #[tool_router] macro. This is the standard pattern for exposing both elicitation capabilities and domain-specific operations.
Basic Composition Pattern
use ;
use ;
use JsonSchema;
use ;
// 1. Define elicitable types
// 2. Define regular tool types
// 3. Compose both in one server
;
// Generate elicitation tools
// Generate tool router
Result: Server exposes 3 tools:
status- Regular toolrestart- Regular toolelicit_config- Elicitation tool (auto-generated)
Multiple Elicitation Types
You can generate tools for multiple types at once:
// Multiple types
Trait-Based Tool Composition
Combine #[elicit_trait_tools_router] with regular tools:
use elicit_trait_tools_router;
Macro Ordering Rules
Critical: Macros must be applied in this order:
// 1. Generate elicitation methods
// 2. Generate trait tool wrappers
// 3. Discover all #[tool] methods
Why? Each macro expands before the next one runs:
#[elicit_tools]adds methods with#[tool]attributes#[elicit_trait_tools_router]adds more methods with#[tool]attributes#[tool_router]discovers all methods marked with#[tool]
Tool Discovery
All tools are automatically discovered and registered:
// After macro expansion, you have:
let router = tool_router;
let tools = router.list_all;
// Tools discovered:
// - Regular tools (marked with #[tool])
// - Elicitation tools (generated by #[elicit_tools])
// - Trait tools (generated by #[elicit_trait_tools_router])
println!;
for tool in &tools
Complete Server Example
Here's a full-featured server using all composition patterns:
use ;
use *;
// Elicitable domain types
// Trait for business logic
// Server combining everything
// Elicitation tools
// Trait tools
// Discover all
// Server now exposes 5 tools:
// 1. status - Regular tool
// 2. version - Regular tool
// 3. elicit_user - Elicitation tool (auto-generated)
// 4. elicit_config - Elicitation tool (auto-generated)
// 5. get_user - Trait tool (auto-generated)
Benefits of Composition
Unified API: Agents see one consistent interface:
Type Safety: All tools share the same type system:
- Regular tools: explicit implementations
- Elicitation tools: derived from domain types
- Trait tools: derived from trait methods
Composability: Mix and match freely:
- Add elicitation to existing servers
- Add regular tools to elicitation-focused servers
- Expose trait-based APIs alongside utilities
Common Patterns
Pattern 1: Configuration + Operations
// Let agents configure
Pattern 2: CRUD + Construction
// Construct entities
Pattern 3: Trait API + Utilities
// Core API
See Also
- ELICIT_TRAIT_TOOLS_ROUTER.md - Trait tools guide
- TOOL_ROUTER_WARNINGS.md - Addressing rmcp warnings
- tests/composition_systematic_test.rs - Composition examples
Architecture
The Elicitation Trait
The core abstraction:
Every type that implements this trait can be constructed through agent interaction. The derive macro generates the implementation automatically.
How It Works
-
At compile time:
#[derive(Elicit)]generates:Elicitationtrait implementation- MCP tool definitions (JSON schemas)
- Prompt templates for each field
- Validation logic
-
At runtime: Agent calls
Type::elicit():- Library presents structured prompts to agent
- Agent responds with selections/values
- Library validates responses against type constraints
- Process repeats for nested types (recursively)
-
Result: Fully constructed, type-checked domain value.
Supported Types (100+ stdlib types)
Primitives: bool, i8-i128, u8-u128, f32, f64, char, String
Collections: Vec<T>, Option<T>, Result<T, E>, [T; N]
Network: IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr
Filesystem: PathBuf, Path
Time: Duration, SystemTime, Instant
DateTime: chrono, time, jiff (3 major datetime libraries)
Data: serde_json::Value (dynamic JSON construction)
Smart Pointers: Box<T>, Arc<T>, Rc<T>
...and more
Plus: Any custom type via #[derive(Elicit)]
Advanced Features
Feature Flags
Default: All third-party support enabled by default via the full feature.
[]
# Default: full feature bundle (all third-party support + rand)
= "0.6"
# Minimal build (opt-out of defaults)
= { = "0.6", = false }
# Custom feature selection
= { = "0.6", = false, = [
"chrono", # chrono datetime types
"time", # time datetime types
"jiff", # jiff datetime types
"uuid", # UUID support
"url", # URL support
"regex", # Regex support
"rand", # Random generation
"serde_json", # JSON value elicitation
] }
Available features:
full(default) - All third-party support + randchrono-DateTime<Utc>,NaiveDateTimetime-OffsetDateTimejiff-Timestampuuid-Uuidurl-Urlregex-Regexrand- Random generation (see Random Generation section)serde_json-serde_json::Valueverification- Contract systemverify-kani- Kani formal verificationverify-creusot- Creusot verificationverify-prusti- Prusti verificationcli- CLI toolsdev- All features + CLI
JSON Schema Generation
All elicited types automatically generate JSON schemas for MCP:
use JsonSchema;
// Schema is automatically registered with MCP server
Datetime Support
Three major datetime libraries supported:
// chrono
use ;
let timestamp: = elicit.await?;
// time
use OffsetDateTime;
let time: OffsetDateTime = elicit.await?;
// jiff
use Timestamp;
let jiff_time: Timestamp = elicit.await?;
Dynamic JSON Construction
Agents can build arbitrary JSON structures:
use Value;
// Agent constructs JSON interactively
let json: Value = elicit.await?;
// Could be: {"name": "Alice", "scores": [95, 87, 92]}
Documentation
- API Docs - Complete API reference
- ELICIT_TRAIT_TOOLS_ROUTER.md - Trait-based tool generation guide
- TOOL_ROUTER_WARNINGS.md - Addressing rmcp warnings
- MIGRATION_0.5_to_0.6.md - Upgrade guide
- Examples - 20+ working examples
Why Elicitation?
For Library Authors
Expose your entire domain as agent-native operations:
- One
#[derive(Elicit)]per type → instant MCP tools - Agents construct domain values, not JSON blobs
- Type safety = correctness guarantees
- Composition = reusable building blocks
For Agent Developers
Stop wrestling with JSON forms:
- Structured operations > unstructured key-value
- Type-driven exploration (what's valid?)
- Multi-step processes with clear semantics
- Formal verification catches bugs the LLM can't
For System Architects
Build verified agent systems:
- Contracts express invariants precisely
- Composition rules checked at compile time
- Kani verification gives mathematical confidence
- Zero-cost abstractions = production-ready performance
Comparison: Before vs. After
Traditional MCP (JSON-Centric)
// Server exposes a form
let schema = json!;
// Agent fills it in (one shot, hope for the best)
let response = agent.call_tool;
Problems:
- Agent guesses field names/values
- Validation happens late (after submission)
- No guidance on nested structures
- No type safety, no composition
Elicitation (Type-Centric)
// Agent constructs through typed operations
let task = elicit.await?;
// 1. Provide title (String elicitation)
// 2. Select priority from {Low, Medium, High} ← No typos possible
// 3. Affirm urgency (yes/no)
Benefits:
- Agent guided step-by-step
- Validation built into types
- Errors impossible to construct
- Composable, reusable, verified
Formal Verification
Elicitation's contract system is verified with Kani, Amazon's Rust model checker:
What's verified:
- Contract composition (sequential and parallel)
- Proof forwarding and combination
- Type-level guarantee preservation
- Zero-cost abstraction (proofs compile to nothing)
See VERIFICATION_FRAMEWORK_DESIGN.md for details.
Contributing
We welcome contributions! Areas of interest:
- New stdlib type support - More types = more expressiveness
- Style system extensions - Custom styles for domain-specific contexts
- Verification coverage - More Kani proofs = more confidence
- Documentation - Examples, tutorials, guides
- MCP integration - Better tooling, better DX
See CONTRIBUTING.md for guidelines.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Acknowledgments
Built on:
- rmcp - Rust MCP SDK by Zed Industries
- Kani - Rust model checker by Amazon
- Model Context Protocol - Anthropic's agent communication standard
Special thanks to the Rust community for creating the type system that makes this possible.
Elicitation: Where types meet agents, and agents learn to think in types. 🎯