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, Schema};
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, VecSkipError, 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;
20use crate::SkipListener;
21
22/// **UNSTABLE**
23///
24/// This capability is not part of the spec yet, and may be removed or changed at any point.
25///
26/// Unique identifier for an elicitation.
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
28#[serde(transparent)]
29#[from(Arc<str>, String, &'static str)]
30#[non_exhaustive]
31pub struct ElicitationId(pub Arc<str>);
32
33impl ElicitationId {
34    /// Wraps a protocol string as a typed [`ElicitationId`].
35    #[must_use]
36    pub fn new(id: impl Into<Arc<str>>) -> Self {
37        Self(id.into())
38    }
39}
40
41/// String format types for string properties in elicitation schemas.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43#[serde(rename_all = "kebab-case")]
44#[non_exhaustive]
45pub enum StringFormat {
46    /// Email address format.
47    Email,
48    /// URI format.
49    Uri,
50    /// Date format (YYYY-MM-DD).
51    Date,
52    /// Date-time format (ISO 8601).
53    DateTime,
54    /// Custom or future string format.
55    ///
56    /// Unknown formats are preserved. Implementations that do not understand a
57    /// format should treat it as an annotation rather than rejecting the schema.
58    #[serde(untagged)]
59    Other(String),
60}
61
62/// Type discriminator for elicitation schemas.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum ElicitationSchemaType {
67    /// Object schema type.
68    #[default]
69    Object,
70}
71
72/// A titled enum option with a const value and human-readable title.
73#[serde_as]
74#[skip_serializing_none]
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76#[non_exhaustive]
77pub struct EnumOption {
78    /// The constant value for this option.
79    #[serde(rename = "const")]
80    pub value: String,
81    /// Human-readable title for this option.
82    pub title: String,
83    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
84    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
85    /// these keys.
86    ///
87    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
88    #[serde_as(deserialize_as = "DefaultOnError")]
89    #[schemars(extend("x-deserialize-default-on-error" = true))]
90    #[serde(default)]
91    #[serde(rename = "_meta")]
92    pub meta: Option<Meta>,
93}
94
95impl EnumOption {
96    /// Create a new enum option.
97    #[must_use]
98    pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
99        Self {
100            value: value.into(),
101            title: title.into(),
102            meta: None,
103        }
104    }
105
106    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
107    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
108    /// these keys.
109    ///
110    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
111    #[must_use]
112    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
113        self.meta = meta.into_option();
114        self
115    }
116}
117
118/// Schema for string properties in an elicitation form.
119///
120/// When `enum` or `oneOf` is set, this represents a single-select enum
121/// with `"type": "string"`.
122#[serde_as]
123#[skip_serializing_none]
124#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
125#[serde(rename_all = "camelCase")]
126#[non_exhaustive]
127pub struct StringPropertySchema {
128    /// Optional title for the property.
129    #[serde_as(deserialize_as = "DefaultOnError")]
130    #[schemars(extend("x-deserialize-default-on-error" = true))]
131    #[serde(default)]
132    pub title: Option<String>,
133    /// Human-readable description.
134    #[serde_as(deserialize_as = "DefaultOnError")]
135    #[schemars(extend("x-deserialize-default-on-error" = true))]
136    #[serde(default)]
137    pub description: Option<String>,
138    /// Minimum string length.
139    #[serde(default)]
140    pub min_length: Option<u32>,
141    /// Maximum string length.
142    #[serde(default)]
143    pub max_length: Option<u32>,
144    /// Pattern the string must match.
145    #[schemars(extend("format" = "regex"))]
146    #[serde(default)]
147    pub pattern: Option<String>,
148    /// String format.
149    #[serde(default)]
150    pub format: Option<StringFormat>,
151    /// Default value.
152    #[serde_as(deserialize_as = "DefaultOnError")]
153    #[schemars(extend("x-deserialize-default-on-error" = true))]
154    #[serde(default)]
155    pub default: Option<String>,
156    /// Enum values for untitled single-select enums.
157    /// Must contain at least one value when present.
158    #[schemars(length(min = 1))]
159    #[serde(default)]
160    #[serde(rename = "enum")]
161    pub enum_values: Option<Vec<String>>,
162    /// Titled enum options for titled single-select enums.
163    /// Must contain at least one option when present.
164    #[schemars(length(min = 1))]
165    #[serde(default)]
166    #[serde(rename = "oneOf")]
167    pub one_of: Option<Vec<EnumOption>>,
168    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
169    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
170    /// these keys.
171    ///
172    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
173    #[serde_as(deserialize_as = "DefaultOnError")]
174    #[schemars(extend("x-deserialize-default-on-error" = true))]
175    #[serde(default)]
176    #[serde(rename = "_meta")]
177    pub meta: Option<Meta>,
178}
179
180impl StringPropertySchema {
181    /// Create a new string property schema.
182    #[must_use]
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Create an email string property schema.
188    #[must_use]
189    pub fn email() -> Self {
190        Self {
191            format: Some(StringFormat::Email),
192            ..Default::default()
193        }
194    }
195
196    /// Create a URI string property schema.
197    #[must_use]
198    pub fn uri() -> Self {
199        Self {
200            format: Some(StringFormat::Uri),
201            ..Default::default()
202        }
203    }
204
205    /// Create a date string property schema.
206    #[must_use]
207    pub fn date() -> Self {
208        Self {
209            format: Some(StringFormat::Date),
210            ..Default::default()
211        }
212    }
213
214    /// Create a date-time string property schema.
215    #[must_use]
216    pub fn date_time() -> Self {
217        Self {
218            format: Some(StringFormat::DateTime),
219            ..Default::default()
220        }
221    }
222
223    /// Optional title for the property.
224    #[must_use]
225    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
226        self.title = title.into_option();
227        self
228    }
229
230    /// Human-readable description.
231    #[must_use]
232    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
233        self.description = description.into_option();
234        self
235    }
236
237    /// Minimum string length.
238    #[must_use]
239    pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
240        self.min_length = min_length.into_option();
241        self
242    }
243
244    /// Maximum string length.
245    #[must_use]
246    pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
247        self.max_length = max_length.into_option();
248        self
249    }
250
251    /// Pattern the string must match.
252    #[must_use]
253    pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
254        self.pattern = pattern.into_option();
255        self
256    }
257
258    /// String format.
259    #[must_use]
260    pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
261        self.format = format.into_option();
262        self
263    }
264
265    /// Default value.
266    #[must_use]
267    pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
268        self.default = default.into_option();
269        self
270    }
271
272    /// Enum values for untitled single-select enums.
273    #[must_use]
274    pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
275        self.enum_values = enum_values.into_option();
276        self
277    }
278
279    /// Titled enum options for titled single-select enums.
280    #[must_use]
281    pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
282        self.one_of = one_of.into_option();
283        self
284    }
285
286    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
287    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
288    /// these keys.
289    ///
290    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
291    #[must_use]
292    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
293        self.meta = meta.into_option();
294        self
295    }
296}
297
298/// Schema for number (floating-point) properties in an elicitation form.
299#[serde_as]
300#[skip_serializing_none]
301#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
302#[serde(rename_all = "camelCase")]
303#[non_exhaustive]
304pub struct NumberPropertySchema {
305    /// Optional title for the property.
306    #[serde_as(deserialize_as = "DefaultOnError")]
307    #[schemars(extend("x-deserialize-default-on-error" = true))]
308    #[serde(default)]
309    pub title: Option<String>,
310    /// Human-readable description.
311    #[serde_as(deserialize_as = "DefaultOnError")]
312    #[schemars(extend("x-deserialize-default-on-error" = true))]
313    #[serde(default)]
314    pub description: Option<String>,
315    /// Minimum value (inclusive).
316    #[serde(default)]
317    pub minimum: Option<f64>,
318    /// Maximum value (inclusive).
319    #[serde(default)]
320    pub maximum: Option<f64>,
321    /// Default value.
322    #[serde_as(deserialize_as = "DefaultOnError")]
323    #[schemars(extend("x-deserialize-default-on-error" = true))]
324    #[serde(default)]
325    pub default: Option<f64>,
326    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
327    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
328    /// these keys.
329    ///
330    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
331    #[serde_as(deserialize_as = "DefaultOnError")]
332    #[schemars(extend("x-deserialize-default-on-error" = true))]
333    #[serde(default)]
334    #[serde(rename = "_meta")]
335    pub meta: Option<Meta>,
336}
337
338impl NumberPropertySchema {
339    /// Create a new number property schema.
340    #[must_use]
341    pub fn new() -> Self {
342        Self::default()
343    }
344
345    /// Optional title for the property.
346    #[must_use]
347    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
348        self.title = title.into_option();
349        self
350    }
351
352    /// Human-readable description.
353    #[must_use]
354    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
355        self.description = description.into_option();
356        self
357    }
358
359    /// Minimum value (inclusive).
360    #[must_use]
361    pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
362        self.minimum = minimum.into_option();
363        self
364    }
365
366    /// Maximum value (inclusive).
367    #[must_use]
368    pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
369        self.maximum = maximum.into_option();
370        self
371    }
372
373    /// Default value.
374    #[must_use]
375    pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
376        self.default = default.into_option();
377        self
378    }
379
380    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
381    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
382    /// these keys.
383    ///
384    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
385    #[must_use]
386    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
387        self.meta = meta.into_option();
388        self
389    }
390}
391
392/// Schema for integer properties in an elicitation form.
393#[serde_as]
394#[skip_serializing_none]
395#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
396#[serde(rename_all = "camelCase")]
397#[non_exhaustive]
398pub struct IntegerPropertySchema {
399    /// Optional title for the property.
400    #[serde_as(deserialize_as = "DefaultOnError")]
401    #[schemars(extend("x-deserialize-default-on-error" = true))]
402    #[serde(default)]
403    pub title: Option<String>,
404    /// Human-readable description.
405    #[serde_as(deserialize_as = "DefaultOnError")]
406    #[schemars(extend("x-deserialize-default-on-error" = true))]
407    #[serde(default)]
408    pub description: Option<String>,
409    /// Minimum value (inclusive).
410    #[serde(default)]
411    pub minimum: Option<i64>,
412    /// Maximum value (inclusive).
413    #[serde(default)]
414    pub maximum: Option<i64>,
415    /// Default value.
416    #[serde_as(deserialize_as = "DefaultOnError")]
417    #[schemars(extend("x-deserialize-default-on-error" = true))]
418    #[serde(default)]
419    pub default: Option<i64>,
420    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
421    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
422    /// these keys.
423    ///
424    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
425    #[serde_as(deserialize_as = "DefaultOnError")]
426    #[schemars(extend("x-deserialize-default-on-error" = true))]
427    #[serde(default)]
428    #[serde(rename = "_meta")]
429    pub meta: Option<Meta>,
430}
431
432impl IntegerPropertySchema {
433    /// Create a new integer property schema.
434    #[must_use]
435    pub fn new() -> Self {
436        Self::default()
437    }
438
439    /// Optional title for the property.
440    #[must_use]
441    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
442        self.title = title.into_option();
443        self
444    }
445
446    /// Human-readable description.
447    #[must_use]
448    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
449        self.description = description.into_option();
450        self
451    }
452
453    /// Minimum value (inclusive).
454    #[must_use]
455    pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
456        self.minimum = minimum.into_option();
457        self
458    }
459
460    /// Maximum value (inclusive).
461    #[must_use]
462    pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
463        self.maximum = maximum.into_option();
464        self
465    }
466
467    /// Default value.
468    #[must_use]
469    pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
470        self.default = default.into_option();
471        self
472    }
473
474    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
475    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
476    /// these keys.
477    ///
478    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
479    #[must_use]
480    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
481        self.meta = meta.into_option();
482        self
483    }
484}
485
486/// Schema for boolean properties in an elicitation form.
487#[serde_as]
488#[skip_serializing_none]
489#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
490#[serde(rename_all = "camelCase")]
491#[non_exhaustive]
492pub struct BooleanPropertySchema {
493    /// Optional title for the property.
494    #[serde_as(deserialize_as = "DefaultOnError")]
495    #[schemars(extend("x-deserialize-default-on-error" = true))]
496    #[serde(default)]
497    pub title: Option<String>,
498    /// Human-readable description.
499    #[serde_as(deserialize_as = "DefaultOnError")]
500    #[schemars(extend("x-deserialize-default-on-error" = true))]
501    #[serde(default)]
502    pub description: Option<String>,
503    /// Default value.
504    #[serde_as(deserialize_as = "DefaultOnError")]
505    #[schemars(extend("x-deserialize-default-on-error" = true))]
506    #[serde(default)]
507    pub default: Option<bool>,
508    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
509    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
510    /// these keys.
511    ///
512    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
513    #[serde_as(deserialize_as = "DefaultOnError")]
514    #[schemars(extend("x-deserialize-default-on-error" = true))]
515    #[serde(default)]
516    #[serde(rename = "_meta")]
517    pub meta: Option<Meta>,
518}
519
520impl BooleanPropertySchema {
521    /// Create a new boolean property schema.
522    #[must_use]
523    pub fn new() -> Self {
524        Self::default()
525    }
526
527    /// Optional title for the property.
528    #[must_use]
529    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
530        self.title = title.into_option();
531        self
532    }
533
534    /// Human-readable description.
535    #[must_use]
536    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
537        self.description = description.into_option();
538        self
539    }
540
541    /// Default value.
542    #[must_use]
543    pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
544        self.default = default.into_option();
545        self
546    }
547
548    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
549    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
550    /// these keys.
551    ///
552    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
553    #[must_use]
554    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
555        self.meta = meta.into_option();
556        self
557    }
558}
559
560/// String item schema for multi-select enum properties.
561#[serde_as]
562#[skip_serializing_none]
563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
564#[non_exhaustive]
565pub struct StringMultiSelectItems {
566    /// Allowed enum values. Must contain at least one value.
567    #[schemars(length(min = 1))]
568    #[serde(rename = "enum")]
569    pub values: Vec<String>,
570    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
571    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
572    /// these keys.
573    ///
574    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
575    #[serde_as(deserialize_as = "DefaultOnError")]
576    #[schemars(extend("x-deserialize-default-on-error" = true))]
577    #[serde(default)]
578    #[serde(rename = "_meta")]
579    pub meta: Option<Meta>,
580}
581
582impl StringMultiSelectItems {
583    /// Create new string multi-select items.
584    #[must_use]
585    pub fn new(values: Vec<String>) -> Self {
586        Self { values, meta: None }
587    }
588
589    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
590    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
591    /// these keys.
592    ///
593    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
594    #[must_use]
595    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
596        self.meta = meta.into_option();
597        self
598    }
599}
600
601/// Items definition for titled multi-select enum properties.
602#[serde_as]
603#[skip_serializing_none]
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
605#[non_exhaustive]
606pub struct TitledMultiSelectItems {
607    /// Titled enum options. Must contain at least one option.
608    #[schemars(length(min = 1))]
609    #[serde(rename = "anyOf")]
610    pub options: Vec<EnumOption>,
611    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
612    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
613    /// these keys.
614    ///
615    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
616    #[serde_as(deserialize_as = "DefaultOnError")]
617    #[schemars(extend("x-deserialize-default-on-error" = true))]
618    #[serde(default)]
619    #[serde(rename = "_meta")]
620    pub meta: Option<Meta>,
621}
622
623impl TitledMultiSelectItems {
624    /// Create new titled multi-select items.
625    #[must_use]
626    pub fn new(options: Vec<EnumOption>) -> Self {
627        Self {
628            options,
629            meta: None,
630        }
631    }
632
633    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
634    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
635    /// these keys.
636    ///
637    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
638    #[must_use]
639    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
640        self.meta = meta.into_option();
641        self
642    }
643}
644
645/// Custom or future typed item schema for multi-select properties.
646///
647/// This preserves unknown item `type` values and the rest of the `items`
648/// payload for clients that store, replay, proxy, or forward elicitation
649/// requests.
650#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
651#[schemars(inline)]
652#[schemars(transform = other_multi_select_items_schema)]
653#[serde(rename_all = "camelCase")]
654#[non_exhaustive]
655pub struct OtherMultiSelectItems {
656    /// Custom or future multi-select item type.
657    ///
658    /// Values beginning with `_` are reserved for implementation-specific
659    /// extensions. Unknown values that do not begin with `_` are reserved for
660    /// future ACP variants.
661    #[serde(rename = "type")]
662    pub type_: String,
663    /// Additional fields from the unknown item schema payload.
664    #[serde(flatten)]
665    pub fields: BTreeMap<String, serde_json::Value>,
666}
667
668impl OtherMultiSelectItems {
669    /// Builds [`OtherMultiSelectItems`] from an unknown discriminator and preserves the remaining extension fields.
670    #[must_use]
671    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
672        fields.remove("type");
673        Self {
674            type_: type_.into(),
675            fields,
676        }
677    }
678}
679
680impl<'de> Deserialize<'de> for OtherMultiSelectItems {
681    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
682    where
683        D: serde::Deserializer<'de>,
684    {
685        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
686        let type_ = fields
687            .remove("type")
688            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
689        let serde_json::Value::String(type_) = type_ else {
690            return Err(serde::de::Error::custom("`type` must be a string"));
691        };
692
693        if is_known_multi_select_item_type(&type_) {
694            return Err(serde::de::Error::custom(format!(
695                "known multi-select item type `{type_}` did not match its schema"
696            )));
697        }
698
699        Ok(Self { type_, fields })
700    }
701}
702
703const KNOWN_MULTI_SELECT_ITEM_TYPES: &[&str] = &["string"];
704
705fn is_known_multi_select_item_type(type_: &str) -> bool {
706    KNOWN_MULTI_SELECT_ITEM_TYPES.contains(&type_)
707}
708
709fn other_multi_select_items_schema(schema: &mut Schema) {
710    super::schema_util::reject_known_string_discriminators(
711        schema,
712        "type",
713        KNOWN_MULTI_SELECT_ITEM_TYPES,
714    );
715}
716
717/// Items for a multi-select (array) property schema.
718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
719#[serde(tag = "type", rename_all = "snake_case")]
720#[schemars(extend("discriminator" = {"propertyName": "type"}))]
721#[non_exhaustive]
722pub enum MultiSelectItems {
723    /// Multi-select string items with plain string values.
724    String(StringMultiSelectItems),
725    /// Custom or future typed multi-select items.
726    #[serde(untagged)]
727    Other(OtherMultiSelectItems),
728    /// Titled multi-select items with human-readable labels.
729    #[serde(untagged)]
730    Titled(TitledMultiSelectItems),
731}
732
733/// Schema for multi-select (array) properties in an elicitation form.
734#[serde_as]
735#[skip_serializing_none]
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
737#[serde(rename_all = "camelCase")]
738#[non_exhaustive]
739pub struct MultiSelectPropertySchema {
740    /// Optional title for the property.
741    #[serde_as(deserialize_as = "DefaultOnError")]
742    #[schemars(extend("x-deserialize-default-on-error" = true))]
743    #[serde(default)]
744    pub title: Option<String>,
745    /// Human-readable description.
746    #[serde_as(deserialize_as = "DefaultOnError")]
747    #[schemars(extend("x-deserialize-default-on-error" = true))]
748    #[serde(default)]
749    pub description: Option<String>,
750    /// Minimum number of items to select.
751    #[serde(default)]
752    pub min_items: Option<u64>,
753    /// Maximum number of items to select.
754    #[serde(default)]
755    pub max_items: Option<u64>,
756    /// The items definition describing allowed values.
757    pub items: MultiSelectItems,
758    /// Default selected values.
759    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
760    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
761    #[serde(default)]
762    pub default: Option<Vec<String>>,
763    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
764    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
765    /// these keys.
766    ///
767    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
768    #[serde_as(deserialize_as = "DefaultOnError")]
769    #[schemars(extend("x-deserialize-default-on-error" = true))]
770    #[serde(default)]
771    #[serde(rename = "_meta")]
772    pub meta: Option<Meta>,
773}
774
775impl MultiSelectPropertySchema {
776    /// Create a new untitled multi-select property schema.
777    #[must_use]
778    pub fn new(values: Vec<String>) -> Self {
779        Self {
780            title: None,
781            description: None,
782            min_items: None,
783            max_items: None,
784            items: MultiSelectItems::String(StringMultiSelectItems::new(values)),
785            default: None,
786            meta: None,
787        }
788    }
789
790    /// Create a new titled multi-select property schema.
791    #[must_use]
792    pub fn titled(options: Vec<EnumOption>) -> Self {
793        Self {
794            title: None,
795            description: None,
796            min_items: None,
797            max_items: None,
798            items: MultiSelectItems::Titled(TitledMultiSelectItems {
799                options,
800                meta: None,
801            }),
802            default: None,
803            meta: None,
804        }
805    }
806
807    /// Optional title for the property.
808    #[must_use]
809    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
810        self.title = title.into_option();
811        self
812    }
813
814    /// Human-readable description.
815    #[must_use]
816    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
817        self.description = description.into_option();
818        self
819    }
820
821    /// Minimum number of items to select.
822    #[must_use]
823    pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
824        self.min_items = min_items.into_option();
825        self
826    }
827
828    /// Maximum number of items to select.
829    #[must_use]
830    pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
831        self.max_items = max_items.into_option();
832        self
833    }
834
835    /// Default selected values.
836    #[must_use]
837    pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
838        self.default = default.into_option();
839        self
840    }
841
842    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
843    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
844    /// these keys.
845    ///
846    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
847    #[must_use]
848    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
849        self.meta = meta.into_option();
850        self
851    }
852}
853
854/// Property schema for elicitation form fields.
855///
856/// Each variant corresponds to a JSON Schema `"type"` value.
857/// Single-select enums use the `String` variant with `enum` or `oneOf` set.
858/// Multi-select enums use the `Array` variant.
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
860#[serde(tag = "type", rename_all = "snake_case")]
861#[schemars(extend("discriminator" = {"propertyName": "type"}))]
862#[non_exhaustive]
863pub enum ElicitationPropertySchema {
864    /// String property (or single-select enum when `enum`/`oneOf` is set).
865    String(StringPropertySchema),
866    /// Number (floating-point) property.
867    Number(NumberPropertySchema),
868    /// Integer property.
869    Integer(IntegerPropertySchema),
870    /// Boolean property.
871    Boolean(BooleanPropertySchema),
872    /// Multi-select array property.
873    Array(MultiSelectPropertySchema),
874    /// Custom or future elicitation property schema.
875    ///
876    /// Values beginning with `_` are reserved for implementation-specific
877    /// extensions. Unknown values that do not begin with `_` are reserved for
878    /// future ACP variants.
879    ///
880    /// Clients that do not understand this property schema type should preserve
881    /// the raw schema when storing, replaying, proxying, or forwarding
882    /// elicitation requests. They MUST NOT render it as a known input control.
883    #[serde(untagged)]
884    Other(OtherElicitationPropertySchema),
885}
886
887/// Custom or future elicitation property schema payload.
888///
889/// This preserves the unknown `type` discriminator and the rest of the property
890/// schema object for clients that store, replay, proxy, or forward elicitation
891/// requests.
892#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
893#[schemars(inline)]
894#[schemars(transform = other_elicitation_property_schema_schema)]
895#[serde(rename_all = "camelCase")]
896#[non_exhaustive]
897pub struct OtherElicitationPropertySchema {
898    /// Custom or future elicitation property schema type.
899    ///
900    /// Values beginning with `_` are reserved for implementation-specific
901    /// extensions. Unknown values that do not begin with `_` are reserved for
902    /// future ACP variants.
903    #[serde(rename = "type")]
904    pub type_: String,
905    /// Additional fields from the unknown property schema payload.
906    #[serde(flatten)]
907    pub fields: BTreeMap<String, serde_json::Value>,
908}
909
910impl OtherElicitationPropertySchema {
911    /// Builds [`OtherElicitationPropertySchema`] from an unknown discriminator and preserves the remaining extension fields.
912    #[must_use]
913    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
914        fields.remove("type");
915        Self {
916            type_: type_.into(),
917            fields,
918        }
919    }
920}
921
922impl<'de> Deserialize<'de> for OtherElicitationPropertySchema {
923    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
924    where
925        D: serde::Deserializer<'de>,
926    {
927        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
928        let type_ = fields
929            .remove("type")
930            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
931        let serde_json::Value::String(type_) = type_ else {
932            return Err(serde::de::Error::custom("`type` must be a string"));
933        };
934
935        if is_known_elicitation_property_schema_type(&type_) {
936            return Err(serde::de::Error::custom(format!(
937                "known elicitation property schema type `{type_}` did not match its schema"
938            )));
939        }
940
941        Ok(Self { type_, fields })
942    }
943}
944
945const KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES: &[&str] =
946    &["string", "number", "integer", "boolean", "array"];
947
948fn is_known_elicitation_property_schema_type(type_: &str) -> bool {
949    KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES.contains(&type_)
950}
951
952fn other_elicitation_property_schema_schema(schema: &mut Schema) {
953    super::schema_util::reject_known_string_discriminators(
954        schema,
955        "type",
956        KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES,
957    );
958}
959
960impl From<StringPropertySchema> for ElicitationPropertySchema {
961    fn from(schema: StringPropertySchema) -> Self {
962        Self::String(schema)
963    }
964}
965
966impl From<NumberPropertySchema> for ElicitationPropertySchema {
967    fn from(schema: NumberPropertySchema) -> Self {
968        Self::Number(schema)
969    }
970}
971
972impl From<IntegerPropertySchema> for ElicitationPropertySchema {
973    fn from(schema: IntegerPropertySchema) -> Self {
974        Self::Integer(schema)
975    }
976}
977
978impl From<BooleanPropertySchema> for ElicitationPropertySchema {
979    fn from(schema: BooleanPropertySchema) -> Self {
980        Self::Boolean(schema)
981    }
982}
983
984impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
985    fn from(schema: MultiSelectPropertySchema) -> Self {
986        Self::Array(schema)
987    }
988}
989
990fn default_object_type() -> ElicitationSchemaType {
991    ElicitationSchemaType::Object
992}
993
994/// Type-safe elicitation schema for requesting structured user input.
995///
996/// This represents a JSON Schema object with primitive-typed properties,
997/// as required by the elicitation specification.
998#[serde_as]
999#[skip_serializing_none]
1000#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1001#[serde(rename_all = "camelCase")]
1002#[non_exhaustive]
1003pub struct ElicitationSchema {
1004    /// Type discriminator. Always `"object"`.
1005    #[serde_as(deserialize_as = "DefaultOnError")]
1006    #[schemars(extend("x-deserialize-default-on-error" = true))]
1007    #[serde(rename = "type", default = "default_object_type")]
1008    pub type_: ElicitationSchemaType,
1009    /// Optional title for the schema.
1010    #[serde_as(deserialize_as = "DefaultOnError")]
1011    #[schemars(extend("x-deserialize-default-on-error" = true))]
1012    #[serde(default)]
1013    pub title: Option<String>,
1014    /// Property definitions (must be primitive types).
1015    #[serde(default)]
1016    pub properties: BTreeMap<String, ElicitationPropertySchema>,
1017    /// List of required property names.
1018    #[serde(default)]
1019    pub required: Option<Vec<String>>,
1020    /// Optional description of what this schema represents.
1021    #[serde_as(deserialize_as = "DefaultOnError")]
1022    #[schemars(extend("x-deserialize-default-on-error" = true))]
1023    #[serde(default)]
1024    pub description: Option<String>,
1025    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1026    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1027    /// these keys.
1028    ///
1029    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1030    #[serde_as(deserialize_as = "DefaultOnError")]
1031    #[schemars(extend("x-deserialize-default-on-error" = true))]
1032    #[serde(default)]
1033    #[serde(rename = "_meta")]
1034    pub meta: Option<Meta>,
1035}
1036
1037impl Default for ElicitationSchema {
1038    fn default() -> Self {
1039        Self {
1040            type_: default_object_type(),
1041            title: None,
1042            properties: BTreeMap::new(),
1043            required: None,
1044            description: None,
1045            meta: None,
1046        }
1047    }
1048}
1049
1050impl ElicitationSchema {
1051    /// Create a new empty elicitation schema.
1052    #[must_use]
1053    pub fn new() -> Self {
1054        Self::default()
1055    }
1056
1057    /// Optional title for the schema.
1058    #[must_use]
1059    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
1060        self.title = title.into_option();
1061        self
1062    }
1063
1064    /// Optional description of what this schema represents.
1065    #[must_use]
1066    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
1067        self.description = description.into_option();
1068        self
1069    }
1070
1071    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1072    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1073    /// these keys.
1074    ///
1075    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1076    #[must_use]
1077    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1078        self.meta = meta.into_option();
1079        self
1080    }
1081
1082    /// Add a property to the schema.
1083    #[must_use]
1084    pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
1085    where
1086        S: Into<ElicitationPropertySchema>,
1087    {
1088        let name = name.into();
1089        self.properties.insert(name.clone(), schema.into());
1090
1091        if required {
1092            let required_fields = self.required.get_or_insert_with(Vec::new);
1093            if !required_fields.contains(&name) {
1094                required_fields.push(name);
1095            }
1096        } else if let Some(required_fields) = &mut self.required {
1097            required_fields.retain(|field| field != &name);
1098
1099            if required_fields.is_empty() {
1100                self.required = None;
1101            }
1102        }
1103
1104        self
1105    }
1106
1107    /// Add a string property.
1108    #[must_use]
1109    pub fn string(self, name: impl Into<String>, required: bool) -> Self {
1110        self.property(name, StringPropertySchema::new(), required)
1111    }
1112
1113    /// Add an email property.
1114    #[must_use]
1115    pub fn email(self, name: impl Into<String>, required: bool) -> Self {
1116        self.property(name, StringPropertySchema::email(), required)
1117    }
1118
1119    /// Add a URI property.
1120    #[must_use]
1121    pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
1122        self.property(name, StringPropertySchema::uri(), required)
1123    }
1124
1125    /// Add a date property.
1126    #[must_use]
1127    pub fn date(self, name: impl Into<String>, required: bool) -> Self {
1128        self.property(name, StringPropertySchema::date(), required)
1129    }
1130
1131    /// Add a date-time property.
1132    #[must_use]
1133    pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
1134        self.property(name, StringPropertySchema::date_time(), required)
1135    }
1136
1137    /// Add a number property with range.
1138    #[must_use]
1139    pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
1140        self.property(
1141            name,
1142            NumberPropertySchema::new().minimum(min).maximum(max),
1143            required,
1144        )
1145    }
1146
1147    /// Add an integer property with range.
1148    #[must_use]
1149    pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
1150        self.property(
1151            name,
1152            IntegerPropertySchema::new().minimum(min).maximum(max),
1153            required,
1154        )
1155    }
1156
1157    /// Add a boolean property.
1158    #[must_use]
1159    pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
1160        self.property(name, BooleanPropertySchema::new(), required)
1161    }
1162}
1163
1164/// **UNSTABLE**
1165///
1166/// This capability is not part of the spec yet, and may be removed or changed at any point.
1167///
1168/// Elicitation capabilities supported by the client.
1169#[serde_as]
1170#[skip_serializing_none]
1171#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1172#[serde(rename_all = "camelCase")]
1173#[non_exhaustive]
1174pub struct ElicitationCapabilities {
1175    /// Whether the client supports form-based elicitation.
1176    ///
1177    /// Optional. Omitted or `null` both mean the client does not advertise support.
1178    /// Supplying `{}` means the client supports form-based elicitation.
1179    #[serde_as(deserialize_as = "DefaultOnError")]
1180    #[schemars(extend("x-deserialize-default-on-error" = true))]
1181    #[serde(default)]
1182    pub form: Option<ElicitationFormCapabilities>,
1183    /// Whether the client supports URL-based elicitation.
1184    ///
1185    /// Optional. Omitted or `null` both mean the client does not advertise support.
1186    /// Supplying `{}` means the client supports URL-based elicitation.
1187    #[serde_as(deserialize_as = "DefaultOnError")]
1188    #[schemars(extend("x-deserialize-default-on-error" = true))]
1189    #[serde(default)]
1190    pub url: Option<ElicitationUrlCapabilities>,
1191    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1192    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1193    /// these keys.
1194    ///
1195    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1196    #[serde_as(deserialize_as = "DefaultOnError")]
1197    #[schemars(extend("x-deserialize-default-on-error" = true))]
1198    #[serde(default)]
1199    #[serde(rename = "_meta")]
1200    pub meta: Option<Meta>,
1201}
1202
1203impl ElicitationCapabilities {
1204    /// Builds an empty [`ElicitationCapabilities`]; use builder methods to advertise supported sub-capabilities.
1205    #[must_use]
1206    pub fn new() -> Self {
1207        Self::default()
1208    }
1209
1210    /// Whether the client supports form-based elicitation.
1211    ///
1212    /// Omitted or `null` both mean the client does not advertise support.
1213    /// Supplying `{}` means the client supports form-based elicitation.
1214    #[must_use]
1215    pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
1216        self.form = form.into_option();
1217        self
1218    }
1219
1220    /// Whether the client supports URL-based elicitation.
1221    ///
1222    /// Omitted or `null` both mean the client does not advertise support.
1223    /// Supplying `{}` means the client supports URL-based elicitation.
1224    #[must_use]
1225    pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
1226        self.url = url.into_option();
1227        self
1228    }
1229
1230    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1231    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1232    /// these keys.
1233    ///
1234    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1235    #[must_use]
1236    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1237        self.meta = meta.into_option();
1238        self
1239    }
1240}
1241
1242/// **UNSTABLE**
1243///
1244/// This capability is not part of the spec yet, and may be removed or changed at any point.
1245///
1246/// Form-based elicitation capabilities.
1247///
1248/// Supplying `{}` means the client supports form-based elicitation.
1249#[serde_as]
1250#[skip_serializing_none]
1251#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1252#[serde(rename_all = "camelCase")]
1253#[non_exhaustive]
1254pub struct ElicitationFormCapabilities {
1255    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1256    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1257    /// these keys.
1258    ///
1259    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1260    #[serde_as(deserialize_as = "DefaultOnError")]
1261    #[schemars(extend("x-deserialize-default-on-error" = true))]
1262    #[serde(default)]
1263    #[serde(rename = "_meta")]
1264    pub meta: Option<Meta>,
1265}
1266
1267impl ElicitationFormCapabilities {
1268    /// Builds an empty [`ElicitationFormCapabilities`]; use builder methods to advertise supported sub-capabilities.
1269    #[must_use]
1270    pub fn new() -> Self {
1271        Self::default()
1272    }
1273
1274    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1275    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1276    /// these keys.
1277    ///
1278    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1279    #[must_use]
1280    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1281        self.meta = meta.into_option();
1282        self
1283    }
1284}
1285
1286/// **UNSTABLE**
1287///
1288/// This capability is not part of the spec yet, and may be removed or changed at any point.
1289///
1290/// URL-based elicitation capabilities.
1291///
1292/// Supplying `{}` means the client supports URL-based elicitation.
1293#[serde_as]
1294#[skip_serializing_none]
1295#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1296#[serde(rename_all = "camelCase")]
1297#[non_exhaustive]
1298pub struct ElicitationUrlCapabilities {
1299    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1300    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1301    /// these keys.
1302    ///
1303    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1304    #[serde_as(deserialize_as = "DefaultOnError")]
1305    #[schemars(extend("x-deserialize-default-on-error" = true))]
1306    #[serde(default)]
1307    #[serde(rename = "_meta")]
1308    pub meta: Option<Meta>,
1309}
1310
1311impl ElicitationUrlCapabilities {
1312    /// Builds an empty [`ElicitationUrlCapabilities`]; use builder methods to advertise supported sub-capabilities.
1313    #[must_use]
1314    pub fn new() -> Self {
1315        Self::default()
1316    }
1317
1318    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1319    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1320    /// these keys.
1321    ///
1322    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1323    #[must_use]
1324    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1325        self.meta = meta.into_option();
1326        self
1327    }
1328}
1329
1330/// **UNSTABLE**
1331///
1332/// This capability is not part of the spec yet, and may be removed or changed at any point.
1333///
1334/// The scope of an elicitation request, determining what context it's tied to.
1335#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1336#[serde(untagged)]
1337#[non_exhaustive]
1338pub enum ElicitationScope {
1339    /// Tied to a session, optionally to a specific tool call within that session.
1340    Session(ElicitationSessionScope),
1341    /// Tied to a specific JSON-RPC request outside of a session
1342    /// (e.g., during auth/configuration phases before any session is started).
1343    Request(ElicitationRequestScope),
1344}
1345
1346/// **UNSTABLE**
1347///
1348/// This capability is not part of the spec yet, and may be removed or changed at any point.
1349///
1350/// Session-scoped elicitation, optionally tied to a specific tool call.
1351///
1352/// When `tool_call_id` is set, the elicitation is tied to a specific tool call.
1353/// This is useful when an agent receives an elicitation from an MCP server
1354/// during a tool call and needs to redirect it to the user.
1355#[serde_as]
1356#[skip_serializing_none]
1357#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1358#[serde(rename_all = "camelCase")]
1359#[non_exhaustive]
1360pub struct ElicitationSessionScope {
1361    /// The session this elicitation is tied to.
1362    pub session_id: SessionId,
1363    /// Optional tool call within the session.
1364    #[serde_as(deserialize_as = "DefaultOnError")]
1365    #[schemars(extend("x-deserialize-default-on-error" = true))]
1366    #[serde(default)]
1367    pub tool_call_id: Option<ToolCallId>,
1368}
1369
1370impl ElicitationSessionScope {
1371    /// Builds [`ElicitationSessionScope`] with the required fields set; optional fields start unset or empty.
1372    #[must_use]
1373    pub fn new(session_id: impl Into<SessionId>) -> Self {
1374        Self {
1375            session_id: session_id.into(),
1376            tool_call_id: None,
1377        }
1378    }
1379
1380    /// Sets or clears the optional `toolCallId` field.
1381    #[must_use]
1382    pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
1383        self.tool_call_id = tool_call_id.into_option();
1384        self
1385    }
1386}
1387
1388/// **UNSTABLE**
1389///
1390/// This capability is not part of the spec yet, and may be removed or changed at any point.
1391///
1392/// Request-scoped elicitation, tied to a specific JSON-RPC request outside of a session
1393/// (e.g., during auth/configuration phases before any session is started).
1394#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1395#[serde(rename_all = "camelCase")]
1396#[non_exhaustive]
1397pub struct ElicitationRequestScope {
1398    /// The request this elicitation is tied to.
1399    pub request_id: RequestId,
1400}
1401
1402impl ElicitationRequestScope {
1403    /// Builds [`ElicitationRequestScope`] with the required fields set; optional fields start unset or empty.
1404    #[must_use]
1405    pub fn new(request_id: impl Into<RequestId>) -> Self {
1406        Self {
1407            request_id: request_id.into(),
1408        }
1409    }
1410}
1411
1412impl From<ElicitationSessionScope> for ElicitationScope {
1413    fn from(scope: ElicitationSessionScope) -> Self {
1414        Self::Session(scope)
1415    }
1416}
1417
1418impl From<ElicitationRequestScope> for ElicitationScope {
1419    fn from(scope: ElicitationRequestScope) -> Self {
1420        Self::Request(scope)
1421    }
1422}
1423
1424/// **UNSTABLE**
1425///
1426/// This capability is not part of the spec yet, and may be removed or changed at any point.
1427///
1428/// Request from the agent to elicit structured user input.
1429///
1430/// The agent sends this to the client to request information from the user,
1431/// either via a form or by directing them to a URL.
1432/// Elicitations are tied to a session (optionally a tool call) or a request.
1433#[serde_as]
1434#[skip_serializing_none]
1435#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1436#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1437#[serde(rename_all = "camelCase")]
1438#[non_exhaustive]
1439pub struct CreateElicitationRequest {
1440    /// The elicitation mode and its mode-specific fields.
1441    #[serde(flatten)]
1442    pub mode: ElicitationMode,
1443    /// A human-readable message describing what input is needed.
1444    pub message: String,
1445    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1446    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1447    /// these keys.
1448    ///
1449    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1450    #[serde_as(deserialize_as = "DefaultOnError")]
1451    #[schemars(extend("x-deserialize-default-on-error" = true))]
1452    #[serde(default)]
1453    #[serde(rename = "_meta")]
1454    pub meta: Option<Meta>,
1455}
1456
1457impl CreateElicitationRequest {
1458    /// Builds [`CreateElicitationRequest`] with the required request fields set; optional fields start unset or empty.
1459    #[must_use]
1460    pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
1461        Self {
1462            mode: mode.into(),
1463            message: message.into(),
1464            meta: None,
1465        }
1466    }
1467
1468    /// Returns the scope this elicitation is tied to.
1469    #[must_use]
1470    pub fn scope(&self) -> &ElicitationScope {
1471        self.mode.scope()
1472    }
1473
1474    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1475    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1476    /// these keys.
1477    ///
1478    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1479    #[must_use]
1480    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1481        self.meta = meta.into_option();
1482        self
1483    }
1484}
1485
1486/// **UNSTABLE**
1487///
1488/// This capability is not part of the spec yet, and may be removed or changed at any point.
1489///
1490/// The mode of elicitation, determining how user input is collected.
1491#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1492#[serde(tag = "mode", rename_all = "snake_case")]
1493#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
1494#[non_exhaustive]
1495pub enum ElicitationMode {
1496    /// Form-based elicitation where the client renders a form from the provided schema.
1497    Form(ElicitationFormMode),
1498    /// URL-based elicitation where the client directs the user to a URL.
1499    Url(ElicitationUrlMode),
1500    /// Custom or future elicitation mode.
1501    ///
1502    /// Values beginning with `_` are reserved for implementation-specific
1503    /// extensions. Unknown values that do not begin with `_` are reserved for
1504    /// future ACP variants.
1505    ///
1506    /// Clients that do not understand this mode should preserve the raw payload
1507    /// when storing, replaying, proxying, or forwarding elicitation requests.
1508    /// They MUST NOT render it as a known elicitation mode.
1509    #[serde(untagged)]
1510    Other(OtherElicitationMode),
1511}
1512
1513/// Custom or future elicitation mode payload.
1514///
1515/// This preserves the unknown `mode` discriminator and the rest of the mode
1516/// object for clients that store, replay, proxy, or forward elicitation
1517/// requests.
1518#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)]
1519#[schemars(inline)]
1520#[schemars(transform = other_elicitation_mode_schema)]
1521#[serde(rename_all = "camelCase")]
1522#[non_exhaustive]
1523pub struct OtherElicitationMode {
1524    /// Custom or future elicitation mode.
1525    ///
1526    /// Values beginning with `_` are reserved for implementation-specific
1527    /// extensions. Unknown values that do not begin with `_` are reserved for
1528    /// future ACP variants.
1529    pub mode: String,
1530    /// The scope this elicitation is tied to.
1531    #[serde(flatten)]
1532    pub scope: ElicitationScope,
1533    /// Additional fields from the unknown elicitation mode payload.
1534    #[serde(flatten)]
1535    pub fields: BTreeMap<String, serde_json::Value>,
1536}
1537
1538impl OtherElicitationMode {
1539    /// Builds [`OtherElicitationMode`] from an unknown discriminator and preserves the remaining extension fields.
1540    #[must_use]
1541    pub fn new(
1542        mode: impl Into<String>,
1543        scope: impl Into<ElicitationScope>,
1544        mut fields: BTreeMap<String, serde_json::Value>,
1545    ) -> Self {
1546        fields.remove("mode");
1547        remove_elicitation_scope_fields(&mut fields);
1548        Self {
1549            mode: mode.into(),
1550            scope: scope.into(),
1551            fields,
1552        }
1553    }
1554}
1555
1556impl<'de> Deserialize<'de> for OtherElicitationMode {
1557    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1558    where
1559        D: serde::Deserializer<'de>,
1560    {
1561        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1562        let mode = fields
1563            .remove("mode")
1564            .ok_or_else(|| serde::de::Error::missing_field("mode"))?;
1565        let serde_json::Value::String(mode) = mode else {
1566            return Err(serde::de::Error::custom("`mode` must be a string"));
1567        };
1568
1569        if is_known_elicitation_mode(&mode) {
1570            return Err(serde::de::Error::custom(format!(
1571                "known elicitation mode `{mode}` did not match its schema"
1572            )));
1573        }
1574
1575        let scope = serde_json::from_value::<ElicitationScope>(serde_json::Value::Object(
1576            fields.clone().into_iter().collect(),
1577        ))
1578        .map_err(serde::de::Error::custom)?;
1579        remove_elicitation_scope_fields(&mut fields);
1580
1581        Ok(Self {
1582            mode,
1583            scope,
1584            fields,
1585        })
1586    }
1587}
1588
1589const KNOWN_ELICITATION_MODES: &[&str] = &["form", "url"];
1590
1591fn is_known_elicitation_mode(mode: &str) -> bool {
1592    KNOWN_ELICITATION_MODES.contains(&mode)
1593}
1594
1595fn remove_elicitation_scope_fields(fields: &mut BTreeMap<String, serde_json::Value>) {
1596    fields.remove("sessionId");
1597    fields.remove("toolCallId");
1598    fields.remove("requestId");
1599}
1600
1601fn other_elicitation_mode_schema(schema: &mut Schema) {
1602    super::schema_util::reject_known_string_discriminators(schema, "mode", KNOWN_ELICITATION_MODES);
1603}
1604
1605impl From<ElicitationFormMode> for ElicitationMode {
1606    fn from(mode: ElicitationFormMode) -> Self {
1607        Self::Form(mode)
1608    }
1609}
1610
1611impl From<ElicitationUrlMode> for ElicitationMode {
1612    fn from(mode: ElicitationUrlMode) -> Self {
1613        Self::Url(mode)
1614    }
1615}
1616
1617impl From<OtherElicitationMode> for ElicitationMode {
1618    fn from(mode: OtherElicitationMode) -> Self {
1619        Self::Other(mode)
1620    }
1621}
1622
1623impl ElicitationMode {
1624    /// Returns the scope this elicitation mode is tied to.
1625    #[must_use]
1626    pub fn scope(&self) -> &ElicitationScope {
1627        match self {
1628            Self::Form(f) => &f.scope,
1629            Self::Url(u) => &u.scope,
1630            Self::Other(other) => &other.scope,
1631        }
1632    }
1633}
1634
1635/// **UNSTABLE**
1636///
1637/// This capability is not part of the spec yet, and may be removed or changed at any point.
1638///
1639/// Form-based elicitation mode where the client renders a form from the provided schema.
1640#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1641#[serde(rename_all = "camelCase")]
1642#[non_exhaustive]
1643pub struct ElicitationFormMode {
1644    /// The scope this elicitation is tied to.
1645    #[serde(flatten)]
1646    pub scope: ElicitationScope,
1647    /// A JSON Schema describing the form fields to present to the user.
1648    pub requested_schema: ElicitationSchema,
1649}
1650
1651impl ElicitationFormMode {
1652    /// Builds [`ElicitationFormMode`] with the required fields set; optional fields start unset or empty.
1653    #[must_use]
1654    pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1655        Self {
1656            scope: scope.into(),
1657            requested_schema,
1658        }
1659    }
1660}
1661
1662/// **UNSTABLE**
1663///
1664/// This capability is not part of the spec yet, and may be removed or changed at any point.
1665///
1666/// URL-based elicitation mode where the client directs the user to a URL.
1667#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1668#[serde(rename_all = "camelCase")]
1669#[non_exhaustive]
1670pub struct ElicitationUrlMode {
1671    /// The scope this elicitation is tied to.
1672    #[serde(flatten)]
1673    pub scope: ElicitationScope,
1674    /// The unique identifier for this elicitation.
1675    pub elicitation_id: ElicitationId,
1676    /// The URL to direct the user to.
1677    #[schemars(url)]
1678    pub url: String,
1679}
1680
1681impl ElicitationUrlMode {
1682    /// Builds [`ElicitationUrlMode`] with the required fields set; optional fields start unset or empty.
1683    #[must_use]
1684    pub fn new(
1685        scope: impl Into<ElicitationScope>,
1686        elicitation_id: impl Into<ElicitationId>,
1687        url: impl Into<String>,
1688    ) -> Self {
1689        Self {
1690            scope: scope.into(),
1691            elicitation_id: elicitation_id.into(),
1692            url: url.into(),
1693        }
1694    }
1695}
1696
1697/// **UNSTABLE**
1698///
1699/// This capability is not part of the spec yet, and may be removed or changed at any point.
1700///
1701/// Response from the client to an elicitation request.
1702#[serde_as]
1703#[skip_serializing_none]
1704#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1705#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1706#[serde(rename_all = "camelCase")]
1707#[non_exhaustive]
1708pub struct CreateElicitationResponse {
1709    /// The user's action in response to the elicitation.
1710    #[serde(flatten)]
1711    pub action: ElicitationAction,
1712    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1713    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1714    /// these keys.
1715    ///
1716    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1717    #[serde_as(deserialize_as = "DefaultOnError")]
1718    #[schemars(extend("x-deserialize-default-on-error" = true))]
1719    #[serde(default)]
1720    #[serde(rename = "_meta")]
1721    pub meta: Option<Meta>,
1722}
1723
1724impl CreateElicitationResponse {
1725    /// Builds [`CreateElicitationResponse`] with the required response fields set; optional fields start unset or empty.
1726    #[must_use]
1727    pub fn new(action: impl Into<ElicitationAction>) -> Self {
1728        Self {
1729            action: action.into(),
1730            meta: None,
1731        }
1732    }
1733
1734    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1735    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1736    /// these keys.
1737    ///
1738    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1739    #[must_use]
1740    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1741        self.meta = meta.into_option();
1742        self
1743    }
1744}
1745
1746/// **UNSTABLE**
1747///
1748/// This capability is not part of the spec yet, and may be removed or changed at any point.
1749///
1750/// The user's action in response to an elicitation.
1751#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1752#[serde(tag = "action", rename_all = "snake_case")]
1753#[schemars(extend("discriminator" = {"propertyName": "action"}))]
1754#[non_exhaustive]
1755pub enum ElicitationAction {
1756    /// The user accepted and provided content.
1757    Accept(ElicitationAcceptAction),
1758    /// The user declined the elicitation.
1759    Decline,
1760    /// The elicitation was cancelled.
1761    Cancel,
1762    /// Custom or future elicitation action.
1763    ///
1764    /// Values beginning with `_` are reserved for implementation-specific
1765    /// extensions. Unknown values that do not begin with `_` are reserved for
1766    /// future ACP variants.
1767    ///
1768    /// Agents that do not understand this action should preserve the raw
1769    /// payload when storing, replaying, proxying, or forwarding elicitation
1770    /// responses. They MUST NOT treat it as a known elicitation action.
1771    #[serde(untagged)]
1772    Other(OtherElicitationAction),
1773}
1774
1775/// Custom or future elicitation action payload.
1776///
1777/// This preserves the unknown `action` discriminator and the rest of the
1778/// response object for agents that store, replay, proxy, or forward elicitation
1779/// responses.
1780#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)]
1781#[schemars(inline)]
1782#[schemars(transform = other_elicitation_action_schema)]
1783#[serde(rename_all = "camelCase")]
1784#[non_exhaustive]
1785pub struct OtherElicitationAction {
1786    /// Custom or future elicitation action.
1787    ///
1788    /// Values beginning with `_` are reserved for implementation-specific
1789    /// extensions. Unknown values that do not begin with `_` are reserved for
1790    /// future ACP variants.
1791    pub action: String,
1792    /// Additional fields from the unknown elicitation action payload.
1793    #[serde(flatten)]
1794    pub fields: BTreeMap<String, serde_json::Value>,
1795}
1796
1797impl OtherElicitationAction {
1798    /// Builds [`OtherElicitationAction`] from an unknown discriminator and preserves the remaining extension fields.
1799    #[must_use]
1800    pub fn new(action: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1801        fields.remove("action");
1802        Self {
1803            action: action.into(),
1804            fields,
1805        }
1806    }
1807}
1808
1809impl<'de> Deserialize<'de> for OtherElicitationAction {
1810    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1811    where
1812        D: serde::Deserializer<'de>,
1813    {
1814        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1815        let action = fields
1816            .remove("action")
1817            .ok_or_else(|| serde::de::Error::missing_field("action"))?;
1818        let serde_json::Value::String(action) = action else {
1819            return Err(serde::de::Error::custom("`action` must be a string"));
1820        };
1821
1822        if is_known_elicitation_action(&action) {
1823            return Err(serde::de::Error::custom(format!(
1824                "known elicitation action `{action}` did not match its schema"
1825            )));
1826        }
1827
1828        Ok(Self { action, fields })
1829    }
1830}
1831
1832const KNOWN_ELICITATION_ACTIONS: &[&str] = &["accept", "decline", "cancel"];
1833
1834fn is_known_elicitation_action(action: &str) -> bool {
1835    KNOWN_ELICITATION_ACTIONS.contains(&action)
1836}
1837
1838fn other_elicitation_action_schema(schema: &mut Schema) {
1839    super::schema_util::reject_known_string_discriminators(
1840        schema,
1841        "action",
1842        KNOWN_ELICITATION_ACTIONS,
1843    );
1844}
1845
1846impl From<ElicitationAcceptAction> for ElicitationAction {
1847    fn from(action: ElicitationAcceptAction) -> Self {
1848        Self::Accept(action)
1849    }
1850}
1851
1852impl From<OtherElicitationAction> for ElicitationAction {
1853    fn from(action: OtherElicitationAction) -> Self {
1854        Self::Other(action)
1855    }
1856}
1857
1858/// **UNSTABLE**
1859///
1860/// This capability is not part of the spec yet, and may be removed or changed at any point.
1861///
1862/// The user accepted the elicitation and provided content.
1863#[serde_as]
1864#[skip_serializing_none]
1865#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1866#[serde(rename_all = "camelCase")]
1867#[non_exhaustive]
1868pub struct ElicitationAcceptAction {
1869    /// The user-provided content, if any, as an object matching the requested schema.
1870    #[serde(default)]
1871    pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1872}
1873
1874impl ElicitationAcceptAction {
1875    /// Builds [`ElicitationAcceptAction`] with the required fields set; optional fields start unset or empty.
1876    #[must_use]
1877    pub fn new() -> Self {
1878        Self { content: None }
1879    }
1880
1881    /// The user-provided content as an object matching the requested schema.
1882    #[must_use]
1883    pub fn content(
1884        mut self,
1885        content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1886    ) -> Self {
1887        self.content = content.into_option();
1888        self
1889    }
1890}
1891
1892/// Allowed wire representations for [`ElicitationContentValue`].
1893#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1894#[serde(untagged)]
1895#[non_exhaustive]
1896pub enum ElicitationContentValue {
1897    /// String value accepted in elicitation response content.
1898    String(String),
1899    /// Integer value accepted in elicitation response content.
1900    Integer(i64),
1901    /// Number value accepted in elicitation response content.
1902    Number(f64),
1903    /// Boolean value accepted in elicitation response content.
1904    Boolean(bool),
1905    /// String array value accepted in elicitation response content.
1906    StringArray(Vec<String>),
1907}
1908
1909impl From<String> for ElicitationContentValue {
1910    fn from(value: String) -> Self {
1911        Self::String(value)
1912    }
1913}
1914
1915impl From<&str> for ElicitationContentValue {
1916    fn from(value: &str) -> Self {
1917        Self::String(value.to_string())
1918    }
1919}
1920
1921impl From<i64> for ElicitationContentValue {
1922    fn from(value: i64) -> Self {
1923        Self::Integer(value)
1924    }
1925}
1926
1927impl From<i32> for ElicitationContentValue {
1928    fn from(value: i32) -> Self {
1929        Self::Integer(i64::from(value))
1930    }
1931}
1932
1933impl From<f64> for ElicitationContentValue {
1934    fn from(value: f64) -> Self {
1935        Self::Number(value)
1936    }
1937}
1938
1939impl From<bool> for ElicitationContentValue {
1940    fn from(value: bool) -> Self {
1941        Self::Boolean(value)
1942    }
1943}
1944
1945impl From<Vec<String>> for ElicitationContentValue {
1946    fn from(value: Vec<String>) -> Self {
1947        Self::StringArray(value)
1948    }
1949}
1950
1951impl From<Vec<&str>> for ElicitationContentValue {
1952    fn from(value: Vec<&str>) -> Self {
1953        Self::StringArray(value.into_iter().map(str::to_string).collect())
1954    }
1955}
1956
1957impl Default for ElicitationAcceptAction {
1958    fn default() -> Self {
1959        Self::new()
1960    }
1961}
1962
1963/// **UNSTABLE**
1964///
1965/// This capability is not part of the spec yet, and may be removed or changed at any point.
1966///
1967/// Notification sent by the agent when a URL-based elicitation is complete.
1968#[serde_as]
1969#[skip_serializing_none]
1970#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1971#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
1972#[serde(rename_all = "camelCase")]
1973#[non_exhaustive]
1974pub struct CompleteElicitationNotification {
1975    /// The ID of the elicitation that completed.
1976    pub elicitation_id: ElicitationId,
1977    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1978    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1979    /// these keys.
1980    ///
1981    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1982    #[serde_as(deserialize_as = "DefaultOnError")]
1983    #[schemars(extend("x-deserialize-default-on-error" = true))]
1984    #[serde(default)]
1985    #[serde(rename = "_meta")]
1986    pub meta: Option<Meta>,
1987}
1988
1989impl CompleteElicitationNotification {
1990    /// Builds [`CompleteElicitationNotification`] with the required notification fields set; optional fields start unset or empty.
1991    #[must_use]
1992    pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
1993        Self {
1994            elicitation_id: elicitation_id.into(),
1995            meta: None,
1996        }
1997    }
1998
1999    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2000    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2001    /// these keys.
2002    ///
2003    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2004    #[must_use]
2005    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2006        self.meta = meta.into_option();
2007        self
2008    }
2009}
2010
2011#[cfg(test)]
2012mod tests {
2013    use super::*;
2014    use serde_json::json;
2015
2016    #[test]
2017    fn form_mode_request_serialization() {
2018        let schema = ElicitationSchema::new().string("name", true);
2019        let req = CreateElicitationRequest::new(
2020            ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
2021            "Please enter your name",
2022        );
2023
2024        let json = serde_json::to_value(&req).unwrap();
2025        assert_eq!(json["sessionId"], "sess_1");
2026        assert!(json.get("toolCallId").is_none());
2027        assert_eq!(json["mode"], "form");
2028        assert_eq!(json["message"], "Please enter your name");
2029        assert!(json["requestedSchema"].is_object());
2030        assert_eq!(json["requestedSchema"]["type"], "object");
2031        assert_eq!(
2032            json["requestedSchema"]["properties"]["name"]["type"],
2033            "string"
2034        );
2035
2036        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2037        assert_eq!(
2038            *roundtripped.scope(),
2039            ElicitationSessionScope::new("sess_1").into()
2040        );
2041        assert_eq!(roundtripped.message, "Please enter your name");
2042        assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
2043    }
2044
2045    #[test]
2046    fn url_mode_request_serialization() {
2047        let req = CreateElicitationRequest::new(
2048            ElicitationUrlMode::new(
2049                ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
2050                "elic_1",
2051                "https://example.com/auth",
2052            ),
2053            "Please authenticate",
2054        );
2055
2056        let json = serde_json::to_value(&req).unwrap();
2057        assert_eq!(json["sessionId"], "sess_2");
2058        assert_eq!(json["toolCallId"], "tc_1");
2059        assert_eq!(json["mode"], "url");
2060        assert_eq!(json["elicitationId"], "elic_1");
2061        assert_eq!(json["url"], "https://example.com/auth");
2062        assert_eq!(json["message"], "Please authenticate");
2063
2064        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2065        assert_eq!(
2066            *roundtripped.scope(),
2067            ElicitationSessionScope::new("sess_2")
2068                .tool_call_id("tc_1")
2069                .into()
2070        );
2071        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
2072    }
2073
2074    #[test]
2075    fn response_accept_serialization() {
2076        let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
2077            ElicitationAcceptAction::new().content(BTreeMap::from([(
2078                "name".to_string(),
2079                ElicitationContentValue::from("Alice"),
2080            )])),
2081        ));
2082
2083        let json = serde_json::to_value(&resp).unwrap();
2084        assert_eq!(json["action"], "accept");
2085        assert_eq!(json["content"]["name"], "Alice");
2086
2087        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2088        assert!(matches!(
2089            roundtripped.action,
2090            ElicitationAction::Accept(ElicitationAcceptAction {
2091                content: Some(_),
2092                ..
2093            })
2094        ));
2095    }
2096
2097    #[test]
2098    fn response_decline_serialization() {
2099        let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
2100
2101        let json = serde_json::to_value(&resp).unwrap();
2102        assert_eq!(json["action"], "decline");
2103
2104        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2105        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
2106    }
2107
2108    #[test]
2109    fn response_cancel_serialization() {
2110        let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
2111
2112        let json = serde_json::to_value(&resp).unwrap();
2113        assert_eq!(json["action"], "cancel");
2114
2115        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2116        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
2117    }
2118
2119    #[test]
2120    fn unknown_action_response_serialization() {
2121        let json = json!({
2122            "action": "_defer",
2123            "reason": "waiting",
2124            "retryAfterMs": 1000
2125        });
2126
2127        let resp: CreateElicitationResponse = serde_json::from_value(json.clone()).unwrap();
2128        let ElicitationAction::Other(other) = &resp.action else {
2129            panic!("expected unknown elicitation action");
2130        };
2131
2132        assert_eq!(other.action, "_defer");
2133        assert_eq!(other.fields.get("reason"), Some(&json!("waiting")));
2134        assert_eq!(other.fields.get("retryAfterMs"), Some(&json!(1000)));
2135        assert_eq!(serde_json::to_value(&resp).unwrap(), json);
2136    }
2137
2138    #[test]
2139    fn unknown_action_does_not_hide_known_action() {
2140        assert!(
2141            serde_json::from_value::<OtherElicitationAction>(json!({
2142                "action": "accept",
2143                "content": {}
2144            }))
2145            .is_err()
2146        );
2147        assert!(serde_json::from_value::<ElicitationAction>(json!({})).is_err());
2148    }
2149
2150    #[test]
2151    fn url_mode_request_scope_serialization() {
2152        let req = CreateElicitationRequest::new(
2153            ElicitationUrlMode::new(
2154                ElicitationRequestScope::new(RequestId::Number(42)),
2155                "elic_2",
2156                "https://example.com/setup",
2157            ),
2158            "Please complete setup",
2159        );
2160
2161        let json = serde_json::to_value(&req).unwrap();
2162        assert_eq!(json["requestId"], 42);
2163        assert!(json.get("sessionId").is_none());
2164        assert_eq!(json["mode"], "url");
2165        assert_eq!(json["elicitationId"], "elic_2");
2166        assert_eq!(json["url"], "https://example.com/setup");
2167        assert_eq!(json["message"], "Please complete setup");
2168
2169        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2170        assert_eq!(
2171            *roundtripped.scope(),
2172            ElicitationRequestScope::new(RequestId::Number(42)).into()
2173        );
2174        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
2175    }
2176
2177    #[test]
2178    fn unknown_mode_request_serialization() {
2179        let json = json!({
2180            "requestId": 42,
2181            "mode": "_browser",
2182            "message": "Open a browser window",
2183            "target": "login"
2184        });
2185
2186        let req: CreateElicitationRequest = serde_json::from_value(json.clone()).unwrap();
2187        let ElicitationMode::Other(other) = &req.mode else {
2188            panic!("expected unknown elicitation mode");
2189        };
2190
2191        assert_eq!(other.mode, "_browser");
2192        assert_eq!(
2193            other.scope,
2194            ElicitationRequestScope::new(RequestId::Number(42)).into()
2195        );
2196        assert_eq!(other.fields.get("target"), Some(&json!("login")));
2197        assert_eq!(
2198            *req.scope(),
2199            ElicitationRequestScope::new(RequestId::Number(42)).into()
2200        );
2201        assert_eq!(serde_json::to_value(&req).unwrap(), json);
2202    }
2203
2204    #[test]
2205    fn unknown_mode_does_not_hide_malformed_known_mode() {
2206        let missing_requested_schema = json!({
2207            "requestId": 42,
2208            "mode": "form",
2209            "message": "Enter your name"
2210        });
2211
2212        assert!(
2213            serde_json::from_value::<CreateElicitationRequest>(missing_requested_schema).is_err()
2214        );
2215        assert!(serde_json::from_value::<ElicitationMode>(json!({})).is_err());
2216    }
2217
2218    #[test]
2219    fn request_scope_request_serialization() {
2220        let req = CreateElicitationRequest::new(
2221            ElicitationFormMode::new(
2222                ElicitationRequestScope::new(RequestId::Number(99)),
2223                ElicitationSchema::new().string("workspace", true),
2224            ),
2225            "Enter workspace name",
2226        );
2227
2228        let json = serde_json::to_value(&req).unwrap();
2229        assert_eq!(json["requestId"], 99);
2230        assert!(json.get("sessionId").is_none());
2231
2232        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2233        assert_eq!(
2234            *roundtripped.scope(),
2235            ElicitationRequestScope::new(RequestId::Number(99)).into()
2236        );
2237    }
2238
2239    /// These tests verify that serialization through `ClientResponse` produces the
2240    /// correct flattened wire format and round-trips back via the concrete
2241    /// `CreateElicitationResponse` type.
2242    #[test]
2243    fn client_response_serialization_accept() {
2244        use crate::v2::ClientResponse;
2245
2246        let resp =
2247            ClientResponse::CreateElicitationResponse(Box::new(CreateElicitationResponse::new(
2248                ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from(
2249                    [("name".to_string(), ElicitationContentValue::from("Alice"))],
2250                ))),
2251            )));
2252        let json = serde_json::to_value(&resp).unwrap();
2253        assert_eq!(json["action"], "accept");
2254        assert_eq!(json["content"]["name"], "Alice");
2255
2256        // Round-trip back through the concrete type
2257        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2258        assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
2259    }
2260
2261    #[test]
2262    fn client_response_serialization_decline() {
2263        use crate::v2::ClientResponse;
2264
2265        let resp = ClientResponse::CreateElicitationResponse(Box::new(
2266            CreateElicitationResponse::new(ElicitationAction::Decline),
2267        ));
2268        let json = serde_json::to_value(&resp).unwrap();
2269        assert_eq!(json["action"], "decline");
2270
2271        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2272        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
2273    }
2274
2275    #[test]
2276    fn client_response_serialization_cancel() {
2277        use crate::v2::ClientResponse;
2278
2279        let resp = ClientResponse::CreateElicitationResponse(Box::new(
2280            CreateElicitationResponse::new(ElicitationAction::Cancel),
2281        ));
2282        let json = serde_json::to_value(&resp).unwrap();
2283        assert_eq!(json["action"], "cancel");
2284
2285        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2286        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
2287    }
2288
2289    /// Guard against serde regressions with the `flatten` + internally-tagged combination.
2290    /// Extra fields in the JSON must not cause deserialization failures.
2291    #[test]
2292    fn request_tolerates_extra_fields() {
2293        let json = json!({
2294            "sessionId": "sess_1",
2295            "mode": "form",
2296            "message": "Enter your name",
2297            "requestedSchema": {
2298                "type": "object",
2299                "properties": {
2300                    "name": { "type": "string", "title": "Name" }
2301                },
2302                "required": ["name"]
2303            },
2304            "unknownStringField": "hello",
2305            "unknownNumberField": 42
2306        });
2307
2308        let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2309        assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
2310        assert_eq!(req.message, "Enter your name");
2311        assert!(matches!(req.mode, ElicitationMode::Form(_)));
2312    }
2313
2314    #[test]
2315    fn completion_notification_serialization() {
2316        let notif = CompleteElicitationNotification::new("elic_1");
2317
2318        let json = serde_json::to_value(&notif).unwrap();
2319        assert_eq!(json["elicitationId"], "elic_1");
2320
2321        let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
2322        assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
2323    }
2324
2325    #[test]
2326    fn capabilities_form_only() {
2327        let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
2328
2329        let json = serde_json::to_value(&caps).unwrap();
2330        assert!(json["form"].is_object());
2331        assert!(json.get("url").is_none());
2332
2333        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2334        assert!(roundtripped.form.is_some());
2335        assert!(roundtripped.url.is_none());
2336    }
2337
2338    #[test]
2339    fn capabilities_url_only() {
2340        let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
2341
2342        let json = serde_json::to_value(&caps).unwrap();
2343        assert!(json.get("form").is_none());
2344        assert!(json["url"].is_object());
2345
2346        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2347        assert!(roundtripped.form.is_none());
2348        assert!(roundtripped.url.is_some());
2349    }
2350
2351    #[test]
2352    fn capabilities_both() {
2353        let caps = ElicitationCapabilities::new()
2354            .form(ElicitationFormCapabilities::new())
2355            .url(ElicitationUrlCapabilities::new());
2356
2357        let json = serde_json::to_value(&caps).unwrap();
2358        assert!(json["form"].is_object());
2359        assert!(json["url"].is_object());
2360
2361        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2362        assert!(roundtripped.form.is_some());
2363        assert!(roundtripped.url.is_some());
2364    }
2365
2366    #[test]
2367    fn schema_default_sets_object_type() {
2368        let schema = ElicitationSchema::default();
2369
2370        assert_eq!(schema.type_, ElicitationSchemaType::Object);
2371        assert!(schema.properties.is_empty());
2372
2373        let json = serde_json::to_value(&schema).unwrap();
2374        assert_eq!(json["type"], "object");
2375    }
2376
2377    #[test]
2378    fn schema_builder_serialization() {
2379        let schema = ElicitationSchema::new()
2380            .string("name", true)
2381            .email("email", true)
2382            .integer("age", 0, 150, true)
2383            .boolean("newsletter", false)
2384            .description("User registration");
2385
2386        let json = serde_json::to_value(&schema).unwrap();
2387        assert_eq!(json["type"], "object");
2388        assert_eq!(json["description"], "User registration");
2389        assert_eq!(json["properties"]["name"]["type"], "string");
2390        assert_eq!(json["properties"]["email"]["type"], "string");
2391        assert_eq!(json["properties"]["email"]["format"], "email");
2392        assert_eq!(json["properties"]["age"]["type"], "integer");
2393        assert_eq!(json["properties"]["age"]["minimum"], 0);
2394        assert_eq!(json["properties"]["age"]["maximum"], 150);
2395        assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
2396
2397        let required = json["required"].as_array().unwrap();
2398        assert!(required.contains(&json!("name")));
2399        assert!(required.contains(&json!("email")));
2400        assert!(required.contains(&json!("age")));
2401        assert!(!required.contains(&json!("newsletter")));
2402
2403        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2404        assert_eq!(roundtripped.properties.len(), 4);
2405        assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
2406    }
2407
2408    #[test]
2409    fn schema_string_enum_serialization() {
2410        let schema = ElicitationSchema::new().property(
2411            "color",
2412            StringPropertySchema::new().enum_values(vec![
2413                "red".into(),
2414                "green".into(),
2415                "blue".into(),
2416            ]),
2417            true,
2418        );
2419
2420        let json = serde_json::to_value(&schema).unwrap();
2421        assert_eq!(json["properties"]["color"]["type"], "string");
2422        let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
2423        assert_eq!(enum_vals.len(), 3);
2424
2425        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2426        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
2427        {
2428            assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
2429        } else {
2430            panic!("expected String variant");
2431        }
2432    }
2433
2434    #[test]
2435    fn schema_multi_select_serialization() {
2436        let schema = ElicitationSchema::new().property(
2437            "colors",
2438            MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
2439                .min_items(1)
2440                .max_items(3),
2441            false,
2442        );
2443
2444        let json = serde_json::to_value(&schema).unwrap();
2445        assert_eq!(json["properties"]["colors"]["type"], "array");
2446        assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
2447        assert_eq!(json["properties"]["colors"]["minItems"], 1);
2448        assert_eq!(json["properties"]["colors"]["maxItems"], 3);
2449
2450        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2451        let ElicitationPropertySchema::Array(array) =
2452            roundtripped.properties.get("colors").unwrap()
2453        else {
2454            panic!("expected Array variant");
2455        };
2456        let MultiSelectItems::String(items) = &array.items else {
2457            panic!("expected String multi-select items");
2458        };
2459        assert_eq!(items.values.len(), 3);
2460    }
2461
2462    #[test]
2463    fn multi_select_titled_items_keep_mcp_shape() {
2464        let items = MultiSelectItems::Titled(TitledMultiSelectItems::new(vec![EnumOption::new(
2465            "#ff0000", "Red",
2466        )]));
2467
2468        let json = serde_json::to_value(&items).unwrap();
2469        assert!(json.get("type").is_none());
2470        assert_eq!(json["anyOf"][0]["const"], "#ff0000");
2471        assert_eq!(json["anyOf"][0]["title"], "Red");
2472
2473        let roundtripped: MultiSelectItems = serde_json::from_value(json).unwrap();
2474        assert!(matches!(roundtripped, MultiSelectItems::Titled(_)));
2475    }
2476
2477    #[test]
2478    fn multi_select_items_preserve_unknown_type() {
2479        let json = json!({
2480            "type": "_token",
2481            "format": "workspace",
2482            "anyOf": [
2483                { "const": "repo", "title": "Repository" }
2484            ]
2485        });
2486
2487        let items: MultiSelectItems = serde_json::from_value(json.clone()).unwrap();
2488        let MultiSelectItems::Other(other) = &items else {
2489            panic!("expected unknown multi-select items");
2490        };
2491
2492        assert_eq!(other.type_, "_token");
2493        assert_eq!(other.fields.get("format"), Some(&json!("workspace")));
2494        assert_eq!(other.fields.get("anyOf"), Some(&json["anyOf"]));
2495        assert_eq!(serde_json::to_value(&items).unwrap(), json);
2496    }
2497
2498    #[test]
2499    fn multi_select_items_unknown_does_not_hide_malformed_string_type() {
2500        assert!(
2501            serde_json::from_value::<MultiSelectItems>(json!({
2502                "type": "string"
2503            }))
2504            .is_err()
2505        );
2506        assert!(
2507            serde_json::from_value::<OtherMultiSelectItems>(json!({
2508                "type": "string",
2509                "format": "workspace"
2510            }))
2511            .is_err()
2512        );
2513    }
2514
2515    #[test]
2516    fn property_schema_preserves_unknown_type() {
2517        let schema: ElicitationSchema = serde_json::from_value(json!({
2518            "type": "object",
2519            "properties": {
2520                "location": {
2521                    "type": "_location",
2522                    "title": "Location",
2523                    "precision": "city"
2524                }
2525            }
2526        }))
2527        .unwrap();
2528
2529        let ElicitationPropertySchema::Other(unknown) = schema.properties.get("location").unwrap()
2530        else {
2531            panic!("expected unknown property schema");
2532        };
2533
2534        assert_eq!(unknown.type_, "_location");
2535        assert_eq!(unknown.fields.get("title"), Some(&json!("Location")));
2536        assert_eq!(unknown.fields.get("precision"), Some(&json!("city")));
2537        assert_eq!(
2538            serde_json::to_value(ElicitationPropertySchema::Other(unknown.clone())).unwrap(),
2539            json!({
2540                "type": "_location",
2541                "title": "Location",
2542                "precision": "city"
2543            })
2544        );
2545    }
2546
2547    #[test]
2548    fn property_schema_unknown_does_not_hide_malformed_known_type() {
2549        assert!(
2550            serde_json::from_value::<ElicitationPropertySchema>(json!({
2551                "type": "array"
2552            }))
2553            .is_err()
2554        );
2555        assert!(serde_json::from_value::<ElicitationPropertySchema>(json!({})).is_err());
2556    }
2557
2558    #[test]
2559    fn schema_titled_enum_serialization() {
2560        let schema = ElicitationSchema::new().property(
2561            "country",
2562            StringPropertySchema::new().one_of(vec![
2563                EnumOption::new("us", "United States"),
2564                EnumOption::new("uk", "United Kingdom"),
2565            ]),
2566            true,
2567        );
2568
2569        let json = serde_json::to_value(&schema).unwrap();
2570        assert_eq!(json["properties"]["country"]["type"], "string");
2571        let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
2572        assert_eq!(one_of.len(), 2);
2573        assert_eq!(one_of[0]["const"], "us");
2574        assert_eq!(one_of[0]["title"], "United States");
2575
2576        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2577        if let ElicitationPropertySchema::String(s) =
2578            roundtripped.properties.get("country").unwrap()
2579        {
2580            assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
2581        } else {
2582            panic!("expected String variant");
2583        }
2584    }
2585
2586    #[test]
2587    fn schema_number_property_serialization() {
2588        let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
2589
2590        let json = serde_json::to_value(&schema).unwrap();
2591        assert_eq!(json["properties"]["rating"]["type"], "number");
2592        assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
2593        assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
2594
2595        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2596        if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
2597        {
2598            assert_eq!(n.minimum, Some(0.0));
2599            assert_eq!(n.maximum, Some(5.0));
2600        } else {
2601            panic!("expected Number variant");
2602        }
2603    }
2604
2605    #[test]
2606    fn schema_string_format_serialization() {
2607        let schema = ElicitationSchema::new()
2608            .uri("website", true)
2609            .date("birthday", true)
2610            .date_time("updated_at", false);
2611
2612        let json = serde_json::to_value(&schema).unwrap();
2613        assert_eq!(json["properties"]["website"]["type"], "string");
2614        assert_eq!(json["properties"]["website"]["format"], "uri");
2615        assert_eq!(json["properties"]["birthday"]["type"], "string");
2616        assert_eq!(json["properties"]["birthday"]["format"], "date");
2617        assert_eq!(json["properties"]["updated_at"]["type"], "string");
2618        assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
2619
2620        let required = json["required"].as_array().unwrap();
2621        assert!(required.contains(&json!("website")));
2622        assert!(required.contains(&json!("birthday")));
2623        assert!(!required.contains(&json!("updated_at")));
2624    }
2625
2626    #[test]
2627    fn schema_string_pattern_serialization() {
2628        let schema = ElicitationSchema::new().property(
2629            "name",
2630            StringPropertySchema::new()
2631                .min_length(1)
2632                .max_length(64)
2633                .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
2634            true,
2635        );
2636
2637        let json = serde_json::to_value(&schema).unwrap();
2638        assert_eq!(json["properties"]["name"]["type"], "string");
2639        assert_eq!(
2640            json["properties"]["name"]["pattern"],
2641            "^[a-zA-Z_][a-zA-Z0-9_]*$"
2642        );
2643
2644        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2645        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
2646            assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
2647        } else {
2648            panic!("expected String variant");
2649        }
2650    }
2651
2652    #[test]
2653    fn schema_property_updates_required_state() {
2654        let schema = ElicitationSchema::new()
2655            .string("name", true)
2656            .email("name", false);
2657
2658        let json = serde_json::to_value(&schema).unwrap();
2659        assert!(json.get("required").is_none());
2660        assert_eq!(json["properties"]["name"]["format"], "email");
2661    }
2662
2663    #[test]
2664    fn schema_defaults_invalid_object_type() {
2665        let schema = serde_json::from_value::<ElicitationSchema>(json!({
2666            "type": "array",
2667            "properties": {
2668                "name": {
2669                    "type": "string"
2670                }
2671            }
2672        }))
2673        .unwrap();
2674
2675        assert_eq!(schema.type_, ElicitationSchemaType::Object);
2676        assert!(schema.properties.contains_key("name"));
2677    }
2678
2679    #[test]
2680    fn titled_multi_select_items_reject_one_of() {
2681        let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
2682            "oneOf": [
2683                {
2684                    "const": "red",
2685                    "title": "Red"
2686                }
2687            ]
2688        }))
2689        .unwrap_err();
2690
2691        assert!(err.to_string().contains("missing field `anyOf`"));
2692    }
2693
2694    #[test]
2695    fn response_accept_rejects_non_object_content() {
2696        assert!(
2697            serde_json::from_value::<CreateElicitationResponse>(json!({
2698                "action": "accept",
2699                "content": "Alice"
2700            }))
2701            .is_err()
2702        );
2703    }
2704
2705    #[test]
2706    fn response_accept_rejects_nested_object_content() {
2707        assert!(
2708            serde_json::from_value::<CreateElicitationResponse>(json!({
2709                "action": "accept",
2710                "content": {
2711                    "profile": {
2712                        "name": "Alice"
2713                    }
2714                }
2715            }))
2716            .is_err()
2717        );
2718    }
2719
2720    #[test]
2721    fn response_accept_allows_primitive_and_string_array_content() {
2722        let response = CreateElicitationResponse::new(ElicitationAction::Accept(
2723            ElicitationAcceptAction::new().content(BTreeMap::from([
2724                ("name".to_string(), ElicitationContentValue::from("Alice")),
2725                ("age".to_string(), ElicitationContentValue::from(30_i32)),
2726                ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
2727                (
2728                    "subscribed".to_string(),
2729                    ElicitationContentValue::from(true),
2730                ),
2731                (
2732                    "tags".to_string(),
2733                    ElicitationContentValue::from(vec!["rust", "acp"]),
2734                ),
2735            ])),
2736        ));
2737
2738        let json = serde_json::to_value(&response).unwrap();
2739        assert_eq!(json["action"], "accept");
2740        assert_eq!(json["content"]["name"], "Alice");
2741        assert_eq!(json["content"]["age"], 30);
2742        assert_eq!(json["content"]["score"], 9.5);
2743        assert_eq!(json["content"]["subscribed"], true);
2744        assert_eq!(json["content"]["tags"][0], "rust");
2745        assert_eq!(json["content"]["tags"][1], "acp");
2746    }
2747}