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