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