turbomcp_protocol/types/
elicitation.rs

1//! User input elicitation types
2//!
3//! This module contains types for server-initiated user input requests:
4//! - **Form Mode** (MCP 2025-11-25): In-band structured data collection
5//! - **URL Mode** (MCP 2025-11-25 draft, SEP-1036): Out-of-band sensitive data collection
6//!
7//! ## Form Mode vs URL Mode
8//!
9//! | Aspect | Form Mode | URL Mode |
10//! |--------|-----------|----------|
11//! | **Data Flow** | In-band (through MCP) | Out-of-band (external URL) |
12//! | **Use Case** | Non-sensitive structured data | Sensitive data, OAuth, credentials |
13//! | **Security** | Data visible to MCP client | Data **NOT** visible to MCP client |
14
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18/// Elicitation action taken by user
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
20#[serde(rename_all = "camelCase")]
21pub enum ElicitationAction {
22    /// User submitted the form/confirmed the action
23    Accept,
24    /// User explicitly declined the action
25    Decline,
26    /// User dismissed without making an explicit choice
27    Cancel,
28}
29
30/// Schema for elicitation input validation
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ElicitationSchema {
33    /// Schema type (must be "object", required by MCP spec)
34    #[serde(rename = "type")]
35    pub schema_type: String,
36    /// Schema properties (required by MCP spec)
37    pub properties: HashMap<String, PrimitiveSchemaDefinition>,
38    /// Required properties
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub required: Option<Vec<String>>,
41    /// Additional properties allowed
42    #[serde(
43        rename = "additionalProperties",
44        skip_serializing_if = "Option::is_none"
45    )]
46    pub additional_properties: Option<bool>,
47}
48
49impl ElicitationSchema {
50    /// Create a new elicitation schema
51    pub fn new() -> Self {
52        Self {
53            schema_type: "object".to_string(),
54            properties: HashMap::new(),
55            required: Some(Vec::new()),
56            additional_properties: Some(false),
57        }
58    }
59
60    /// Add a string property to the schema
61    pub fn add_string_property(
62        mut self,
63        name: String,
64        required: bool,
65        description: Option<String>,
66    ) -> Self {
67        let property = PrimitiveSchemaDefinition::String {
68            title: None,
69            description,
70            format: None,
71            min_length: None,
72            max_length: None,
73            enum_values: None,
74            enum_names: None,
75        };
76
77        self.properties.insert(name.clone(), property);
78
79        if required && let Some(ref mut required_fields) = self.required {
80            required_fields.push(name);
81        }
82
83        self
84    }
85
86    /// Add a number property to the schema
87    pub fn add_number_property(
88        mut self,
89        name: String,
90        required: bool,
91        description: Option<String>,
92        minimum: Option<f64>,
93        maximum: Option<f64>,
94    ) -> Self {
95        let property = PrimitiveSchemaDefinition::Number {
96            title: None,
97            description,
98            minimum,
99            maximum,
100        };
101
102        self.properties.insert(name.clone(), property);
103
104        if required && let Some(ref mut required_fields) = self.required {
105            required_fields.push(name);
106        }
107
108        self
109    }
110
111    /// Add a boolean property to the schema
112    pub fn add_boolean_property(
113        mut self,
114        name: String,
115        required: bool,
116        description: Option<String>,
117        default: Option<bool>,
118    ) -> Self {
119        let property = PrimitiveSchemaDefinition::Boolean {
120            title: None,
121            description,
122            default,
123        };
124
125        self.properties.insert(name.clone(), property);
126
127        if required && let Some(ref mut required_fields) = self.required {
128            required_fields.push(name);
129        }
130
131        self
132    }
133}
134
135impl Default for ElicitationSchema {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141/// Primitive schema definition for elicitation fields
142///
143/// ## Version Support
144/// - MCP 2025-11-25: String (with legacy enumNames), Number, Integer, Boolean
145/// - MCP 2025-11-25 draft (SEP-1330): Use `EnumSchema` for standards-compliant enum handling
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(tag = "type")]
148pub enum PrimitiveSchemaDefinition {
149    /// String field schema definition
150    ///
151    /// **Note**: For enum fields, prefer using `EnumSchema` (MCP 2025-11-25 draft)
152    /// over the legacy `enum_values`/`enum_names` pattern for standards compliance.
153    #[serde(rename = "string")]
154    String {
155        /// Field title
156        #[serde(skip_serializing_if = "Option::is_none")]
157        title: Option<String>,
158        /// Field description
159        #[serde(skip_serializing_if = "Option::is_none")]
160        description: Option<String>,
161        /// String format (email, uri, date, date-time, etc.)
162        #[serde(skip_serializing_if = "Option::is_none")]
163        format: Option<String>,
164        /// Minimum string length
165        #[serde(skip_serializing_if = "Option::is_none")]
166        #[serde(rename = "minLength")]
167        min_length: Option<u32>,
168        /// Maximum string length
169        #[serde(skip_serializing_if = "Option::is_none")]
170        #[serde(rename = "maxLength")]
171        max_length: Option<u32>,
172        /// Allowed enum values
173        ///
174        /// **Legacy**: Use `EnumSchema::UntitledSingleSelect` instead (MCP 2025-11-25)
175        #[serde(skip_serializing_if = "Option::is_none")]
176        #[serde(rename = "enum")]
177        enum_values: Option<Vec<String>>,
178        /// Display names for enum values
179        ///
180        /// **Deprecated**: This is non-standard JSON Schema. Use `EnumSchema::TitledSingleSelect`
181        /// with `oneOf` pattern instead (MCP 2025-11-25 draft, SEP-1330).
182        #[serde(skip_serializing_if = "Option::is_none")]
183        #[serde(rename = "enumNames")]
184        enum_names: Option<Vec<String>>,
185    },
186    /// Number field schema definition
187    #[serde(rename = "number")]
188    Number {
189        /// Field title
190        #[serde(skip_serializing_if = "Option::is_none")]
191        title: Option<String>,
192        /// Field description
193        #[serde(skip_serializing_if = "Option::is_none")]
194        description: Option<String>,
195        /// Minimum value
196        #[serde(skip_serializing_if = "Option::is_none")]
197        minimum: Option<f64>,
198        /// Maximum value
199        #[serde(skip_serializing_if = "Option::is_none")]
200        maximum: Option<f64>,
201    },
202    /// Integer field schema definition
203    #[serde(rename = "integer")]
204    Integer {
205        /// Field title
206        #[serde(skip_serializing_if = "Option::is_none")]
207        title: Option<String>,
208        /// Field description
209        #[serde(skip_serializing_if = "Option::is_none")]
210        description: Option<String>,
211        /// Minimum value
212        #[serde(skip_serializing_if = "Option::is_none")]
213        minimum: Option<i64>,
214        /// Maximum value
215        #[serde(skip_serializing_if = "Option::is_none")]
216        maximum: Option<i64>,
217    },
218    /// Boolean field schema definition
219    #[serde(rename = "boolean")]
220    Boolean {
221        /// Field title
222        #[serde(skip_serializing_if = "Option::is_none")]
223        title: Option<String>,
224        /// Field description
225        #[serde(skip_serializing_if = "Option::is_none")]
226        description: Option<String>,
227        /// Default value
228        #[serde(skip_serializing_if = "Option::is_none")]
229        default: Option<bool>,
230    },
231}
232
233// ========== MCP 2025-11-25 Draft: Standards-Based Enum Schemas (SEP-1330) ==========
234
235/// Enum option with value and display title (JSON Schema 2020-12 compliant)
236///
237/// Used with `oneOf` (single-select) or `anyOf` (multi-select) patterns
238/// to provide human-readable labels for enum values.
239///
240/// ## Example
241/// ```json
242/// { "const": "#FF0000", "title": "Red" }
243/// ```
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245#[cfg(feature = "mcp-enum-improvements")]
246pub struct EnumOption {
247    /// The enum value (must match one of the allowed values)
248    #[serde(rename = "const")]
249    pub const_value: String,
250    /// Human-readable display name for this option
251    pub title: String,
252}
253
254/// Single-select enum schema with display titles (JSON Schema 2020-12 compliant)
255///
256/// Replaces the legacy `enum + enumNames` pattern with standards-compliant `oneOf` + `const`.
257///
258/// ## Example
259/// ```json
260/// {
261///   "type": "string",
262///   "title": "Color Selection",
263///   "oneOf": [
264///     { "const": "#FF0000", "title": "Red" },
265///     { "const": "#00FF00", "title": "Green" }
266///   ],
267///   "default": "#FF0000"
268/// }
269/// ```
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[cfg(feature = "mcp-enum-improvements")]
272pub struct TitledSingleSelectEnumSchema {
273    /// Schema type (must be "string")
274    #[serde(rename = "type")]
275    pub schema_type: String,
276    /// Array of enum options with const/title pairs (JSON Schema 2020-12)
277    #[serde(rename = "oneOf")]
278    pub one_of: Vec<EnumOption>,
279    /// Optional schema title
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub title: Option<String>,
282    /// Optional schema description
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub description: Option<String>,
285    /// Optional default value (must match one of the const values)
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub default: Option<String>,
288}
289
290/// Single-select enum schema without display titles (standard JSON Schema)
291///
292/// Simple enum pattern without custom titles - uses enum values as labels.
293///
294/// ## Example
295/// ```json
296/// {
297///   "type": "string",
298///   "enum": ["red", "green", "blue"],
299///   "default": "red"
300/// }
301/// ```
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[cfg(feature = "mcp-enum-improvements")]
304pub struct UntitledSingleSelectEnumSchema {
305    /// Schema type (must be "string")
306    #[serde(rename = "type")]
307    pub schema_type: String,
308    /// Array of allowed string values (standard JSON Schema)
309    #[serde(rename = "enum")]
310    pub enum_values: Vec<String>,
311    /// Optional schema title
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub title: Option<String>,
314    /// Optional schema description
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub description: Option<String>,
317    /// Optional default value (must match one of the enum values)
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub default: Option<String>,
320}
321
322/// Multi-select enum schema with display titles (JSON Schema 2020-12 compliant)
323///
324/// Allows selecting multiple values from an enumeration with human-readable labels.
325/// Uses `anyOf` pattern for array items.
326///
327/// ## Example
328/// ```json
329/// {
330///   "type": "array",
331///   "title": "Color Selection",
332///   "minItems": 1,
333///   "maxItems": 2,
334///   "items": {
335///     "anyOf": [
336///       { "const": "#FF0000", "title": "Red" },
337///       { "const": "#00FF00", "title": "Green" }
338///     ]
339///   },
340///   "default": ["#FF0000"]
341/// }
342/// ```
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[cfg(feature = "mcp-enum-improvements")]
345pub struct TitledMultiSelectEnumSchema {
346    /// Schema type (must be "array")
347    #[serde(rename = "type")]
348    pub schema_type: String,
349    /// Minimum number of selections required
350    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
351    pub min_items: Option<u32>,
352    /// Maximum number of selections allowed
353    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
354    pub max_items: Option<u32>,
355    /// Array item schema with anyOf enum options
356    pub items: MultiSelectItems,
357    /// Optional schema title
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub title: Option<String>,
360    /// Optional schema description
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub description: Option<String>,
363    /// Optional default value (array of selected values)
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub default: Option<Vec<String>>,
366}
367
368/// Multi-select enum schema without display titles (standard JSON Schema)
369///
370/// Simple multi-select pattern using enum array.
371///
372/// ## Example
373/// ```json
374/// {
375///   "type": "array",
376///   "items": {
377///     "type": "string",
378///     "enum": ["red", "green", "blue"]
379///   },
380///   "default": ["red", "green"]
381/// }
382/// ```
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[cfg(feature = "mcp-enum-improvements")]
385pub struct UntitledMultiSelectEnumSchema {
386    /// Schema type (must be "array")
387    #[serde(rename = "type")]
388    pub schema_type: String,
389    /// Minimum number of selections required
390    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
391    pub min_items: Option<u32>,
392    /// Maximum number of selections allowed
393    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
394    pub max_items: Option<u32>,
395    /// Array item schema with simple enum
396    pub items: UntitledMultiSelectItems,
397    /// Optional schema title
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub title: Option<String>,
400    /// Optional schema description
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub description: Option<String>,
403    /// Optional default value (array of selected values)
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub default: Option<Vec<String>>,
406}
407
408/// Array items schema for titled multi-select (using anyOf)
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[cfg(feature = "mcp-enum-improvements")]
411pub struct MultiSelectItems {
412    /// Array of enum options with const/title pairs (JSON Schema 2020-12)
413    #[serde(rename = "anyOf")]
414    pub any_of: Vec<EnumOption>,
415}
416
417/// Array items schema for untitled multi-select (using simple enum)
418#[derive(Debug, Clone, Serialize, Deserialize)]
419#[cfg(feature = "mcp-enum-improvements")]
420pub struct UntitledMultiSelectItems {
421    /// Item type (must be "string")
422    #[serde(rename = "type")]
423    pub schema_type: String,
424    /// Array of allowed string values
425    #[serde(rename = "enum")]
426    pub enum_values: Vec<String>,
427}
428
429/// Legacy enum schema with enumNames (deprecated, non-standard)
430///
431/// **Deprecated**: This uses the non-standard `enumNames` keyword which is not part of
432/// JSON Schema 2020-12. Use `TitledSingleSelectEnumSchema` with `oneOf` pattern instead.
433///
434/// This type is maintained for backward compatibility only and will be removed
435/// in a future version of the MCP specification.
436///
437/// ## Example
438/// ```json
439/// {
440///   "type": "string",
441///   "enum": ["#FF0000", "#00FF00"],
442///   "enumNames": ["Red", "Green"]
443/// }
444/// ```
445#[derive(Debug, Clone, Serialize, Deserialize)]
446#[cfg(feature = "mcp-enum-improvements")]
447pub struct LegacyTitledEnumSchema {
448    /// Schema type (must be "string")
449    #[serde(rename = "type")]
450    pub schema_type: String,
451    /// Array of allowed values
452    #[serde(rename = "enum")]
453    pub enum_values: Vec<String>,
454    /// Display names for enum values (non-standard, deprecated)
455    #[serde(rename = "enumNames")]
456    pub enum_names: Vec<String>,
457    /// Optional schema title
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub title: Option<String>,
460    /// Optional schema description
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub description: Option<String>,
463    /// Optional default value
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub default: Option<String>,
466}
467
468/// Standards-based enum schema (MCP 2025-11-25 draft, SEP-1330)
469///
470/// Replaces non-standard `enumNames` pattern with JSON Schema 2020-12 compliant
471/// `oneOf`/`const` (single-select) and `anyOf` (multi-select) patterns.
472///
473/// ## Variants
474///
475/// - **TitledSingleSelect**: Single-select with display titles (oneOf + const)
476/// - **UntitledSingleSelect**: Single-select without titles (simple enum)
477/// - **TitledMultiSelect**: Multi-select with display titles (array + anyOf)
478/// - **UntitledMultiSelect**: Multi-select without titles (array + enum)
479/// - **LegacyTitled**: Backward compatibility (enum + enumNames, deprecated)
480///
481/// ## Standards Compliance
482///
483/// The new patterns use only standard JSON Schema 2020-12 keywords:
484/// - `oneOf` with `const` and `title` for discriminated unions
485/// - `anyOf` for array items with multiple allowed types
486/// - `enum` for simple value restrictions
487///
488/// The legacy `enumNames` keyword was never part of any JSON Schema specification
489/// and has been replaced with standards-compliant patterns.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491#[serde(untagged)]
492#[cfg(feature = "mcp-enum-improvements")]
493pub enum EnumSchema {
494    /// Single-select enum with display titles (oneOf pattern)
495    TitledSingleSelect(TitledSingleSelectEnumSchema),
496    /// Single-select enum without titles (simple enum)
497    UntitledSingleSelect(UntitledSingleSelectEnumSchema),
498    /// Multi-select enum with display titles (array + anyOf)
499    TitledMultiSelect(TitledMultiSelectEnumSchema),
500    /// Multi-select enum without titles (array + enum)
501    UntitledMultiSelect(UntitledMultiSelectEnumSchema),
502    /// Legacy enum with enumNames (deprecated, for backward compatibility)
503    LegacyTitled(LegacyTitledEnumSchema),
504}
505
506/// Elicit request mode (MCP 2025-11-25 draft, SEP-1036)
507#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
508#[serde(rename_all = "lowercase")]
509#[cfg(feature = "mcp-url-elicitation")]
510pub enum ElicitMode {
511    /// Form mode: In-band structured data collection (MCP 2025-11-25)
512    Form,
513    /// URL mode: Out-of-band sensitive data collection (MCP 2025-11-25 draft)
514    Url,
515}
516
517/// URL mode elicitation parameters (MCP 2025-11-25 draft, SEP-1036)
518///
519/// Used for out-of-band interactions where sensitive information must not
520/// pass through the MCP client (e.g., OAuth flows, API keys, credentials).
521#[derive(Debug, Clone, Serialize, Deserialize)]
522#[cfg(feature = "mcp-url-elicitation")]
523pub struct URLElicitRequestParams {
524    /// Elicitation mode (must be "url")
525    pub mode: ElicitMode,
526
527    /// Unique identifier for this elicitation
528    #[serde(rename = "elicitationId")]
529    pub elicitation_id: String,
530
531    /// Human-readable message explaining why the interaction is needed
532    pub message: String,
533
534    /// URL user should navigate to (MUST NOT contain sensitive information)
535    #[serde(with = "url_serde")]
536    pub url: url::Url,
537}
538
539// Custom serde for url::Url to serialize as string
540#[cfg(feature = "mcp-url-elicitation")]
541mod url_serde {
542    use serde::{Deserialize, Deserializer, Serializer};
543    use url::Url;
544
545    pub(super) fn serialize<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
546    where
547        S: Serializer,
548    {
549        serializer.serialize_str(url.as_str())
550    }
551
552    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Url, D::Error>
553    where
554        D: Deserializer<'de>,
555    {
556        let s = String::deserialize(deserializer)?;
557        Url::parse(&s).map_err(serde::de::Error::custom)
558    }
559}
560
561/// Form mode elicitation parameters (MCP 2025-11-25)
562///
563/// Used for in-band structured data collection with JSON schema validation.
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct FormElicitRequestParams {
566    /// Human-readable message for the user
567    pub message: String,
568
569    /// Schema for input validation (per MCP specification)
570    #[serde(rename = "requestedSchema")]
571    pub schema: ElicitationSchema,
572
573    /// Optional timeout in milliseconds
574    #[serde(rename = "timeoutMs", skip_serializing_if = "Option::is_none")]
575    pub timeout_ms: Option<u32>,
576
577    /// Whether the request can be cancelled
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub cancellable: Option<bool>,
580}
581
582/// Elicit request parameters (supports both form and URL modes)
583#[derive(Debug, Clone, Serialize, Deserialize)]
584#[serde(untagged)]
585pub enum ElicitRequestParams {
586    /// Form mode: In-band structured data collection
587    #[cfg(not(feature = "mcp-url-elicitation"))]
588    Form(FormElicitRequestParams),
589
590    /// Form mode: In-band structured data collection
591    #[cfg(feature = "mcp-url-elicitation")]
592    Form(FormElicitRequestParams),
593
594    /// URL mode: Out-of-band sensitive data collection (MCP 2025-11-25 draft)
595    #[cfg(feature = "mcp-url-elicitation")]
596    Url(URLElicitRequestParams),
597}
598
599impl ElicitRequestParams {
600    /// Create a new form mode elicitation request
601    pub fn form(
602        message: String,
603        schema: ElicitationSchema,
604        timeout_ms: Option<u32>,
605        cancellable: Option<bool>,
606    ) -> Self {
607        ElicitRequestParams::Form(FormElicitRequestParams {
608            message,
609            schema,
610            timeout_ms,
611            cancellable,
612        })
613    }
614
615    /// Create a new URL mode elicitation request (requires mcp-url-elicitation feature)
616    #[cfg(feature = "mcp-url-elicitation")]
617    pub fn url(elicitation_id: String, message: String, url: url::Url) -> Self {
618        ElicitRequestParams::Url(URLElicitRequestParams {
619            mode: ElicitMode::Url,
620            elicitation_id,
621            message,
622            url,
623        })
624    }
625
626    /// Get the message for this elicitation
627    pub fn message(&self) -> &str {
628        match self {
629            ElicitRequestParams::Form(form) => &form.message,
630            #[cfg(feature = "mcp-url-elicitation")]
631            ElicitRequestParams::Url(url_params) => &url_params.message,
632        }
633    }
634}
635
636/// Elicit request wrapper
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct ElicitRequest {
639    /// Elicitation parameters
640    #[serde(flatten)]
641    pub params: ElicitRequestParams,
642    /// Task metadata for task-augmented elicitation (MCP 2025-11-25 draft, SEP-1686)
643    ///
644    /// When present, indicates the server should execute this elicitation request as a long-running
645    /// task and return a CreateTaskResult instead of the immediate ElicitResult.
646    /// The actual result can be retrieved later via tasks/result.
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub task: Option<crate::types::tasks::TaskMetadata>,
649    /// Optional metadata per MCP 2025-11-25 specification
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub _meta: Option<serde_json::Value>,
652}
653
654impl Default for ElicitRequest {
655    fn default() -> Self {
656        Self {
657            params: ElicitRequestParams::form(String::new(), ElicitationSchema::new(), None, None),
658            task: None,
659            _meta: None,
660        }
661    }
662}
663
664/// Elicit result
665///
666/// ## Version Support
667/// - MCP 2025-11-25: action, content (form mode), _meta
668/// - MCP 2025-11-25 draft (SEP-1330): Clarified content field behavior
669///
670/// ## Content Field Behavior (SEP-1330 Clarification)
671///
672/// The `content` field is **only present** when:
673/// 1. `action` is `"accept"` (user submitted the form), AND
674/// 2. Mode was `"form"` (in-band structured data collection)
675///
676/// The `content` field is **omitted** when:
677/// - Action is `"decline"` or `"cancel"`
678/// - Mode was `"url"` (out-of-band, data doesn't transit through MCP)
679///
680/// ## Example
681///
682/// **Form mode with accept:**
683/// ```json
684/// {
685///   "action": "accept",
686///   "content": {
687///     "name": "Alice",
688///     "email": "alice@example.com"
689///   }
690/// }
691/// ```
692///
693/// **URL mode with accept:**
694/// ```json
695/// {
696///   "action": "accept"
697/// }
698/// ```
699/// Note: No `content` field - data was collected out-of-band
700#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct ElicitResult {
702    /// The user action in response to the elicitation
703    ///
704    /// - `accept`: User submitted the form/confirmed the action
705    /// - `decline`: User explicitly declined the action
706    /// - `cancel`: User dismissed without making an explicit choice
707    pub action: ElicitationAction,
708
709    /// The submitted form data, only present when action is "accept"
710    /// and mode was "form".
711    ///
712    /// Contains values matching the requested schema. Omitted for:
713    /// - `action` is `"decline"` or `"cancel"`
714    /// - URL mode responses (out-of-band data collection)
715    ///
716    /// Per MCP 2025-11-25 draft (SEP-1330), this clarification ensures
717    /// clients understand when to expect form data vs. out-of-band completion.
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
720
721    /// Optional metadata per MCP 2025-11-25 specification
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub _meta: Option<serde_json::Value>,
724}
725
726/// Elicitation completion notification parameters (MCP 2025-11-25 draft, SEP-1036)
727///
728/// Sent by the server to indicate that an out-of-band elicitation has been completed.
729/// This allows the client to know when to retry the original request.
730#[derive(Debug, Clone, Serialize, Deserialize)]
731#[cfg(feature = "mcp-url-elicitation")]
732pub struct ElicitationCompleteParams {
733    /// The ID of the completed elicitation
734    #[serde(rename = "elicitationId")]
735    pub elicitation_id: String,
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    #[test]
743    fn test_elicitation_action_serialization() {
744        assert_eq!(
745            serde_json::to_string(&ElicitationAction::Accept).unwrap(),
746            "\"accept\""
747        );
748        assert_eq!(
749            serde_json::to_string(&ElicitationAction::Decline).unwrap(),
750            "\"decline\""
751        );
752        assert_eq!(
753            serde_json::to_string(&ElicitationAction::Cancel).unwrap(),
754            "\"cancel\""
755        );
756    }
757
758    #[test]
759    fn test_form_elicit_params() {
760        let schema = ElicitationSchema::new().add_string_property(
761            "name".to_string(),
762            true,
763            Some("User name".to_string()),
764        );
765
766        let params = ElicitRequestParams::form(
767            "Please provide your name".to_string(),
768            schema,
769            Some(30000),
770            Some(true),
771        );
772
773        assert_eq!(params.message(), "Please provide your name");
774
775        // Test serialization
776        let json = serde_json::to_string(&params).unwrap();
777        assert!(json.contains("Please provide your name"));
778        assert!(json.contains("requestedSchema"));
779    }
780
781    #[test]
782    #[cfg(feature = "mcp-url-elicitation")]
783    fn test_url_elicit_params() {
784        use url::Url;
785
786        let url = Url::parse("https://example.com/oauth/authorize").unwrap();
787        let params = ElicitRequestParams::url(
788            "test-id-123".to_string(),
789            "Please authorize the connection".to_string(),
790            url,
791        );
792
793        assert_eq!(params.message(), "Please authorize the connection");
794
795        // Test serialization
796        let json = serde_json::to_string(&params).unwrap();
797        assert!(json.contains("test-id-123"));
798        assert!(json.contains("https://example.com/oauth/authorize"));
799        assert!(json.contains("\"mode\":\"url\""));
800    }
801
802    #[test]
803    #[cfg(feature = "mcp-url-elicitation")]
804    fn test_elicit_mode_serialization() {
805        assert_eq!(
806            serde_json::to_string(&ElicitMode::Form).unwrap(),
807            "\"form\""
808        );
809        assert_eq!(serde_json::to_string(&ElicitMode::Url).unwrap(), "\"url\"");
810    }
811
812    #[test]
813    #[cfg(feature = "mcp-url-elicitation")]
814    fn test_completion_notification() {
815        let params = ElicitationCompleteParams {
816            elicitation_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
817        };
818
819        let json = serde_json::to_string(&params).unwrap();
820        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
821        assert!(json.contains("elicitationId"));
822    }
823
824    #[test]
825    fn test_elicit_result_form_mode() {
826        let mut content = std::collections::HashMap::new();
827        content.insert("name".to_string(), serde_json::json!("Alice"));
828
829        let result = ElicitResult {
830            action: ElicitationAction::Accept,
831            content: Some(content),
832            _meta: None,
833        };
834
835        let json = serde_json::to_string(&result).unwrap();
836        assert!(json.contains("\"action\":\"accept\""));
837        assert!(json.contains("\"name\":\"Alice\""));
838    }
839
840    #[test]
841    fn test_elicit_result_url_mode() {
842        // URL mode: no content field
843        let result = ElicitResult {
844            action: ElicitationAction::Accept,
845            content: None,
846            _meta: None,
847        };
848
849        let json = serde_json::to_string(&result).unwrap();
850        assert!(json.contains("\"action\":\"accept\""));
851        assert!(!json.contains("content"));
852    }
853
854    // ========== SEP-1330 Enum Schema Tests ==========
855
856    #[test]
857    #[cfg(feature = "mcp-enum-improvements")]
858    fn test_titled_single_select_enum_schema() {
859        use super::{EnumOption, TitledSingleSelectEnumSchema};
860
861        let schema = TitledSingleSelectEnumSchema {
862            schema_type: "string".to_string(),
863            one_of: vec![
864                EnumOption {
865                    const_value: "#FF0000".to_string(),
866                    title: "Red".to_string(),
867                },
868                EnumOption {
869                    const_value: "#00FF00".to_string(),
870                    title: "Green".to_string(),
871                },
872                EnumOption {
873                    const_value: "#0000FF".to_string(),
874                    title: "Blue".to_string(),
875                },
876            ],
877            title: Some("Color Selection".to_string()),
878            description: Some("Choose your favorite color".to_string()),
879            default: Some("#FF0000".to_string()),
880        };
881
882        let json = serde_json::to_string(&schema).unwrap();
883        assert!(json.contains("\"type\":\"string\""));
884        assert!(json.contains("\"oneOf\""));
885        assert!(json.contains("\"const\":\"#FF0000\""));
886        assert!(json.contains("\"title\":\"Red\""));
887        assert!(json.contains("\"default\":\"#FF0000\""));
888
889        // Verify deserialization
890        let deserialized: TitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
891        assert_eq!(deserialized.one_of.len(), 3);
892        assert_eq!(deserialized.one_of[0].const_value, "#FF0000");
893        assert_eq!(deserialized.one_of[0].title, "Red");
894    }
895
896    #[test]
897    #[cfg(feature = "mcp-enum-improvements")]
898    fn test_untitled_single_select_enum_schema() {
899        use super::UntitledSingleSelectEnumSchema;
900
901        let schema = UntitledSingleSelectEnumSchema {
902            schema_type: "string".to_string(),
903            enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
904            title: Some("Color Selection".to_string()),
905            description: Some("Choose a color".to_string()),
906            default: Some("red".to_string()),
907        };
908
909        let json = serde_json::to_string(&schema).unwrap();
910        assert!(json.contains("\"type\":\"string\""));
911        assert!(json.contains("\"enum\""));
912        assert!(json.contains("\"red\""));
913        assert!(json.contains("\"green\""));
914        assert!(json.contains("\"blue\""));
915        assert!(!json.contains("oneOf"));
916
917        // Verify deserialization
918        let deserialized: UntitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
919        assert_eq!(deserialized.enum_values.len(), 3);
920        assert_eq!(deserialized.enum_values[0], "red");
921    }
922
923    #[test]
924    #[cfg(feature = "mcp-enum-improvements")]
925    fn test_titled_multi_select_enum_schema() {
926        use super::{EnumOption, MultiSelectItems, TitledMultiSelectEnumSchema};
927
928        let schema = TitledMultiSelectEnumSchema {
929            schema_type: "array".to_string(),
930            min_items: Some(1),
931            max_items: Some(2),
932            items: MultiSelectItems {
933                any_of: vec![
934                    EnumOption {
935                        const_value: "#FF0000".to_string(),
936                        title: "Red".to_string(),
937                    },
938                    EnumOption {
939                        const_value: "#00FF00".to_string(),
940                        title: "Green".to_string(),
941                    },
942                ],
943            },
944            title: Some("Color Selection".to_string()),
945            description: Some("Choose up to 2 colors".to_string()),
946            default: Some(vec!["#FF0000".to_string()]),
947        };
948
949        let json = serde_json::to_string(&schema).unwrap();
950        assert!(json.contains("\"type\":\"array\""));
951        assert!(json.contains("\"minItems\":1"));
952        assert!(json.contains("\"maxItems\":2"));
953        assert!(json.contains("\"anyOf\""));
954        assert!(json.contains("\"const\":\"#FF0000\""));
955
956        // Verify deserialization
957        let deserialized: TitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
958        assert_eq!(deserialized.items.any_of.len(), 2);
959        assert_eq!(deserialized.min_items, Some(1));
960        assert_eq!(deserialized.max_items, Some(2));
961    }
962
963    #[test]
964    #[cfg(feature = "mcp-enum-improvements")]
965    fn test_untitled_multi_select_enum_schema() {
966        use super::{UntitledMultiSelectEnumSchema, UntitledMultiSelectItems};
967
968        let schema = UntitledMultiSelectEnumSchema {
969            schema_type: "array".to_string(),
970            min_items: Some(1),
971            max_items: None,
972            items: UntitledMultiSelectItems {
973                schema_type: "string".to_string(),
974                enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
975            },
976            title: Some("Color Selection".to_string()),
977            description: Some("Choose colors".to_string()),
978            default: Some(vec!["red".to_string(), "green".to_string()]),
979        };
980
981        let json = serde_json::to_string(&schema).unwrap();
982        assert!(json.contains("\"type\":\"array\""));
983        assert!(json.contains("\"minItems\":1"));
984        assert!(json.contains("\"items\""));
985        assert!(json.contains("\"enum\""));
986        assert!(!json.contains("anyOf"));
987
988        // Verify deserialization
989        let deserialized: UntitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
990        assert_eq!(deserialized.items.enum_values.len(), 3);
991        assert_eq!(deserialized.default.as_ref().unwrap().len(), 2);
992    }
993
994    #[test]
995    #[cfg(feature = "mcp-enum-improvements")]
996    fn test_legacy_titled_enum_schema() {
997        use super::LegacyTitledEnumSchema;
998
999        let schema = LegacyTitledEnumSchema {
1000            schema_type: "string".to_string(),
1001            enum_values: vec!["#FF0000".to_string(), "#00FF00".to_string()],
1002            enum_names: vec!["Red".to_string(), "Green".to_string()],
1003            title: Some("Color Selection".to_string()),
1004            description: Some("Choose a color (legacy)".to_string()),
1005            default: Some("#FF0000".to_string()),
1006        };
1007
1008        let json = serde_json::to_string(&schema).unwrap();
1009        assert!(json.contains("\"type\":\"string\""));
1010        assert!(json.contains("\"enum\""));
1011        assert!(json.contains("\"enumNames\""));
1012        assert!(json.contains("\"Red\""));
1013        assert!(!json.contains("oneOf"));
1014
1015        // Verify deserialization
1016        let deserialized: LegacyTitledEnumSchema = serde_json::from_str(&json).unwrap();
1017        assert_eq!(deserialized.enum_values.len(), 2);
1018        assert_eq!(deserialized.enum_names.len(), 2);
1019        assert_eq!(deserialized.enum_names[0], "Red");
1020    }
1021
1022    #[test]
1023    #[cfg(feature = "mcp-enum-improvements")]
1024    fn test_enum_schema_union_type() {
1025        use super::EnumSchema;
1026
1027        // Test that EnumSchema can deserialize different variants
1028        let titled_json = r#"{
1029            "type": "string",
1030            "oneOf": [
1031                {"const": "red", "title": "Red"},
1032                {"const": "green", "title": "Green"}
1033            ]
1034        }"#;
1035
1036        let schema: EnumSchema = serde_json::from_str(titled_json).unwrap();
1037        match schema {
1038            EnumSchema::TitledSingleSelect(s) => {
1039                assert_eq!(s.one_of.len(), 2);
1040                assert_eq!(s.one_of[0].const_value, "red");
1041            }
1042            _ => panic!("Expected TitledSingleSelect variant"),
1043        }
1044
1045        // Test untitled variant
1046        let untitled_json = r#"{
1047            "type": "string",
1048            "enum": ["red", "green", "blue"]
1049        }"#;
1050
1051        let schema: EnumSchema = serde_json::from_str(untitled_json).unwrap();
1052        match schema {
1053            EnumSchema::UntitledSingleSelect(s) => {
1054                assert_eq!(s.enum_values.len(), 3);
1055            }
1056            _ => panic!("Expected UntitledSingleSelect variant"),
1057        }
1058    }
1059
1060    #[test]
1061    #[cfg(feature = "mcp-enum-improvements")]
1062    fn test_enum_option_serialization() {
1063        use super::EnumOption;
1064
1065        let option = EnumOption {
1066            const_value: "#FF0000".to_string(),
1067            title: "Red".to_string(),
1068        };
1069
1070        let json = serde_json::to_string(&option).unwrap();
1071        assert!(json.contains("\"const\":\"#FF0000\""));
1072        assert!(json.contains("\"title\":\"Red\""));
1073        assert!(!json.contains("const_value")); // Verifies camelCase rename
1074
1075        // Verify deserialization
1076        let deserialized: EnumOption = serde_json::from_str(&json).unwrap();
1077        assert_eq!(deserialized.const_value, "#FF0000");
1078        assert_eq!(deserialized.title, "Red");
1079    }
1080}