Skip to main content

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