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-06-18): 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-06-18: 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-06-18)
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-06-18)
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    #[cfg(feature = "mcp-tasks")]
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub task: Option<crate::types::tasks::TaskMetadata>,
650    /// Optional metadata per MCP 2025-06-18 specification
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub _meta: Option<serde_json::Value>,
653}
654
655impl Default for ElicitRequest {
656    fn default() -> Self {
657        Self {
658            params: ElicitRequestParams::form(String::new(), ElicitationSchema::new(), None, None),
659            #[cfg(feature = "mcp-tasks")]
660            task: None,
661            _meta: None,
662        }
663    }
664}
665
666/// Elicit result
667///
668/// ## Version Support
669/// - MCP 2025-06-18: action, content (form mode), _meta
670/// - MCP 2025-11-25 draft (SEP-1330): Clarified content field behavior
671///
672/// ## Content Field Behavior (SEP-1330 Clarification)
673///
674/// The `content` field is **only present** when:
675/// 1. `action` is `"accept"` (user submitted the form), AND
676/// 2. Mode was `"form"` (in-band structured data collection)
677///
678/// The `content` field is **omitted** when:
679/// - Action is `"decline"` or `"cancel"`
680/// - Mode was `"url"` (out-of-band, data doesn't transit through MCP)
681///
682/// ## Example
683///
684/// **Form mode with accept:**
685/// ```json
686/// {
687///   "action": "accept",
688///   "content": {
689///     "name": "Alice",
690///     "email": "alice@example.com"
691///   }
692/// }
693/// ```
694///
695/// **URL mode with accept:**
696/// ```json
697/// {
698///   "action": "accept"
699/// }
700/// ```
701/// Note: No `content` field - data was collected out-of-band
702#[derive(Debug, Clone, Serialize, Deserialize)]
703pub struct ElicitResult {
704    /// The user action in response to the elicitation
705    ///
706    /// - `accept`: User submitted the form/confirmed the action
707    /// - `decline`: User explicitly declined the action
708    /// - `cancel`: User dismissed without making an explicit choice
709    pub action: ElicitationAction,
710
711    /// The submitted form data, only present when action is "accept"
712    /// and mode was "form".
713    ///
714    /// Contains values matching the requested schema. Omitted for:
715    /// - `action` is `"decline"` or `"cancel"`
716    /// - URL mode responses (out-of-band data collection)
717    ///
718    /// Per MCP 2025-11-25 draft (SEP-1330), this clarification ensures
719    /// clients understand when to expect form data vs. out-of-band completion.
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
722
723    /// Optional metadata per MCP 2025-06-18 specification
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub _meta: Option<serde_json::Value>,
726}
727
728/// Elicitation completion notification parameters (MCP 2025-11-25 draft, SEP-1036)
729///
730/// Sent by the server to indicate that an out-of-band elicitation has been completed.
731/// This allows the client to know when to retry the original request.
732#[derive(Debug, Clone, Serialize, Deserialize)]
733#[cfg(feature = "mcp-url-elicitation")]
734pub struct ElicitationCompleteParams {
735    /// The ID of the completed elicitation
736    #[serde(rename = "elicitationId")]
737    pub elicitation_id: String,
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_elicitation_action_serialization() {
746        assert_eq!(
747            serde_json::to_string(&ElicitationAction::Accept).unwrap(),
748            "\"accept\""
749        );
750        assert_eq!(
751            serde_json::to_string(&ElicitationAction::Decline).unwrap(),
752            "\"decline\""
753        );
754        assert_eq!(
755            serde_json::to_string(&ElicitationAction::Cancel).unwrap(),
756            "\"cancel\""
757        );
758    }
759
760    #[test]
761    fn test_form_elicit_params() {
762        let schema = ElicitationSchema::new().add_string_property(
763            "name".to_string(),
764            true,
765            Some("User name".to_string()),
766        );
767
768        let params = ElicitRequestParams::form(
769            "Please provide your name".to_string(),
770            schema,
771            Some(30000),
772            Some(true),
773        );
774
775        assert_eq!(params.message(), "Please provide your name");
776
777        // Test serialization
778        let json = serde_json::to_string(&params).unwrap();
779        assert!(json.contains("Please provide your name"));
780        assert!(json.contains("requestedSchema"));
781    }
782
783    #[test]
784    #[cfg(feature = "mcp-url-elicitation")]
785    fn test_url_elicit_params() {
786        use url::Url;
787
788        let url = Url::parse("https://example.com/oauth/authorize").unwrap();
789        let params = ElicitRequestParams::url(
790            "test-id-123".to_string(),
791            "Please authorize the connection".to_string(),
792            url,
793        );
794
795        assert_eq!(params.message(), "Please authorize the connection");
796
797        // Test serialization
798        let json = serde_json::to_string(&params).unwrap();
799        assert!(json.contains("test-id-123"));
800        assert!(json.contains("https://example.com/oauth/authorize"));
801        assert!(json.contains("\"mode\":\"url\""));
802    }
803
804    #[test]
805    #[cfg(feature = "mcp-url-elicitation")]
806    fn test_elicit_mode_serialization() {
807        assert_eq!(
808            serde_json::to_string(&ElicitMode::Form).unwrap(),
809            "\"form\""
810        );
811        assert_eq!(serde_json::to_string(&ElicitMode::Url).unwrap(), "\"url\"");
812    }
813
814    #[test]
815    #[cfg(feature = "mcp-url-elicitation")]
816    fn test_completion_notification() {
817        let params = ElicitationCompleteParams {
818            elicitation_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
819        };
820
821        let json = serde_json::to_string(&params).unwrap();
822        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
823        assert!(json.contains("elicitationId"));
824    }
825
826    #[test]
827    fn test_elicit_result_form_mode() {
828        let mut content = std::collections::HashMap::new();
829        content.insert("name".to_string(), serde_json::json!("Alice"));
830
831        let result = ElicitResult {
832            action: ElicitationAction::Accept,
833            content: Some(content),
834            _meta: None,
835        };
836
837        let json = serde_json::to_string(&result).unwrap();
838        assert!(json.contains("\"action\":\"accept\""));
839        assert!(json.contains("\"name\":\"Alice\""));
840    }
841
842    #[test]
843    fn test_elicit_result_url_mode() {
844        // URL mode: no content field
845        let result = ElicitResult {
846            action: ElicitationAction::Accept,
847            content: None,
848            _meta: None,
849        };
850
851        let json = serde_json::to_string(&result).unwrap();
852        assert!(json.contains("\"action\":\"accept\""));
853        assert!(!json.contains("content"));
854    }
855
856    // ========== SEP-1330 Enum Schema Tests ==========
857
858    #[test]
859    #[cfg(feature = "mcp-enum-improvements")]
860    fn test_titled_single_select_enum_schema() {
861        use super::{EnumOption, TitledSingleSelectEnumSchema};
862
863        let schema = TitledSingleSelectEnumSchema {
864            schema_type: "string".to_string(),
865            one_of: vec![
866                EnumOption {
867                    const_value: "#FF0000".to_string(),
868                    title: "Red".to_string(),
869                },
870                EnumOption {
871                    const_value: "#00FF00".to_string(),
872                    title: "Green".to_string(),
873                },
874                EnumOption {
875                    const_value: "#0000FF".to_string(),
876                    title: "Blue".to_string(),
877                },
878            ],
879            title: Some("Color Selection".to_string()),
880            description: Some("Choose your favorite color".to_string()),
881            default: Some("#FF0000".to_string()),
882        };
883
884        let json = serde_json::to_string(&schema).unwrap();
885        assert!(json.contains("\"type\":\"string\""));
886        assert!(json.contains("\"oneOf\""));
887        assert!(json.contains("\"const\":\"#FF0000\""));
888        assert!(json.contains("\"title\":\"Red\""));
889        assert!(json.contains("\"default\":\"#FF0000\""));
890
891        // Verify deserialization
892        let deserialized: TitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
893        assert_eq!(deserialized.one_of.len(), 3);
894        assert_eq!(deserialized.one_of[0].const_value, "#FF0000");
895        assert_eq!(deserialized.one_of[0].title, "Red");
896    }
897
898    #[test]
899    #[cfg(feature = "mcp-enum-improvements")]
900    fn test_untitled_single_select_enum_schema() {
901        use super::UntitledSingleSelectEnumSchema;
902
903        let schema = UntitledSingleSelectEnumSchema {
904            schema_type: "string".to_string(),
905            enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
906            title: Some("Color Selection".to_string()),
907            description: Some("Choose a color".to_string()),
908            default: Some("red".to_string()),
909        };
910
911        let json = serde_json::to_string(&schema).unwrap();
912        assert!(json.contains("\"type\":\"string\""));
913        assert!(json.contains("\"enum\""));
914        assert!(json.contains("\"red\""));
915        assert!(json.contains("\"green\""));
916        assert!(json.contains("\"blue\""));
917        assert!(!json.contains("oneOf"));
918
919        // Verify deserialization
920        let deserialized: UntitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
921        assert_eq!(deserialized.enum_values.len(), 3);
922        assert_eq!(deserialized.enum_values[0], "red");
923    }
924
925    #[test]
926    #[cfg(feature = "mcp-enum-improvements")]
927    fn test_titled_multi_select_enum_schema() {
928        use super::{EnumOption, MultiSelectItems, TitledMultiSelectEnumSchema};
929
930        let schema = TitledMultiSelectEnumSchema {
931            schema_type: "array".to_string(),
932            min_items: Some(1),
933            max_items: Some(2),
934            items: MultiSelectItems {
935                any_of: vec![
936                    EnumOption {
937                        const_value: "#FF0000".to_string(),
938                        title: "Red".to_string(),
939                    },
940                    EnumOption {
941                        const_value: "#00FF00".to_string(),
942                        title: "Green".to_string(),
943                    },
944                ],
945            },
946            title: Some("Color Selection".to_string()),
947            description: Some("Choose up to 2 colors".to_string()),
948            default: Some(vec!["#FF0000".to_string()]),
949        };
950
951        let json = serde_json::to_string(&schema).unwrap();
952        assert!(json.contains("\"type\":\"array\""));
953        assert!(json.contains("\"minItems\":1"));
954        assert!(json.contains("\"maxItems\":2"));
955        assert!(json.contains("\"anyOf\""));
956        assert!(json.contains("\"const\":\"#FF0000\""));
957
958        // Verify deserialization
959        let deserialized: TitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
960        assert_eq!(deserialized.items.any_of.len(), 2);
961        assert_eq!(deserialized.min_items, Some(1));
962        assert_eq!(deserialized.max_items, Some(2));
963    }
964
965    #[test]
966    #[cfg(feature = "mcp-enum-improvements")]
967    fn test_untitled_multi_select_enum_schema() {
968        use super::{UntitledMultiSelectEnumSchema, UntitledMultiSelectItems};
969
970        let schema = UntitledMultiSelectEnumSchema {
971            schema_type: "array".to_string(),
972            min_items: Some(1),
973            max_items: None,
974            items: UntitledMultiSelectItems {
975                schema_type: "string".to_string(),
976                enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
977            },
978            title: Some("Color Selection".to_string()),
979            description: Some("Choose colors".to_string()),
980            default: Some(vec!["red".to_string(), "green".to_string()]),
981        };
982
983        let json = serde_json::to_string(&schema).unwrap();
984        assert!(json.contains("\"type\":\"array\""));
985        assert!(json.contains("\"minItems\":1"));
986        assert!(json.contains("\"items\""));
987        assert!(json.contains("\"enum\""));
988        assert!(!json.contains("anyOf"));
989
990        // Verify deserialization
991        let deserialized: UntitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
992        assert_eq!(deserialized.items.enum_values.len(), 3);
993        assert_eq!(deserialized.default.as_ref().unwrap().len(), 2);
994    }
995
996    #[test]
997    #[cfg(feature = "mcp-enum-improvements")]
998    fn test_legacy_titled_enum_schema() {
999        use super::LegacyTitledEnumSchema;
1000
1001        let schema = LegacyTitledEnumSchema {
1002            schema_type: "string".to_string(),
1003            enum_values: vec!["#FF0000".to_string(), "#00FF00".to_string()],
1004            enum_names: vec!["Red".to_string(), "Green".to_string()],
1005            title: Some("Color Selection".to_string()),
1006            description: Some("Choose a color (legacy)".to_string()),
1007            default: Some("#FF0000".to_string()),
1008        };
1009
1010        let json = serde_json::to_string(&schema).unwrap();
1011        assert!(json.contains("\"type\":\"string\""));
1012        assert!(json.contains("\"enum\""));
1013        assert!(json.contains("\"enumNames\""));
1014        assert!(json.contains("\"Red\""));
1015        assert!(!json.contains("oneOf"));
1016
1017        // Verify deserialization
1018        let deserialized: LegacyTitledEnumSchema = serde_json::from_str(&json).unwrap();
1019        assert_eq!(deserialized.enum_values.len(), 2);
1020        assert_eq!(deserialized.enum_names.len(), 2);
1021        assert_eq!(deserialized.enum_names[0], "Red");
1022    }
1023
1024    #[test]
1025    #[cfg(feature = "mcp-enum-improvements")]
1026    fn test_enum_schema_union_type() {
1027        use super::EnumSchema;
1028
1029        // Test that EnumSchema can deserialize different variants
1030        let titled_json = r#"{
1031            "type": "string",
1032            "oneOf": [
1033                {"const": "red", "title": "Red"},
1034                {"const": "green", "title": "Green"}
1035            ]
1036        }"#;
1037
1038        let schema: EnumSchema = serde_json::from_str(titled_json).unwrap();
1039        match schema {
1040            EnumSchema::TitledSingleSelect(s) => {
1041                assert_eq!(s.one_of.len(), 2);
1042                assert_eq!(s.one_of[0].const_value, "red");
1043            }
1044            _ => panic!("Expected TitledSingleSelect variant"),
1045        }
1046
1047        // Test untitled variant
1048        let untitled_json = r#"{
1049            "type": "string",
1050            "enum": ["red", "green", "blue"]
1051        }"#;
1052
1053        let schema: EnumSchema = serde_json::from_str(untitled_json).unwrap();
1054        match schema {
1055            EnumSchema::UntitledSingleSelect(s) => {
1056                assert_eq!(s.enum_values.len(), 3);
1057            }
1058            _ => panic!("Expected UntitledSingleSelect variant"),
1059        }
1060    }
1061
1062    #[test]
1063    #[cfg(feature = "mcp-enum-improvements")]
1064    fn test_enum_option_serialization() {
1065        use super::EnumOption;
1066
1067        let option = EnumOption {
1068            const_value: "#FF0000".to_string(),
1069            title: "Red".to_string(),
1070        };
1071
1072        let json = serde_json::to_string(&option).unwrap();
1073        assert!(json.contains("\"const\":\"#FF0000\""));
1074        assert!(json.contains("\"title\":\"Red\""));
1075        assert!(!json.contains("const_value")); // Verifies camelCase rename
1076
1077        // Verify deserialization
1078        let deserialized: EnumOption = serde_json::from_str(&json).unwrap();
1079        assert_eq!(deserialized.const_value, "#FF0000");
1080        assert_eq!(deserialized.title, "Red");
1081    }
1082}