pmcp/types/
elicitation.rs

1//! User input elicitation support for MCP.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Input elicitation request.
8///
9/// # Examples
10///
11/// ```rust
12/// use pmcp::types::elicitation::{ElicitInputRequest, InputType, InputValidation, SelectOption};
13/// use serde_json::json;
14/// use std::collections::HashMap;
15///
16/// // Text input request
17/// let text_request = ElicitInputRequest {
18///     elicitation_id: "get_username".to_string(),
19///     input_type: InputType::Text,
20///     prompt: "Please enter your username:".to_string(),
21///     description: Some("This will be used to identify you in the system".to_string()),
22///     default: Some(json!("guest")),
23///     validation: Some(InputValidation {
24///         required: true,
25///         pattern: Some(r"^[a-zA-Z0-9_]+$".to_string()),
26///         min: Some(3.0), // minimum length
27///         max: Some(20.0), // maximum length
28///         options: None,
29///         message: None,
30///     }),
31///     metadata: HashMap::new(),
32/// };
33///
34/// // Select input request
35/// let select_request = ElicitInputRequest {
36///     elicitation_id: "choose_theme".to_string(),
37///     input_type: InputType::Select,
38///     prompt: "Choose a theme:".to_string(),
39///     description: None,
40///     default: Some(json!("dark")),
41///     validation: Some(InputValidation {
42///         required: true,
43///         pattern: None,
44///         min: None,
45///         max: None,
46///         options: Some(vec![
47///             SelectOption {
48///                 value: json!("light"),
49///                 label: "Light Theme".to_string(),
50///                 description: None,
51///                 disabled: false,
52///             },
53///             SelectOption {
54///                 value: json!("dark"),
55///                 label: "Dark Theme".to_string(),
56///                 description: None,
57///                 disabled: false,
58///             },
59///             SelectOption {
60///                 value: json!("auto"),
61///                 label: "Auto (System)".to_string(),
62///                 description: None,
63///                 disabled: false,
64///             }
65///         ]),
66///         message: None,
67///     }),
68///     metadata: {
69///         let mut meta = HashMap::new();
70///         meta.insert("category".to_string(), json!("ui"));
71///         meta
72///     },
73/// };
74///
75/// // Number input request
76/// let number_request = ElicitInputRequest {
77///     elicitation_id: "set_timeout".to_string(),
78///     input_type: InputType::Number,
79///     prompt: "Set request timeout (seconds):".to_string(),
80///     description: Some("Timeout for network requests".to_string()),
81///     default: Some(json!(30)),
82///     validation: Some(InputValidation {
83///         required: true,
84///         pattern: None,
85///         min: Some(1.0),
86///         max: Some(300.0),
87///         options: None,
88///         message: None,
89///     }),
90///     metadata: HashMap::new(),
91/// };
92/// ```
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct ElicitInputRequest {
96    /// Unique identifier for this elicitation request.
97    pub elicitation_id: String,
98
99    /// Type of input being requested.
100    pub input_type: InputType,
101
102    /// Prompt message for the user.
103    pub prompt: String,
104
105    /// Additional context or instructions.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub description: Option<String>,
108
109    /// Default value if any.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub default: Option<Value>,
112
113    /// Validation rules for the input.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub validation: Option<InputValidation>,
116
117    /// Additional metadata.
118    #[serde(flatten)]
119    pub metadata: HashMap<String, Value>,
120}
121
122/// Types of input that can be requested.
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub enum InputType {
126    /// Single line text input.
127    Text,
128    /// Multi-line text input.
129    Textarea,
130    /// Boolean yes/no input.
131    Boolean,
132    /// Numeric input.
133    Number,
134    /// Single selection from options.
135    Select,
136    /// Multiple selection from options.
137    MultiSelect,
138    /// File path selection.
139    FilePath,
140    /// Directory path selection.
141    DirectoryPath,
142    /// Password or sensitive input.
143    Password,
144    /// Date input.
145    Date,
146    /// Time input.
147    Time,
148    /// Date and time input.
149    DateTime,
150    /// Color picker.
151    Color,
152    /// URL input.
153    Url,
154    /// Email input.
155    Email,
156}
157
158/// Input validation rules.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct InputValidation {
162    /// Whether the input is required.
163    #[serde(default)]
164    pub required: bool,
165
166    /// Minimum value (for numbers) or length (for strings).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub min: Option<f64>,
169
170    /// Maximum value (for numbers) or length (for strings).
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub max: Option<f64>,
173
174    /// Regular expression pattern for validation.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub pattern: Option<String>,
177
178    /// List of allowed values (for select/multiselect).
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub options: Option<Vec<SelectOption>>,
181
182    /// Custom validation message.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub message: Option<String>,
185}
186
187/// Option for select/multiselect inputs.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct SelectOption {
191    /// Option value.
192    pub value: Value,
193
194    /// Display label.
195    pub label: String,
196
197    /// Option description.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub description: Option<String>,
200
201    /// Whether this option is disabled.
202    #[serde(default)]
203    pub disabled: bool,
204}
205
206/// Response to an input elicitation request.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct ElicitInputResponse {
210    /// The elicitation ID this response is for.
211    pub elicitation_id: String,
212
213    /// The user's input value.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub value: Option<Value>,
216
217    /// Whether the user cancelled the input.
218    #[serde(default)]
219    pub cancelled: bool,
220
221    /// Error message if validation failed.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub error: Option<String>,
224}
225
226/// Builder for creating input elicitation requests.
227#[derive(Debug)]
228pub struct ElicitInputBuilder {
229    elicitation_id: String,
230    input_type: InputType,
231    prompt: String,
232    description: Option<String>,
233    default: Option<Value>,
234    validation: Option<InputValidation>,
235    metadata: HashMap<String, Value>,
236}
237
238impl ElicitInputBuilder {
239    /// Create a new elicitation builder.
240    pub fn new(input_type: InputType, prompt: impl Into<String>) -> Self {
241        Self {
242            elicitation_id: uuid::Uuid::new_v4().to_string(),
243            input_type,
244            prompt: prompt.into(),
245            description: None,
246            default: None,
247            validation: None,
248            metadata: HashMap::new(),
249        }
250    }
251
252    /// Set a custom elicitation ID.
253    pub fn id(mut self, id: impl Into<String>) -> Self {
254        self.elicitation_id = id.into();
255        self
256    }
257
258    /// Set the description.
259    pub fn description(mut self, desc: impl Into<String>) -> Self {
260        self.description = Some(desc.into());
261        self
262    }
263
264    /// Set the default value.
265    pub fn default(mut self, value: impl Into<Value>) -> Self {
266        self.default = Some(value.into());
267        self
268    }
269
270    /// Mark as required.
271    pub fn required(mut self) -> Self {
272        if self.validation.is_none() {
273            self.validation = Some(InputValidation {
274                required: true,
275                min: None,
276                max: None,
277                pattern: None,
278                options: None,
279                message: None,
280            });
281        } else if let Some(validation) = &mut self.validation {
282            validation.required = true;
283        }
284        self
285    }
286
287    /// Set minimum value or length.
288    pub fn min(mut self, min: f64) -> Self {
289        if self.validation.is_none() {
290            self.validation = Some(InputValidation {
291                required: false,
292                min: Some(min),
293                max: None,
294                pattern: None,
295                options: None,
296                message: None,
297            });
298        } else if let Some(validation) = &mut self.validation {
299            validation.min = Some(min);
300        }
301        self
302    }
303
304    /// Set maximum value or length.
305    pub fn max(mut self, max: f64) -> Self {
306        if self.validation.is_none() {
307            self.validation = Some(InputValidation {
308                required: false,
309                min: None,
310                max: Some(max),
311                pattern: None,
312                options: None,
313                message: None,
314            });
315        } else if let Some(validation) = &mut self.validation {
316            validation.max = Some(max);
317        }
318        self
319    }
320
321    /// Set validation pattern.
322    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
323        if self.validation.is_none() {
324            self.validation = Some(InputValidation {
325                required: false,
326                min: None,
327                max: None,
328                pattern: Some(pattern.into()),
329                options: None,
330                message: None,
331            });
332        } else if let Some(validation) = &mut self.validation {
333            validation.pattern = Some(pattern.into());
334        }
335        self
336    }
337
338    /// Set options for select/multiselect.
339    pub fn options(mut self, options: Vec<SelectOption>) -> Self {
340        if self.validation.is_none() {
341            self.validation = Some(InputValidation {
342                required: false,
343                min: None,
344                max: None,
345                pattern: None,
346                options: Some(options),
347                message: None,
348            });
349        } else if let Some(validation) = &mut self.validation {
350            validation.options = Some(options);
351        }
352        self
353    }
354
355    /// Add metadata.
356    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
357        self.metadata.insert(key.into(), value.into());
358        self
359    }
360
361    /// Build the elicitation request.
362    pub fn build(self) -> ElicitInputRequest {
363        ElicitInputRequest {
364            elicitation_id: self.elicitation_id,
365            input_type: self.input_type,
366            prompt: self.prompt,
367            description: self.description,
368            default: self.default,
369            validation: self.validation,
370            metadata: self.metadata,
371        }
372    }
373}
374
375/// Helper function to create a text input elicitation.
376pub fn elicit_text(prompt: impl Into<String>) -> ElicitInputBuilder {
377    ElicitInputBuilder::new(InputType::Text, prompt)
378}
379
380/// Helper function to create a boolean input elicitation.
381pub fn elicit_boolean(prompt: impl Into<String>) -> ElicitInputBuilder {
382    ElicitInputBuilder::new(InputType::Boolean, prompt)
383}
384
385/// Helper function to create a select input elicitation.
386pub fn elicit_select(prompt: impl Into<String>, options: Vec<SelectOption>) -> ElicitInputBuilder {
387    ElicitInputBuilder::new(InputType::Select, prompt).options(options)
388}
389
390/// Helper function to create a number input elicitation.
391pub fn elicit_number(prompt: impl Into<String>) -> ElicitInputBuilder {
392    ElicitInputBuilder::new(InputType::Number, prompt)
393}
394
395/// Helper function to create a file path input elicitation.
396pub fn elicit_file(prompt: impl Into<String>) -> ElicitInputBuilder {
397    ElicitInputBuilder::new(InputType::FilePath, prompt)
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use serde_json::json;
404
405    #[test]
406    fn test_elicit_text() {
407        let request = elicit_text("Enter your name")
408            .description("Please provide your full name")
409            .required()
410            .min(2.0)
411            .max(100.0)
412            .build();
413
414        assert_eq!(request.input_type, InputType::Text);
415        assert_eq!(request.prompt, "Enter your name");
416        assert_eq!(
417            request.description,
418            Some("Please provide your full name".to_string())
419        );
420        assert!(request.validation.as_ref().unwrap().required);
421        assert_eq!(request.validation.as_ref().unwrap().min, Some(2.0));
422        assert_eq!(request.validation.as_ref().unwrap().max, Some(100.0));
423    }
424
425    #[test]
426    fn test_elicit_select() {
427        let options = vec![
428            SelectOption {
429                value: json!("small"),
430                label: "Small".to_string(),
431                description: Some("Suitable for personal use".to_string()),
432                disabled: false,
433            },
434            SelectOption {
435                value: json!("medium"),
436                label: "Medium".to_string(),
437                description: Some("Good for small teams".to_string()),
438                disabled: false,
439            },
440            SelectOption {
441                value: json!("large"),
442                label: "Large".to_string(),
443                description: Some("For enterprise use".to_string()),
444                disabled: false,
445            },
446        ];
447
448        let request = elicit_select("Choose a size", options.clone())
449            .default(json!("medium"))
450            .build();
451
452        assert_eq!(request.input_type, InputType::Select);
453        assert_eq!(request.prompt, "Choose a size");
454        assert_eq!(request.default, Some(json!("medium")));
455        assert_eq!(
456            request
457                .validation
458                .as_ref()
459                .unwrap()
460                .options
461                .as_ref()
462                .unwrap()
463                .len(),
464            3
465        );
466    }
467
468    #[test]
469    fn test_serialization() {
470        let request = elicit_boolean("Enable feature?")
471            .default(json!(true))
472            .description("This will enable the experimental feature")
473            .build();
474
475        let json = serde_json::to_value(&request).unwrap();
476        assert_eq!(json["inputType"], "boolean");
477        assert_eq!(json["prompt"], "Enable feature?");
478        assert_eq!(json["default"], true);
479        assert_eq!(
480            json["description"],
481            "This will enable the experimental feature"
482        );
483
484        // Test deserialization
485        let deserialized: ElicitInputRequest = serde_json::from_value(json).unwrap();
486        assert_eq!(deserialized.prompt, request.prompt);
487    }
488}