Skip to main content

agent_client_protocol_schema/v2/
elicitation.rs

1//! Elicitation types for structured user input.
2//!
3//! **UNSTABLE**: This module is not part of the spec yet, and may be removed or changed at any point.
4//!
5//! This module defines the types used for agent-initiated elicitation,
6//! where the agent requests structured input from the user via forms or URLs.
7
8use std::{collections::BTreeMap, sync::Arc};
9
10use derive_more::{Display, From};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
14
15use super::{
16    ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME, Meta, RequestId, SessionId,
17    ToolCallId,
18};
19use crate::IntoOption;
20
21/// **UNSTABLE**
22///
23/// This capability is not part of the spec yet, and may be removed or changed at any point.
24///
25/// Unique identifier for an elicitation.
26#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
27#[serde(transparent)]
28#[from(Arc<str>, String, &'static str)]
29#[non_exhaustive]
30pub struct ElicitationId(pub Arc<str>);
31
32impl ElicitationId {
33    #[must_use]
34    pub fn new(id: impl Into<Arc<str>>) -> Self {
35        Self(id.into())
36    }
37}
38
39/// String format types for string properties in elicitation schemas.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
41#[serde(rename_all = "kebab-case")]
42#[non_exhaustive]
43pub enum StringFormat {
44    /// Email address format.
45    Email,
46    /// URI format.
47    Uri,
48    /// Date format (YYYY-MM-DD).
49    Date,
50    /// Date-time format (ISO 8601).
51    DateTime,
52    /// Custom or future string format.
53    ///
54    /// Unknown formats are preserved. Implementations that do not understand a
55    /// format should treat it as an annotation rather than rejecting the schema.
56    #[serde(untagged)]
57    Other(String),
58}
59
60/// Type discriminator for elicitation schemas.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
62#[serde(rename_all = "snake_case")]
63#[non_exhaustive]
64pub enum ElicitationSchemaType {
65    /// Object schema type.
66    #[default]
67    Object,
68}
69
70/// A titled enum option with a const value and human-readable title.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72#[non_exhaustive]
73pub struct EnumOption {
74    /// The constant value for this option.
75    #[serde(rename = "const")]
76    pub value: String,
77    /// Human-readable title for this option.
78    pub title: String,
79}
80
81impl EnumOption {
82    /// Create a new enum option.
83    #[must_use]
84    pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
85        Self {
86            value: value.into(),
87            title: title.into(),
88        }
89    }
90}
91
92/// Schema for string properties in an elicitation form.
93///
94/// When `enum` or `oneOf` is set, this represents a single-select enum
95/// with `"type": "string"`.
96#[skip_serializing_none]
97#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
98#[serde(rename_all = "camelCase")]
99#[non_exhaustive]
100pub struct StringPropertySchema {
101    /// Optional title for the property.
102    pub title: Option<String>,
103    /// Human-readable description.
104    pub description: Option<String>,
105    /// Minimum string length.
106    pub min_length: Option<u32>,
107    /// Maximum string length.
108    pub max_length: Option<u32>,
109    /// Pattern the string must match.
110    pub pattern: Option<String>,
111    /// String format.
112    pub format: Option<StringFormat>,
113    /// Default value.
114    pub default: Option<String>,
115    /// Enum values for untitled single-select enums.
116    #[serde(rename = "enum")]
117    pub enum_values: Option<Vec<String>>,
118    /// Titled enum options for titled single-select enums.
119    #[serde(rename = "oneOf")]
120    pub one_of: Option<Vec<EnumOption>>,
121}
122
123impl StringPropertySchema {
124    /// Create a new string property schema.
125    #[must_use]
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// Create an email string property schema.
131    #[must_use]
132    pub fn email() -> Self {
133        Self {
134            format: Some(StringFormat::Email),
135            ..Default::default()
136        }
137    }
138
139    /// Create a URI string property schema.
140    #[must_use]
141    pub fn uri() -> Self {
142        Self {
143            format: Some(StringFormat::Uri),
144            ..Default::default()
145        }
146    }
147
148    /// Create a date string property schema.
149    #[must_use]
150    pub fn date() -> Self {
151        Self {
152            format: Some(StringFormat::Date),
153            ..Default::default()
154        }
155    }
156
157    /// Create a date-time string property schema.
158    #[must_use]
159    pub fn date_time() -> Self {
160        Self {
161            format: Some(StringFormat::DateTime),
162            ..Default::default()
163        }
164    }
165
166    /// Optional title for the property.
167    #[must_use]
168    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
169        self.title = title.into_option();
170        self
171    }
172
173    /// Human-readable description.
174    #[must_use]
175    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
176        self.description = description.into_option();
177        self
178    }
179
180    /// Minimum string length.
181    #[must_use]
182    pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
183        self.min_length = min_length.into_option();
184        self
185    }
186
187    /// Maximum string length.
188    #[must_use]
189    pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
190        self.max_length = max_length.into_option();
191        self
192    }
193
194    /// Pattern the string must match.
195    #[must_use]
196    pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
197        self.pattern = pattern.into_option();
198        self
199    }
200
201    /// String format.
202    #[must_use]
203    pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
204        self.format = format.into_option();
205        self
206    }
207
208    /// Default value.
209    #[must_use]
210    pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
211        self.default = default.into_option();
212        self
213    }
214
215    /// Enum values for untitled single-select enums.
216    #[must_use]
217    pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
218        self.enum_values = enum_values.into_option();
219        self
220    }
221
222    /// Titled enum options for titled single-select enums.
223    #[must_use]
224    pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
225        self.one_of = one_of.into_option();
226        self
227    }
228}
229
230/// Schema for number (floating-point) properties in an elicitation form.
231#[skip_serializing_none]
232#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
233#[serde(rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct NumberPropertySchema {
236    /// Optional title for the property.
237    pub title: Option<String>,
238    /// Human-readable description.
239    pub description: Option<String>,
240    /// Minimum value (inclusive).
241    pub minimum: Option<f64>,
242    /// Maximum value (inclusive).
243    pub maximum: Option<f64>,
244    /// Default value.
245    pub default: Option<f64>,
246}
247
248impl NumberPropertySchema {
249    /// Create a new number property schema.
250    #[must_use]
251    pub fn new() -> Self {
252        Self::default()
253    }
254
255    /// Optional title for the property.
256    #[must_use]
257    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
258        self.title = title.into_option();
259        self
260    }
261
262    /// Human-readable description.
263    #[must_use]
264    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
265        self.description = description.into_option();
266        self
267    }
268
269    /// Minimum value (inclusive).
270    #[must_use]
271    pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
272        self.minimum = minimum.into_option();
273        self
274    }
275
276    /// Maximum value (inclusive).
277    #[must_use]
278    pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
279        self.maximum = maximum.into_option();
280        self
281    }
282
283    /// Default value.
284    #[must_use]
285    pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
286        self.default = default.into_option();
287        self
288    }
289}
290
291/// Schema for integer properties in an elicitation form.
292#[skip_serializing_none]
293#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
294#[serde(rename_all = "camelCase")]
295#[non_exhaustive]
296pub struct IntegerPropertySchema {
297    /// Optional title for the property.
298    pub title: Option<String>,
299    /// Human-readable description.
300    pub description: Option<String>,
301    /// Minimum value (inclusive).
302    pub minimum: Option<i64>,
303    /// Maximum value (inclusive).
304    pub maximum: Option<i64>,
305    /// Default value.
306    pub default: Option<i64>,
307}
308
309impl IntegerPropertySchema {
310    /// Create a new integer property schema.
311    #[must_use]
312    pub fn new() -> Self {
313        Self::default()
314    }
315
316    /// Optional title for the property.
317    #[must_use]
318    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
319        self.title = title.into_option();
320        self
321    }
322
323    /// Human-readable description.
324    #[must_use]
325    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
326        self.description = description.into_option();
327        self
328    }
329
330    /// Minimum value (inclusive).
331    #[must_use]
332    pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
333        self.minimum = minimum.into_option();
334        self
335    }
336
337    /// Maximum value (inclusive).
338    #[must_use]
339    pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
340        self.maximum = maximum.into_option();
341        self
342    }
343
344    /// Default value.
345    #[must_use]
346    pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
347        self.default = default.into_option();
348        self
349    }
350}
351
352/// Schema for boolean properties in an elicitation form.
353#[skip_serializing_none]
354#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct BooleanPropertySchema {
358    /// Optional title for the property.
359    pub title: Option<String>,
360    /// Human-readable description.
361    pub description: Option<String>,
362    /// Default value.
363    pub default: Option<bool>,
364}
365
366impl BooleanPropertySchema {
367    /// Create a new boolean property schema.
368    #[must_use]
369    pub fn new() -> Self {
370        Self::default()
371    }
372
373    /// Optional title for the property.
374    #[must_use]
375    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
376        self.title = title.into_option();
377        self
378    }
379
380    /// Human-readable description.
381    #[must_use]
382    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
383        self.description = description.into_option();
384        self
385    }
386
387    /// Default value.
388    #[must_use]
389    pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
390        self.default = default.into_option();
391        self
392    }
393}
394
395/// Items definition for untitled multi-select enum properties.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
397#[serde(rename_all = "snake_case")]
398#[non_exhaustive]
399pub enum ElicitationStringType {
400    /// String schema type.
401    #[default]
402    String,
403}
404
405/// Items definition for untitled multi-select enum properties.
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
407#[non_exhaustive]
408pub struct UntitledMultiSelectItems {
409    /// Item type discriminator. Must be `"string"`.
410    #[serde(rename = "type")]
411    pub type_: ElicitationStringType,
412    /// Allowed enum values.
413    #[serde(rename = "enum")]
414    pub values: Vec<String>,
415}
416
417/// Items definition for titled multi-select enum properties.
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419#[non_exhaustive]
420pub struct TitledMultiSelectItems {
421    /// Titled enum options.
422    #[serde(rename = "anyOf")]
423    pub options: Vec<EnumOption>,
424}
425
426impl TitledMultiSelectItems {
427    /// Create new titled multi-select items.
428    #[must_use]
429    pub fn new(options: Vec<EnumOption>) -> Self {
430        Self { options }
431    }
432}
433
434/// Items for a multi-select (array) property schema.
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
436#[serde(untagged)]
437#[non_exhaustive]
438pub enum MultiSelectItems {
439    /// Untitled multi-select items with plain string values.
440    Untitled(UntitledMultiSelectItems),
441    /// Titled multi-select items with human-readable labels.
442    Titled(TitledMultiSelectItems),
443}
444
445/// Schema for multi-select (array) properties in an elicitation form.
446#[skip_serializing_none]
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
448#[serde(rename_all = "camelCase")]
449#[non_exhaustive]
450pub struct MultiSelectPropertySchema {
451    /// Optional title for the property.
452    pub title: Option<String>,
453    /// Human-readable description.
454    pub description: Option<String>,
455    /// Minimum number of items to select.
456    pub min_items: Option<u64>,
457    /// Maximum number of items to select.
458    pub max_items: Option<u64>,
459    /// The items definition describing allowed values.
460    pub items: MultiSelectItems,
461    /// Default selected values.
462    pub default: Option<Vec<String>>,
463}
464
465impl MultiSelectPropertySchema {
466    /// Create a new untitled multi-select property schema.
467    #[must_use]
468    pub fn new(values: Vec<String>) -> Self {
469        Self {
470            title: None,
471            description: None,
472            min_items: None,
473            max_items: None,
474            items: MultiSelectItems::Untitled(UntitledMultiSelectItems {
475                type_: ElicitationStringType::String,
476                values,
477            }),
478            default: None,
479        }
480    }
481
482    /// Create a new titled multi-select property schema.
483    #[must_use]
484    pub fn titled(options: Vec<EnumOption>) -> Self {
485        Self {
486            title: None,
487            description: None,
488            min_items: None,
489            max_items: None,
490            items: MultiSelectItems::Titled(TitledMultiSelectItems { options }),
491            default: None,
492        }
493    }
494
495    /// Optional title for the property.
496    #[must_use]
497    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
498        self.title = title.into_option();
499        self
500    }
501
502    /// Human-readable description.
503    #[must_use]
504    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
505        self.description = description.into_option();
506        self
507    }
508
509    /// Minimum number of items to select.
510    #[must_use]
511    pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
512        self.min_items = min_items.into_option();
513        self
514    }
515
516    /// Maximum number of items to select.
517    #[must_use]
518    pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
519        self.max_items = max_items.into_option();
520        self
521    }
522
523    /// Default selected values.
524    #[must_use]
525    pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
526        self.default = default.into_option();
527        self
528    }
529}
530
531/// Property schema for elicitation form fields.
532///
533/// Each variant corresponds to a JSON Schema `"type"` value.
534/// Single-select enums use the `String` variant with `enum` or `oneOf` set.
535/// Multi-select enums use the `Array` variant.
536#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
537#[serde(tag = "type", rename_all = "snake_case")]
538#[schemars(extend("discriminator" = {"propertyName": "type"}))]
539#[non_exhaustive]
540pub enum ElicitationPropertySchema {
541    /// String property (or single-select enum when `enum`/`oneOf` is set).
542    String(StringPropertySchema),
543    /// Number (floating-point) property.
544    Number(NumberPropertySchema),
545    /// Integer property.
546    Integer(IntegerPropertySchema),
547    /// Boolean property.
548    Boolean(BooleanPropertySchema),
549    /// Multi-select array property.
550    Array(MultiSelectPropertySchema),
551}
552
553impl From<StringPropertySchema> for ElicitationPropertySchema {
554    fn from(schema: StringPropertySchema) -> Self {
555        Self::String(schema)
556    }
557}
558
559impl From<NumberPropertySchema> for ElicitationPropertySchema {
560    fn from(schema: NumberPropertySchema) -> Self {
561        Self::Number(schema)
562    }
563}
564
565impl From<IntegerPropertySchema> for ElicitationPropertySchema {
566    fn from(schema: IntegerPropertySchema) -> Self {
567        Self::Integer(schema)
568    }
569}
570
571impl From<BooleanPropertySchema> for ElicitationPropertySchema {
572    fn from(schema: BooleanPropertySchema) -> Self {
573        Self::Boolean(schema)
574    }
575}
576
577impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
578    fn from(schema: MultiSelectPropertySchema) -> Self {
579        Self::Array(schema)
580    }
581}
582
583fn default_object_type() -> ElicitationSchemaType {
584    ElicitationSchemaType::Object
585}
586
587/// Type-safe elicitation schema for requesting structured user input.
588///
589/// This represents a JSON Schema object with primitive-typed properties,
590/// as required by the elicitation specification.
591#[skip_serializing_none]
592#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
593#[serde(rename_all = "camelCase")]
594#[non_exhaustive]
595pub struct ElicitationSchema {
596    /// Type discriminator. Always `"object"`.
597    #[serde(rename = "type", default = "default_object_type")]
598    pub type_: ElicitationSchemaType,
599    /// Optional title for the schema.
600    pub title: Option<String>,
601    /// Property definitions (must be primitive types).
602    #[serde(default)]
603    pub properties: BTreeMap<String, ElicitationPropertySchema>,
604    /// List of required property names.
605    pub required: Option<Vec<String>>,
606    /// Optional description of what this schema represents.
607    pub description: Option<String>,
608}
609
610impl Default for ElicitationSchema {
611    fn default() -> Self {
612        Self {
613            type_: default_object_type(),
614            title: None,
615            properties: BTreeMap::new(),
616            required: None,
617            description: None,
618        }
619    }
620}
621
622impl ElicitationSchema {
623    /// Create a new empty elicitation schema.
624    #[must_use]
625    pub fn new() -> Self {
626        Self::default()
627    }
628
629    /// Optional title for the schema.
630    #[must_use]
631    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
632        self.title = title.into_option();
633        self
634    }
635
636    /// Optional description of what this schema represents.
637    #[must_use]
638    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
639        self.description = description.into_option();
640        self
641    }
642
643    /// Add a property to the schema.
644    #[must_use]
645    pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
646    where
647        S: Into<ElicitationPropertySchema>,
648    {
649        let name = name.into();
650        self.properties.insert(name.clone(), schema.into());
651
652        if required {
653            let required_fields = self.required.get_or_insert_with(Vec::new);
654            if !required_fields.contains(&name) {
655                required_fields.push(name);
656            }
657        } else if let Some(required_fields) = &mut self.required {
658            required_fields.retain(|field| field != &name);
659
660            if required_fields.is_empty() {
661                self.required = None;
662            }
663        }
664
665        self
666    }
667
668    /// Add a string property.
669    #[must_use]
670    pub fn string(self, name: impl Into<String>, required: bool) -> Self {
671        self.property(name, StringPropertySchema::new(), required)
672    }
673
674    /// Add an email property.
675    #[must_use]
676    pub fn email(self, name: impl Into<String>, required: bool) -> Self {
677        self.property(name, StringPropertySchema::email(), required)
678    }
679
680    /// Add a URI property.
681    #[must_use]
682    pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
683        self.property(name, StringPropertySchema::uri(), required)
684    }
685
686    /// Add a date property.
687    #[must_use]
688    pub fn date(self, name: impl Into<String>, required: bool) -> Self {
689        self.property(name, StringPropertySchema::date(), required)
690    }
691
692    /// Add a date-time property.
693    #[must_use]
694    pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
695        self.property(name, StringPropertySchema::date_time(), required)
696    }
697
698    /// Add a number property with range.
699    #[must_use]
700    pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
701        self.property(
702            name,
703            NumberPropertySchema::new().minimum(min).maximum(max),
704            required,
705        )
706    }
707
708    /// Add an integer property with range.
709    #[must_use]
710    pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
711        self.property(
712            name,
713            IntegerPropertySchema::new().minimum(min).maximum(max),
714            required,
715        )
716    }
717
718    /// Add a boolean property.
719    #[must_use]
720    pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
721        self.property(name, BooleanPropertySchema::new(), required)
722    }
723}
724
725/// **UNSTABLE**
726///
727/// This capability is not part of the spec yet, and may be removed or changed at any point.
728///
729/// Elicitation capabilities supported by the client.
730#[serde_as]
731#[skip_serializing_none]
732#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
733#[serde(rename_all = "camelCase")]
734#[non_exhaustive]
735pub struct ElicitationCapabilities {
736    /// Whether the client supports form-based elicitation.
737    #[serde_as(deserialize_as = "DefaultOnError")]
738    #[schemars(extend("x-deserialize-default-on-error" = true))]
739    #[serde(default)]
740    pub form: Option<ElicitationFormCapabilities>,
741    /// Whether the client supports URL-based elicitation.
742    #[serde_as(deserialize_as = "DefaultOnError")]
743    #[schemars(extend("x-deserialize-default-on-error" = true))]
744    #[serde(default)]
745    pub url: Option<ElicitationUrlCapabilities>,
746    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
747    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
748    /// these keys.
749    ///
750    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
751    #[serde(rename = "_meta")]
752    pub meta: Option<Meta>,
753}
754
755impl ElicitationCapabilities {
756    #[must_use]
757    pub fn new() -> Self {
758        Self::default()
759    }
760
761    /// Whether the client supports form-based elicitation.
762    #[must_use]
763    pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
764        self.form = form.into_option();
765        self
766    }
767
768    /// Whether the client supports URL-based elicitation.
769    #[must_use]
770    pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
771        self.url = url.into_option();
772        self
773    }
774
775    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
776    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
777    /// these keys.
778    ///
779    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
780    #[must_use]
781    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
782        self.meta = meta.into_option();
783        self
784    }
785}
786
787/// **UNSTABLE**
788///
789/// This capability is not part of the spec yet, and may be removed or changed at any point.
790///
791/// Form-based elicitation capabilities.
792#[skip_serializing_none]
793#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
794#[serde(rename_all = "camelCase")]
795#[non_exhaustive]
796pub struct ElicitationFormCapabilities {
797    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
798    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
799    /// these keys.
800    ///
801    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
802    #[serde(rename = "_meta")]
803    pub meta: Option<Meta>,
804}
805
806impl ElicitationFormCapabilities {
807    #[must_use]
808    pub fn new() -> Self {
809        Self::default()
810    }
811
812    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
813    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
814    /// these keys.
815    ///
816    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
817    #[must_use]
818    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
819        self.meta = meta.into_option();
820        self
821    }
822}
823
824/// **UNSTABLE**
825///
826/// This capability is not part of the spec yet, and may be removed or changed at any point.
827///
828/// URL-based elicitation capabilities.
829#[skip_serializing_none]
830#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
831#[serde(rename_all = "camelCase")]
832#[non_exhaustive]
833pub struct ElicitationUrlCapabilities {
834    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
835    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
836    /// these keys.
837    ///
838    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
839    #[serde(rename = "_meta")]
840    pub meta: Option<Meta>,
841}
842
843impl ElicitationUrlCapabilities {
844    #[must_use]
845    pub fn new() -> Self {
846        Self::default()
847    }
848
849    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
850    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
851    /// these keys.
852    ///
853    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
854    #[must_use]
855    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
856        self.meta = meta.into_option();
857        self
858    }
859}
860
861/// **UNSTABLE**
862///
863/// This capability is not part of the spec yet, and may be removed or changed at any point.
864///
865/// The scope of an elicitation request, determining what context it's tied to.
866#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
867#[serde(untagged)]
868#[non_exhaustive]
869pub enum ElicitationScope {
870    /// Tied to a session, optionally to a specific tool call within that session.
871    Session(ElicitationSessionScope),
872    /// Tied to a specific JSON-RPC request outside of a session
873    /// (e.g., during auth/configuration phases before any session is started).
874    Request(ElicitationRequestScope),
875}
876
877/// **UNSTABLE**
878///
879/// This capability is not part of the spec yet, and may be removed or changed at any point.
880///
881/// Session-scoped elicitation, optionally tied to a specific tool call.
882///
883/// When `tool_call_id` is set, the elicitation is tied to a specific tool call.
884/// This is useful when an agent receives an elicitation from an MCP server
885/// during a tool call and needs to redirect it to the user.
886#[skip_serializing_none]
887#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
888#[serde(rename_all = "camelCase")]
889#[non_exhaustive]
890pub struct ElicitationSessionScope {
891    /// The session this elicitation is tied to.
892    pub session_id: SessionId,
893    /// Optional tool call within the session.
894    pub tool_call_id: Option<ToolCallId>,
895}
896
897impl ElicitationSessionScope {
898    #[must_use]
899    pub fn new(session_id: impl Into<SessionId>) -> Self {
900        Self {
901            session_id: session_id.into(),
902            tool_call_id: None,
903        }
904    }
905
906    #[must_use]
907    pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
908        self.tool_call_id = tool_call_id.into_option();
909        self
910    }
911}
912
913/// **UNSTABLE**
914///
915/// This capability is not part of the spec yet, and may be removed or changed at any point.
916///
917/// Request-scoped elicitation, tied to a specific JSON-RPC request outside of a session
918/// (e.g., during auth/configuration phases before any session is started).
919#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
920#[serde(rename_all = "camelCase")]
921#[non_exhaustive]
922pub struct ElicitationRequestScope {
923    /// The request this elicitation is tied to.
924    pub request_id: RequestId,
925}
926
927impl ElicitationRequestScope {
928    #[must_use]
929    pub fn new(request_id: impl Into<RequestId>) -> Self {
930        Self {
931            request_id: request_id.into(),
932        }
933    }
934}
935
936impl From<ElicitationSessionScope> for ElicitationScope {
937    fn from(scope: ElicitationSessionScope) -> Self {
938        Self::Session(scope)
939    }
940}
941
942impl From<ElicitationRequestScope> for ElicitationScope {
943    fn from(scope: ElicitationRequestScope) -> Self {
944        Self::Request(scope)
945    }
946}
947
948/// **UNSTABLE**
949///
950/// This capability is not part of the spec yet, and may be removed or changed at any point.
951///
952/// Request from the agent to elicit structured user input.
953///
954/// The agent sends this to the client to request information from the user,
955/// either via a form or by directing them to a URL.
956/// Elicitations are tied to a session (optionally a tool call) or a request.
957#[skip_serializing_none]
958#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
959#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
960#[serde(rename_all = "camelCase")]
961#[non_exhaustive]
962pub struct CreateElicitationRequest {
963    /// The elicitation mode and its mode-specific fields.
964    #[serde(flatten)]
965    pub mode: ElicitationMode,
966    /// A human-readable message describing what input is needed.
967    pub message: String,
968    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
969    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
970    /// these keys.
971    ///
972    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
973    #[serde(rename = "_meta")]
974    pub meta: Option<Meta>,
975}
976
977impl CreateElicitationRequest {
978    #[must_use]
979    pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
980        Self {
981            mode: mode.into(),
982            message: message.into(),
983            meta: None,
984        }
985    }
986
987    /// Returns the scope this elicitation is tied to.
988    #[must_use]
989    pub fn scope(&self) -> &ElicitationScope {
990        self.mode.scope()
991    }
992
993    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
994    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
995    /// these keys.
996    ///
997    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
998    #[must_use]
999    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1000        self.meta = meta.into_option();
1001        self
1002    }
1003}
1004
1005/// **UNSTABLE**
1006///
1007/// This capability is not part of the spec yet, and may be removed or changed at any point.
1008///
1009/// The mode of elicitation, determining how user input is collected.
1010#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1011#[serde(tag = "mode", rename_all = "snake_case")]
1012#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
1013#[non_exhaustive]
1014pub enum ElicitationMode {
1015    /// Form-based elicitation where the client renders a form from the provided schema.
1016    Form(ElicitationFormMode),
1017    /// URL-based elicitation where the client directs the user to a URL.
1018    Url(ElicitationUrlMode),
1019}
1020
1021impl From<ElicitationFormMode> for ElicitationMode {
1022    fn from(mode: ElicitationFormMode) -> Self {
1023        Self::Form(mode)
1024    }
1025}
1026
1027impl From<ElicitationUrlMode> for ElicitationMode {
1028    fn from(mode: ElicitationUrlMode) -> Self {
1029        Self::Url(mode)
1030    }
1031}
1032
1033impl ElicitationMode {
1034    /// Returns the scope this elicitation mode is tied to.
1035    #[must_use]
1036    pub fn scope(&self) -> &ElicitationScope {
1037        match self {
1038            Self::Form(f) => &f.scope,
1039            Self::Url(u) => &u.scope,
1040        }
1041    }
1042}
1043
1044/// **UNSTABLE**
1045///
1046/// This capability is not part of the spec yet, and may be removed or changed at any point.
1047///
1048/// Form-based elicitation mode where the client renders a form from the provided schema.
1049#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1050#[serde(rename_all = "camelCase")]
1051#[non_exhaustive]
1052pub struct ElicitationFormMode {
1053    /// The scope this elicitation is tied to.
1054    #[serde(flatten)]
1055    pub scope: ElicitationScope,
1056    /// A JSON Schema describing the form fields to present to the user.
1057    pub requested_schema: ElicitationSchema,
1058}
1059
1060impl ElicitationFormMode {
1061    #[must_use]
1062    pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1063        Self {
1064            scope: scope.into(),
1065            requested_schema,
1066        }
1067    }
1068}
1069
1070/// **UNSTABLE**
1071///
1072/// This capability is not part of the spec yet, and may be removed or changed at any point.
1073///
1074/// URL-based elicitation mode where the client directs the user to a URL.
1075#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1076#[serde(rename_all = "camelCase")]
1077#[non_exhaustive]
1078pub struct ElicitationUrlMode {
1079    /// The scope this elicitation is tied to.
1080    #[serde(flatten)]
1081    pub scope: ElicitationScope,
1082    /// The unique identifier for this elicitation.
1083    pub elicitation_id: ElicitationId,
1084    /// The URL to direct the user to.
1085    #[schemars(extend("format" = "uri"))]
1086    pub url: String,
1087}
1088
1089impl ElicitationUrlMode {
1090    #[must_use]
1091    pub fn new(
1092        scope: impl Into<ElicitationScope>,
1093        elicitation_id: impl Into<ElicitationId>,
1094        url: impl Into<String>,
1095    ) -> Self {
1096        Self {
1097            scope: scope.into(),
1098            elicitation_id: elicitation_id.into(),
1099            url: url.into(),
1100        }
1101    }
1102}
1103
1104/// **UNSTABLE**
1105///
1106/// This capability is not part of the spec yet, and may be removed or changed at any point.
1107///
1108/// Response from the client to an elicitation request.
1109#[skip_serializing_none]
1110#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1111#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1112#[serde(rename_all = "camelCase")]
1113#[non_exhaustive]
1114pub struct CreateElicitationResponse {
1115    /// The user's action in response to the elicitation.
1116    #[serde(flatten)]
1117    pub action: ElicitationAction,
1118    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1119    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1120    /// these keys.
1121    ///
1122    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1123    #[serde(rename = "_meta")]
1124    pub meta: Option<Meta>,
1125}
1126
1127impl CreateElicitationResponse {
1128    #[must_use]
1129    pub fn new(action: ElicitationAction) -> Self {
1130        Self { action, meta: None }
1131    }
1132
1133    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1134    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1135    /// these keys.
1136    ///
1137    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1138    #[must_use]
1139    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1140        self.meta = meta.into_option();
1141        self
1142    }
1143}
1144
1145/// **UNSTABLE**
1146///
1147/// This capability is not part of the spec yet, and may be removed or changed at any point.
1148///
1149/// The user's action in response to an elicitation.
1150#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1151#[serde(tag = "action", rename_all = "snake_case")]
1152#[schemars(extend("discriminator" = {"propertyName": "action"}))]
1153#[non_exhaustive]
1154pub enum ElicitationAction {
1155    /// The user accepted and provided content.
1156    Accept(ElicitationAcceptAction),
1157    /// The user declined the elicitation.
1158    Decline,
1159    /// The elicitation was cancelled.
1160    Cancel,
1161}
1162
1163/// **UNSTABLE**
1164///
1165/// This capability is not part of the spec yet, and may be removed or changed at any point.
1166///
1167/// The user accepted the elicitation and provided content.
1168#[skip_serializing_none]
1169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1170#[serde(rename_all = "camelCase")]
1171#[non_exhaustive]
1172pub struct ElicitationAcceptAction {
1173    /// The user-provided content, if any, as an object matching the requested schema.
1174    #[serde(default)]
1175    pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1176}
1177
1178impl ElicitationAcceptAction {
1179    #[must_use]
1180    pub fn new() -> Self {
1181        Self { content: None }
1182    }
1183
1184    /// The user-provided content as an object matching the requested schema.
1185    #[must_use]
1186    pub fn content(
1187        mut self,
1188        content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1189    ) -> Self {
1190        self.content = content.into_option();
1191        self
1192    }
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1196#[serde(untagged)]
1197#[non_exhaustive]
1198pub enum ElicitationContentValue {
1199    String(String),
1200    Integer(i64),
1201    Number(f64),
1202    Boolean(bool),
1203    StringArray(Vec<String>),
1204}
1205
1206impl From<String> for ElicitationContentValue {
1207    fn from(value: String) -> Self {
1208        Self::String(value)
1209    }
1210}
1211
1212impl From<&str> for ElicitationContentValue {
1213    fn from(value: &str) -> Self {
1214        Self::String(value.to_string())
1215    }
1216}
1217
1218impl From<i64> for ElicitationContentValue {
1219    fn from(value: i64) -> Self {
1220        Self::Integer(value)
1221    }
1222}
1223
1224impl From<i32> for ElicitationContentValue {
1225    fn from(value: i32) -> Self {
1226        Self::Integer(i64::from(value))
1227    }
1228}
1229
1230impl From<f64> for ElicitationContentValue {
1231    fn from(value: f64) -> Self {
1232        Self::Number(value)
1233    }
1234}
1235
1236impl From<bool> for ElicitationContentValue {
1237    fn from(value: bool) -> Self {
1238        Self::Boolean(value)
1239    }
1240}
1241
1242impl From<Vec<String>> for ElicitationContentValue {
1243    fn from(value: Vec<String>) -> Self {
1244        Self::StringArray(value)
1245    }
1246}
1247
1248impl From<Vec<&str>> for ElicitationContentValue {
1249    fn from(value: Vec<&str>) -> Self {
1250        Self::StringArray(value.into_iter().map(str::to_string).collect())
1251    }
1252}
1253
1254impl Default for ElicitationAcceptAction {
1255    fn default() -> Self {
1256        Self::new()
1257    }
1258}
1259
1260/// **UNSTABLE**
1261///
1262/// This capability is not part of the spec yet, and may be removed or changed at any point.
1263///
1264/// Notification sent by the agent when a URL-based elicitation is complete.
1265#[skip_serializing_none]
1266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1267#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
1268#[serde(rename_all = "camelCase")]
1269#[non_exhaustive]
1270pub struct CompleteElicitationNotification {
1271    /// The ID of the elicitation that completed.
1272    pub elicitation_id: ElicitationId,
1273    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1274    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1275    /// these keys.
1276    ///
1277    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1278    #[serde(rename = "_meta")]
1279    pub meta: Option<Meta>,
1280}
1281
1282impl CompleteElicitationNotification {
1283    #[must_use]
1284    pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
1285        Self {
1286            elicitation_id: elicitation_id.into(),
1287            meta: None,
1288        }
1289    }
1290
1291    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1292    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1293    /// these keys.
1294    ///
1295    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1296    #[must_use]
1297    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1298        self.meta = meta.into_option();
1299        self
1300    }
1301}
1302
1303/// **UNSTABLE**
1304///
1305/// This capability is not part of the spec yet, and may be removed or changed at any point.
1306///
1307/// Data payload for the `UrlElicitationRequired` error, describing the URL elicitations
1308/// the user must complete.
1309#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1310#[serde(rename_all = "camelCase")]
1311#[non_exhaustive]
1312pub struct UrlElicitationRequiredData {
1313    /// The URL elicitations the user must complete.
1314    pub elicitations: Vec<UrlElicitationRequiredItem>,
1315}
1316
1317impl UrlElicitationRequiredData {
1318    #[must_use]
1319    pub fn new(elicitations: Vec<UrlElicitationRequiredItem>) -> Self {
1320        Self { elicitations }
1321    }
1322}
1323
1324/// **UNSTABLE**
1325///
1326/// This capability is not part of the spec yet, and may be removed or changed at any point.
1327///
1328/// A single URL elicitation item within the `UrlElicitationRequired` error data.
1329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1330#[serde(rename_all = "camelCase")]
1331#[non_exhaustive]
1332pub struct UrlElicitationRequiredItem {
1333    /// The elicitation mode (always `"url"` for this item type).
1334    pub mode: ElicitationUrlOnlyMode,
1335    /// The unique identifier for this elicitation.
1336    pub elicitation_id: ElicitationId,
1337    /// The URL the user should be directed to.
1338    #[schemars(extend("format" = "uri"))]
1339    pub url: String,
1340    /// A human-readable message describing what input is needed.
1341    pub message: String,
1342}
1343
1344/// Type discriminator for URL-only elicitation error items.
1345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1346#[serde(rename_all = "snake_case")]
1347#[non_exhaustive]
1348pub enum ElicitationUrlOnlyMode {
1349    /// URL elicitation mode.
1350    #[default]
1351    Url,
1352}
1353
1354impl UrlElicitationRequiredItem {
1355    #[must_use]
1356    pub fn new(
1357        elicitation_id: impl Into<ElicitationId>,
1358        url: impl Into<String>,
1359        message: impl Into<String>,
1360    ) -> Self {
1361        Self {
1362            mode: ElicitationUrlOnlyMode::Url,
1363            elicitation_id: elicitation_id.into(),
1364            url: url.into(),
1365            message: message.into(),
1366        }
1367    }
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372    use super::*;
1373    use serde_json::json;
1374
1375    #[test]
1376    fn form_mode_request_serialization() {
1377        let schema = ElicitationSchema::new().string("name", true);
1378        let req = CreateElicitationRequest::new(
1379            ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
1380            "Please enter your name",
1381        );
1382
1383        let json = serde_json::to_value(&req).unwrap();
1384        assert_eq!(json["sessionId"], "sess_1");
1385        assert!(json.get("toolCallId").is_none());
1386        assert_eq!(json["mode"], "form");
1387        assert_eq!(json["message"], "Please enter your name");
1388        assert!(json["requestedSchema"].is_object());
1389        assert_eq!(json["requestedSchema"]["type"], "object");
1390        assert_eq!(
1391            json["requestedSchema"]["properties"]["name"]["type"],
1392            "string"
1393        );
1394
1395        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1396        assert_eq!(
1397            *roundtripped.scope(),
1398            ElicitationSessionScope::new("sess_1").into()
1399        );
1400        assert_eq!(roundtripped.message, "Please enter your name");
1401        assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
1402    }
1403
1404    #[test]
1405    fn url_mode_request_serialization() {
1406        let req = CreateElicitationRequest::new(
1407            ElicitationUrlMode::new(
1408                ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
1409                "elic_1",
1410                "https://example.com/auth",
1411            ),
1412            "Please authenticate",
1413        );
1414
1415        let json = serde_json::to_value(&req).unwrap();
1416        assert_eq!(json["sessionId"], "sess_2");
1417        assert_eq!(json["toolCallId"], "tc_1");
1418        assert_eq!(json["mode"], "url");
1419        assert_eq!(json["elicitationId"], "elic_1");
1420        assert_eq!(json["url"], "https://example.com/auth");
1421        assert_eq!(json["message"], "Please authenticate");
1422
1423        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1424        assert_eq!(
1425            *roundtripped.scope(),
1426            ElicitationSessionScope::new("sess_2")
1427                .tool_call_id("tc_1")
1428                .into()
1429        );
1430        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1431    }
1432
1433    #[test]
1434    fn response_accept_serialization() {
1435        let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
1436            ElicitationAcceptAction::new().content(BTreeMap::from([(
1437                "name".to_string(),
1438                ElicitationContentValue::from("Alice"),
1439            )])),
1440        ));
1441
1442        let json = serde_json::to_value(&resp).unwrap();
1443        assert_eq!(json["action"], "accept");
1444        assert_eq!(json["content"]["name"], "Alice");
1445
1446        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1447        assert!(matches!(
1448            roundtripped.action,
1449            ElicitationAction::Accept(ElicitationAcceptAction {
1450                content: Some(_),
1451                ..
1452            })
1453        ));
1454    }
1455
1456    #[test]
1457    fn response_decline_serialization() {
1458        let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
1459
1460        let json = serde_json::to_value(&resp).unwrap();
1461        assert_eq!(json["action"], "decline");
1462
1463        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1464        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1465    }
1466
1467    #[test]
1468    fn response_cancel_serialization() {
1469        let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
1470
1471        let json = serde_json::to_value(&resp).unwrap();
1472        assert_eq!(json["action"], "cancel");
1473
1474        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1475        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1476    }
1477
1478    #[test]
1479    fn url_mode_request_scope_serialization() {
1480        let req = CreateElicitationRequest::new(
1481            ElicitationUrlMode::new(
1482                ElicitationRequestScope::new(RequestId::Number(42)),
1483                "elic_2",
1484                "https://example.com/setup",
1485            ),
1486            "Please complete setup",
1487        );
1488
1489        let json = serde_json::to_value(&req).unwrap();
1490        assert_eq!(json["requestId"], 42);
1491        assert!(json.get("sessionId").is_none());
1492        assert_eq!(json["mode"], "url");
1493        assert_eq!(json["elicitationId"], "elic_2");
1494        assert_eq!(json["url"], "https://example.com/setup");
1495        assert_eq!(json["message"], "Please complete setup");
1496
1497        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1498        assert_eq!(
1499            *roundtripped.scope(),
1500            ElicitationRequestScope::new(RequestId::Number(42)).into()
1501        );
1502        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1503    }
1504
1505    #[test]
1506    fn request_scope_request_serialization() {
1507        let req = CreateElicitationRequest::new(
1508            ElicitationFormMode::new(
1509                ElicitationRequestScope::new(RequestId::Number(99)),
1510                ElicitationSchema::new().string("workspace", true),
1511            ),
1512            "Enter workspace name",
1513        );
1514
1515        let json = serde_json::to_value(&req).unwrap();
1516        assert_eq!(json["requestId"], 99);
1517        assert!(json.get("sessionId").is_none());
1518
1519        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1520        assert_eq!(
1521            *roundtripped.scope(),
1522            ElicitationRequestScope::new(RequestId::Number(99)).into()
1523        );
1524    }
1525
1526    /// These tests verify that serialization through `ClientResponse` produces the
1527    /// correct flattened wire format and round-trips back via the concrete
1528    /// `CreateElicitationResponse` type.
1529    #[test]
1530    fn client_response_serialization_accept() {
1531        use crate::v2::ClientResponse;
1532
1533        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1534            ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
1535                "name".to_string(),
1536                ElicitationContentValue::from("Alice"),
1537            )]))),
1538        ));
1539        let json = serde_json::to_value(&resp).unwrap();
1540        assert_eq!(json["action"], "accept");
1541        assert_eq!(json["content"]["name"], "Alice");
1542
1543        // Round-trip back through the concrete type
1544        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1545        assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
1546    }
1547
1548    #[test]
1549    fn client_response_serialization_decline() {
1550        use crate::v2::ClientResponse;
1551
1552        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1553            ElicitationAction::Decline,
1554        ));
1555        let json = serde_json::to_value(&resp).unwrap();
1556        assert_eq!(json["action"], "decline");
1557
1558        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1559        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1560    }
1561
1562    #[test]
1563    fn client_response_serialization_cancel() {
1564        use crate::v2::ClientResponse;
1565
1566        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1567            ElicitationAction::Cancel,
1568        ));
1569        let json = serde_json::to_value(&resp).unwrap();
1570        assert_eq!(json["action"], "cancel");
1571
1572        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1573        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1574    }
1575
1576    /// Guard against serde regressions with the `flatten` + internally-tagged combination.
1577    /// Extra fields in the JSON must not cause deserialization failures.
1578    #[test]
1579    fn request_tolerates_extra_fields() {
1580        let json = json!({
1581            "sessionId": "sess_1",
1582            "mode": "form",
1583            "message": "Enter your name",
1584            "requestedSchema": {
1585                "type": "object",
1586                "properties": {
1587                    "name": { "type": "string", "title": "Name" }
1588                },
1589                "required": ["name"]
1590            },
1591            "unknownStringField": "hello",
1592            "unknownNumberField": 42
1593        });
1594
1595        let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1596        assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
1597        assert_eq!(req.message, "Enter your name");
1598        assert!(matches!(req.mode, ElicitationMode::Form(_)));
1599    }
1600
1601    #[test]
1602    fn completion_notification_serialization() {
1603        let notif = CompleteElicitationNotification::new("elic_1");
1604
1605        let json = serde_json::to_value(&notif).unwrap();
1606        assert_eq!(json["elicitationId"], "elic_1");
1607
1608        let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
1609        assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
1610    }
1611
1612    #[test]
1613    fn capabilities_form_only() {
1614        let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
1615
1616        let json = serde_json::to_value(&caps).unwrap();
1617        assert!(json["form"].is_object());
1618        assert!(json.get("url").is_none());
1619
1620        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1621        assert!(roundtripped.form.is_some());
1622        assert!(roundtripped.url.is_none());
1623    }
1624
1625    #[test]
1626    fn capabilities_url_only() {
1627        let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
1628
1629        let json = serde_json::to_value(&caps).unwrap();
1630        assert!(json.get("form").is_none());
1631        assert!(json["url"].is_object());
1632
1633        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1634        assert!(roundtripped.form.is_none());
1635        assert!(roundtripped.url.is_some());
1636    }
1637
1638    #[test]
1639    fn capabilities_both() {
1640        let caps = ElicitationCapabilities::new()
1641            .form(ElicitationFormCapabilities::new())
1642            .url(ElicitationUrlCapabilities::new());
1643
1644        let json = serde_json::to_value(&caps).unwrap();
1645        assert!(json["form"].is_object());
1646        assert!(json["url"].is_object());
1647
1648        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1649        assert!(roundtripped.form.is_some());
1650        assert!(roundtripped.url.is_some());
1651    }
1652
1653    #[test]
1654    fn url_elicitation_required_data_serialization() {
1655        let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
1656            "elic_1",
1657            "https://example.com/auth",
1658            "Please authenticate",
1659        )]);
1660
1661        let json = serde_json::to_value(&data).unwrap();
1662        assert_eq!(json["elicitations"][0]["mode"], "url");
1663        assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
1664        assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
1665
1666        let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
1667        assert_eq!(roundtripped.elicitations.len(), 1);
1668        assert_eq!(
1669            roundtripped.elicitations[0].mode,
1670            ElicitationUrlOnlyMode::Url
1671        );
1672    }
1673
1674    #[test]
1675    fn schema_default_sets_object_type() {
1676        let schema = ElicitationSchema::default();
1677
1678        assert_eq!(schema.type_, ElicitationSchemaType::Object);
1679        assert!(schema.properties.is_empty());
1680
1681        let json = serde_json::to_value(&schema).unwrap();
1682        assert_eq!(json["type"], "object");
1683    }
1684
1685    #[test]
1686    fn schema_builder_serialization() {
1687        let schema = ElicitationSchema::new()
1688            .string("name", true)
1689            .email("email", true)
1690            .integer("age", 0, 150, true)
1691            .boolean("newsletter", false)
1692            .description("User registration");
1693
1694        let json = serde_json::to_value(&schema).unwrap();
1695        assert_eq!(json["type"], "object");
1696        assert_eq!(json["description"], "User registration");
1697        assert_eq!(json["properties"]["name"]["type"], "string");
1698        assert_eq!(json["properties"]["email"]["type"], "string");
1699        assert_eq!(json["properties"]["email"]["format"], "email");
1700        assert_eq!(json["properties"]["age"]["type"], "integer");
1701        assert_eq!(json["properties"]["age"]["minimum"], 0);
1702        assert_eq!(json["properties"]["age"]["maximum"], 150);
1703        assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
1704
1705        let required = json["required"].as_array().unwrap();
1706        assert!(required.contains(&json!("name")));
1707        assert!(required.contains(&json!("email")));
1708        assert!(required.contains(&json!("age")));
1709        assert!(!required.contains(&json!("newsletter")));
1710
1711        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1712        assert_eq!(roundtripped.properties.len(), 4);
1713        assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
1714    }
1715
1716    #[test]
1717    fn schema_string_enum_serialization() {
1718        let schema = ElicitationSchema::new().property(
1719            "color",
1720            StringPropertySchema::new().enum_values(vec![
1721                "red".into(),
1722                "green".into(),
1723                "blue".into(),
1724            ]),
1725            true,
1726        );
1727
1728        let json = serde_json::to_value(&schema).unwrap();
1729        assert_eq!(json["properties"]["color"]["type"], "string");
1730        let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
1731        assert_eq!(enum_vals.len(), 3);
1732
1733        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1734        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
1735        {
1736            assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
1737        } else {
1738            panic!("expected String variant");
1739        }
1740    }
1741
1742    #[test]
1743    fn schema_multi_select_serialization() {
1744        let schema = ElicitationSchema::new().property(
1745            "colors",
1746            MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
1747                .min_items(1)
1748                .max_items(3),
1749            false,
1750        );
1751
1752        let json = serde_json::to_value(&schema).unwrap();
1753        assert_eq!(json["properties"]["colors"]["type"], "array");
1754        assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
1755        assert_eq!(json["properties"]["colors"]["minItems"], 1);
1756        assert_eq!(json["properties"]["colors"]["maxItems"], 3);
1757
1758        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1759        assert!(matches!(
1760            roundtripped.properties.get("colors").unwrap(),
1761            ElicitationPropertySchema::Array(_)
1762        ));
1763    }
1764
1765    #[test]
1766    fn schema_titled_enum_serialization() {
1767        let schema = ElicitationSchema::new().property(
1768            "country",
1769            StringPropertySchema::new().one_of(vec![
1770                EnumOption::new("us", "United States"),
1771                EnumOption::new("uk", "United Kingdom"),
1772            ]),
1773            true,
1774        );
1775
1776        let json = serde_json::to_value(&schema).unwrap();
1777        assert_eq!(json["properties"]["country"]["type"], "string");
1778        let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
1779        assert_eq!(one_of.len(), 2);
1780        assert_eq!(one_of[0]["const"], "us");
1781        assert_eq!(one_of[0]["title"], "United States");
1782
1783        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1784        if let ElicitationPropertySchema::String(s) =
1785            roundtripped.properties.get("country").unwrap()
1786        {
1787            assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
1788        } else {
1789            panic!("expected String variant");
1790        }
1791    }
1792
1793    #[test]
1794    fn schema_number_property_serialization() {
1795        let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
1796
1797        let json = serde_json::to_value(&schema).unwrap();
1798        assert_eq!(json["properties"]["rating"]["type"], "number");
1799        assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
1800        assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
1801
1802        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1803        if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
1804        {
1805            assert_eq!(n.minimum, Some(0.0));
1806            assert_eq!(n.maximum, Some(5.0));
1807        } else {
1808            panic!("expected Number variant");
1809        }
1810    }
1811
1812    #[test]
1813    fn schema_string_format_serialization() {
1814        let schema = ElicitationSchema::new()
1815            .uri("website", true)
1816            .date("birthday", true)
1817            .date_time("updated_at", false);
1818
1819        let json = serde_json::to_value(&schema).unwrap();
1820        assert_eq!(json["properties"]["website"]["type"], "string");
1821        assert_eq!(json["properties"]["website"]["format"], "uri");
1822        assert_eq!(json["properties"]["birthday"]["type"], "string");
1823        assert_eq!(json["properties"]["birthday"]["format"], "date");
1824        assert_eq!(json["properties"]["updated_at"]["type"], "string");
1825        assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
1826
1827        let required = json["required"].as_array().unwrap();
1828        assert!(required.contains(&json!("website")));
1829        assert!(required.contains(&json!("birthday")));
1830        assert!(!required.contains(&json!("updated_at")));
1831    }
1832
1833    #[test]
1834    fn schema_string_pattern_serialization() {
1835        let schema = ElicitationSchema::new().property(
1836            "name",
1837            StringPropertySchema::new()
1838                .min_length(1)
1839                .max_length(64)
1840                .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
1841            true,
1842        );
1843
1844        let json = serde_json::to_value(&schema).unwrap();
1845        assert_eq!(json["properties"]["name"]["type"], "string");
1846        assert_eq!(
1847            json["properties"]["name"]["pattern"],
1848            "^[a-zA-Z_][a-zA-Z0-9_]*$"
1849        );
1850
1851        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1852        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
1853            assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
1854        } else {
1855            panic!("expected String variant");
1856        }
1857    }
1858
1859    #[test]
1860    fn schema_property_updates_required_state() {
1861        let schema = ElicitationSchema::new()
1862            .string("name", true)
1863            .email("name", false);
1864
1865        let json = serde_json::to_value(&schema).unwrap();
1866        assert!(json.get("required").is_none());
1867        assert_eq!(json["properties"]["name"]["format"], "email");
1868    }
1869
1870    #[test]
1871    fn schema_rejects_invalid_object_type() {
1872        let err = serde_json::from_value::<ElicitationSchema>(json!({
1873            "type": "array",
1874            "properties": {
1875                "name": {
1876                    "type": "string"
1877                }
1878            }
1879        }))
1880        .unwrap_err();
1881
1882        assert!(err.to_string().contains("unknown variant"));
1883    }
1884
1885    #[test]
1886    fn titled_multi_select_items_reject_one_of() {
1887        let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
1888            "oneOf": [
1889                {
1890                    "const": "red",
1891                    "title": "Red"
1892                }
1893            ]
1894        }))
1895        .unwrap_err();
1896
1897        assert!(err.to_string().contains("missing field `anyOf`"));
1898    }
1899
1900    #[test]
1901    fn response_accept_rejects_non_object_content() {
1902        let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1903            "action": "accept",
1904            "content": "Alice"
1905        }))
1906        .unwrap_err();
1907
1908        assert!(err.to_string().contains("invalid type"));
1909    }
1910
1911    #[test]
1912    fn response_accept_rejects_nested_object_content() {
1913        let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1914            "action": "accept",
1915            "content": {
1916                "profile": {
1917                    "name": "Alice"
1918                }
1919            }
1920        }))
1921        .unwrap_err();
1922
1923        assert!(err.to_string().contains("data did not match any variant"));
1924    }
1925
1926    #[test]
1927    fn response_accept_allows_primitive_and_string_array_content() {
1928        let response = CreateElicitationResponse::new(ElicitationAction::Accept(
1929            ElicitationAcceptAction::new().content(BTreeMap::from([
1930                ("name".to_string(), ElicitationContentValue::from("Alice")),
1931                ("age".to_string(), ElicitationContentValue::from(30_i32)),
1932                ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
1933                (
1934                    "subscribed".to_string(),
1935                    ElicitationContentValue::from(true),
1936                ),
1937                (
1938                    "tags".to_string(),
1939                    ElicitationContentValue::from(vec!["rust", "acp"]),
1940                ),
1941            ])),
1942        ));
1943
1944        let json = serde_json::to_value(&response).unwrap();
1945        assert_eq!(json["action"], "accept");
1946        assert_eq!(json["content"]["name"], "Alice");
1947        assert_eq!(json["content"]["age"], 30);
1948        assert_eq!(json["content"]["score"], 9.5);
1949        assert_eq!(json["content"]["subscribed"], true);
1950        assert_eq!(json["content"]["tags"][0], "rust");
1951        assert_eq!(json["content"]["tags"][1], "acp");
1952    }
1953}