Skip to main content

agent_client_protocol_schema/v2/
content.rs

1//! Content blocks for representing various types of information in the Agent Client Protocol.
2//!
3//! This module defines the core content types used throughout the protocol for communication
4//! between agents and clients. Content blocks provide a flexible, extensible way to represent
5//! text, images, audio, and other resources in prompts, responses, and tool call results.
6//!
7//! The content block structure is designed to be compatible with the Model Context Protocol (MCP),
8//! allowing seamless integration between ACP and MCP-based tools.
9//!
10//! See: [Content](https://agentclientprotocol.com/protocol/content)
11
12use std::collections::BTreeMap;
13
14use schemars::{JsonSchema, Schema};
15use serde::{Deserialize, Serialize};
16use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
17
18use super::Meta;
19use crate::{IntoOption, SkipListener};
20
21/// Content blocks represent displayable information in the Agent Client Protocol.
22///
23/// They provide a structured way to handle various types of user-facing content—whether
24/// it's text from language models, images for analysis, or embedded resources for context.
25///
26/// Content blocks appear in:
27/// - User prompts sent via `session/prompt`
28/// - Language model output reported through `session/update` notifications as
29///   message updates or streamed chunks
30/// - Progress updates and results from tool calls
31///
32/// This structure is compatible with the Model Context Protocol (MCP), enabling
33/// agents to seamlessly forward content from MCP tool outputs without transformation.
34///
35/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
36#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
37#[serde(tag = "type", rename_all = "snake_case")]
38#[schemars(extend("discriminator" = {"propertyName": "type"}))]
39#[non_exhaustive]
40pub enum ContentBlock {
41    /// Text content. May be plain text or formatted with Markdown.
42    ///
43    /// All agents MUST support text content blocks in prompts.
44    /// Clients SHOULD render this text as Markdown.
45    Text(TextContent),
46    /// Images for visual context or analysis.
47    ///
48    /// Requires the `image` prompt capability when included in prompts.
49    Image(ImageContent),
50    /// Audio data for transcription or analysis.
51    ///
52    /// Requires the `audio` prompt capability when included in prompts.
53    Audio(AudioContent),
54    /// References to resources that the agent can access.
55    ///
56    /// All agents MUST support resource links in prompts.
57    ResourceLink(ResourceLink),
58    /// Complete resource contents embedded directly in the message.
59    ///
60    /// Preferred for including context as it avoids extra round-trips.
61    ///
62    /// Requires the `embeddedContext` prompt capability when included in prompts.
63    Resource(EmbeddedResource),
64    /// Custom or future content block.
65    ///
66    /// Values beginning with `_` are reserved for implementation-specific
67    /// extensions. Unknown values that do not begin with `_` are reserved for
68    /// future ACP variants.
69    ///
70    /// Receivers that do not understand this content block type should preserve
71    /// the raw payload when storing, replaying, proxying, or forwarding content,
72    /// and otherwise ignore it or display it generically.
73    #[serde(untagged)]
74    Other(OtherContentBlock),
75}
76
77/// Custom or future content block payload.
78#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
79#[schemars(inline)]
80#[schemars(transform = other_content_block_schema)]
81#[serde(rename_all = "camelCase")]
82#[non_exhaustive]
83pub struct OtherContentBlock {
84    /// Custom or future content block type.
85    ///
86    /// Values beginning with `_` are reserved for implementation-specific
87    /// extensions. Unknown values that do not begin with `_` are reserved for
88    /// future ACP variants.
89    #[serde(rename = "type")]
90    pub type_: String,
91    /// Additional fields from the unknown content block payload.
92    #[serde(flatten)]
93    pub fields: BTreeMap<String, serde_json::Value>,
94}
95
96impl OtherContentBlock {
97    /// Builds [`OtherContentBlock`] from an unknown discriminator and preserves the remaining extension fields.
98    #[must_use]
99    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
100        fields.remove("type");
101        Self {
102            type_: type_.into(),
103            fields,
104        }
105    }
106}
107
108impl<'de> Deserialize<'de> for OtherContentBlock {
109    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110    where
111        D: serde::Deserializer<'de>,
112    {
113        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
114        let type_ = fields
115            .remove("type")
116            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
117        let serde_json::Value::String(type_) = type_ else {
118            return Err(serde::de::Error::custom("`type` must be a string"));
119        };
120
121        if is_known_content_block_type(&type_) {
122            return Err(serde::de::Error::custom(format!(
123                "known content block `{type_}` did not match its schema"
124            )));
125        }
126
127        Ok(Self { type_, fields })
128    }
129}
130
131fn is_known_content_block_type(type_: &str) -> bool {
132    matches!(
133        type_,
134        "text" | "image" | "audio" | "resource_link" | "resource"
135    )
136}
137
138fn other_content_block_schema(schema: &mut Schema) {
139    super::schema_util::reject_known_string_discriminators(
140        schema,
141        "type",
142        &["text", "image", "audio", "resource_link", "resource"],
143    );
144}
145
146/// Text provided to or from an LLM.
147#[serde_as]
148#[skip_serializing_none]
149#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
150#[non_exhaustive]
151pub struct TextContent {
152    /// Text payload carried by this content block.
153    pub text: String,
154    /// Optional annotations that help clients decide how to display or route this content.
155    #[serde_as(deserialize_as = "DefaultOnError")]
156    #[schemars(extend("x-deserialize-default-on-error" = true))]
157    #[serde(default)]
158    pub annotations: Option<Annotations>,
159    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
160    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
161    /// these keys.
162    ///
163    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
164    #[serde_as(deserialize_as = "DefaultOnError")]
165    #[schemars(extend("x-deserialize-default-on-error" = true))]
166    #[serde(default)]
167    #[serde(rename = "_meta")]
168    pub meta: Option<Meta>,
169}
170
171impl TextContent {
172    /// Builds [`TextContent`] with its required content payload; optional annotations and metadata start unset.
173    #[must_use]
174    pub fn new(text: impl Into<String>) -> Self {
175        Self {
176            annotations: None,
177            text: text.into(),
178            meta: None,
179        }
180    }
181
182    /// Sets or clears the optional `annotations` field.
183    #[must_use]
184    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
185        self.annotations = annotations.into_option();
186        self
187    }
188
189    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
190    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
191    /// these keys.
192    ///
193    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
194    #[must_use]
195    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
196        self.meta = meta.into_option();
197        self
198    }
199}
200
201impl<T: Into<String>> From<T> for ContentBlock {
202    fn from(value: T) -> Self {
203        Self::Text(TextContent::new(value))
204    }
205}
206
207/// An image provided to or from an LLM.
208#[serde_as]
209#[skip_serializing_none]
210#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
211#[serde(rename_all = "camelCase")]
212#[non_exhaustive]
213pub struct ImageContent {
214    /// Base64-encoded media payload.
215    #[schemars(extend("format" = "byte"))]
216    pub data: String,
217    /// MIME type describing the encoded media payload.
218    pub mime_type: String,
219    /// URI associated with this resource or media payload.
220    #[serde_as(deserialize_as = "DefaultOnError")]
221    #[schemars(extend("x-deserialize-default-on-error" = true))]
222    #[schemars(url)]
223    #[serde(default)]
224    pub uri: Option<String>,
225    /// Optional annotations that help clients decide how to display or route this content.
226    #[serde_as(deserialize_as = "DefaultOnError")]
227    #[schemars(extend("x-deserialize-default-on-error" = true))]
228    #[serde(default)]
229    pub annotations: Option<Annotations>,
230    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
231    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
232    /// these keys.
233    ///
234    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
235    #[serde_as(deserialize_as = "DefaultOnError")]
236    #[schemars(extend("x-deserialize-default-on-error" = true))]
237    #[serde(default)]
238    #[serde(rename = "_meta")]
239    pub meta: Option<Meta>,
240}
241
242impl ImageContent {
243    /// Builds [`ImageContent`] with its required content payload; optional annotations and metadata start unset.
244    #[must_use]
245    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
246        Self {
247            annotations: None,
248            data: data.into(),
249            mime_type: mime_type.into(),
250            uri: None,
251            meta: None,
252        }
253    }
254
255    /// Sets or clears the optional `annotations` field.
256    #[must_use]
257    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
258        self.annotations = annotations.into_option();
259        self
260    }
261
262    /// Sets or clears the optional `uri` field.
263    #[must_use]
264    pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
265        self.uri = uri.into_option();
266        self
267    }
268
269    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
270    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
271    /// these keys.
272    ///
273    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
274    #[must_use]
275    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
276        self.meta = meta.into_option();
277        self
278    }
279}
280
281/// Audio provided to or from an LLM.
282#[serde_as]
283#[skip_serializing_none]
284#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
285#[serde(rename_all = "camelCase")]
286#[non_exhaustive]
287pub struct AudioContent {
288    /// Base64-encoded media payload.
289    #[schemars(extend("format" = "byte"))]
290    pub data: String,
291    /// MIME type describing the encoded media payload.
292    pub mime_type: String,
293    /// Optional annotations that help clients decide how to display or route this content.
294    #[serde_as(deserialize_as = "DefaultOnError")]
295    #[schemars(extend("x-deserialize-default-on-error" = true))]
296    #[serde(default)]
297    pub annotations: Option<Annotations>,
298    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
299    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
300    /// these keys.
301    ///
302    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
303    #[serde_as(deserialize_as = "DefaultOnError")]
304    #[schemars(extend("x-deserialize-default-on-error" = true))]
305    #[serde(default)]
306    #[serde(rename = "_meta")]
307    pub meta: Option<Meta>,
308}
309
310impl AudioContent {
311    /// Builds [`AudioContent`] with its required content payload; optional annotations and metadata start unset.
312    #[must_use]
313    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
314        Self {
315            annotations: None,
316            data: data.into(),
317            mime_type: mime_type.into(),
318            meta: None,
319        }
320    }
321
322    /// Sets or clears the optional `annotations` field.
323    #[must_use]
324    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
325        self.annotations = annotations.into_option();
326        self
327    }
328
329    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
330    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
331    /// these keys.
332    ///
333    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
334    #[must_use]
335    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
336        self.meta = meta.into_option();
337        self
338    }
339}
340
341/// The contents of a resource, embedded into a prompt or tool call result.
342#[serde_as]
343#[skip_serializing_none]
344#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
345#[non_exhaustive]
346pub struct EmbeddedResource {
347    /// Embedded resource payload, either text or binary data.
348    pub resource: EmbeddedResourceResource,
349    /// Optional annotations that help clients decide how to display or route this content.
350    #[serde_as(deserialize_as = "DefaultOnError")]
351    #[schemars(extend("x-deserialize-default-on-error" = true))]
352    #[serde(default)]
353    pub annotations: Option<Annotations>,
354    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
355    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
356    /// these keys.
357    ///
358    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
359    #[serde_as(deserialize_as = "DefaultOnError")]
360    #[schemars(extend("x-deserialize-default-on-error" = true))]
361    #[serde(default)]
362    #[serde(rename = "_meta")]
363    pub meta: Option<Meta>,
364}
365
366impl EmbeddedResource {
367    /// Builds [`EmbeddedResource`] with its required content payload; optional annotations and metadata start unset.
368    #[must_use]
369    pub fn new(resource: EmbeddedResourceResource) -> Self {
370        Self {
371            annotations: None,
372            resource,
373            meta: None,
374        }
375    }
376
377    /// Sets or clears the optional `annotations` field.
378    #[must_use]
379    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
380        self.annotations = annotations.into_option();
381        self
382    }
383
384    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
385    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
386    /// these keys.
387    ///
388    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
389    #[must_use]
390    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
391        self.meta = meta.into_option();
392        self
393    }
394}
395
396/// Resource content that can be embedded in a message.
397#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
398#[serde(untagged)]
399#[non_exhaustive]
400pub enum EmbeddedResourceResource {
401    /// Text resource contents embedded directly in the message.
402    TextResourceContents(TextResourceContents),
403    /// Binary resource contents embedded directly in the message.
404    BlobResourceContents(BlobResourceContents),
405}
406
407/// Text-based resource contents.
408#[serde_as]
409#[skip_serializing_none]
410#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
411#[serde(rename_all = "camelCase")]
412#[non_exhaustive]
413pub struct TextResourceContents {
414    /// Text payload carried by this content block.
415    pub text: String,
416    /// URI associated with this resource or media payload.
417    #[schemars(url)]
418    pub uri: String,
419    /// MIME type describing the encoded media payload.
420    #[serde_as(deserialize_as = "DefaultOnError")]
421    #[schemars(extend("x-deserialize-default-on-error" = true))]
422    #[serde(default)]
423    pub mime_type: Option<String>,
424    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
425    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
426    /// these keys.
427    ///
428    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
429    #[serde_as(deserialize_as = "DefaultOnError")]
430    #[schemars(extend("x-deserialize-default-on-error" = true))]
431    #[serde(default)]
432    #[serde(rename = "_meta")]
433    pub meta: Option<Meta>,
434}
435
436impl TextResourceContents {
437    /// Builds [`TextResourceContents`] with its required content payload; optional annotations and metadata start unset.
438    #[must_use]
439    pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
440        Self {
441            mime_type: None,
442            text: text.into(),
443            uri: uri.into(),
444            meta: None,
445        }
446    }
447
448    /// Sets or clears the optional `mimeType` field.
449    #[must_use]
450    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
451        self.mime_type = mime_type.into_option();
452        self
453    }
454
455    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
456    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
457    /// these keys.
458    ///
459    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
460    #[must_use]
461    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
462        self.meta = meta.into_option();
463        self
464    }
465}
466
467/// Binary resource contents.
468#[serde_as]
469#[skip_serializing_none]
470#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
471#[serde(rename_all = "camelCase")]
472#[non_exhaustive]
473pub struct BlobResourceContents {
474    /// Base64-encoded bytes for a binary resource payload.
475    #[schemars(extend("format" = "byte"))]
476    pub blob: String,
477    /// URI associated with this resource or media payload.
478    #[schemars(url)]
479    pub uri: String,
480    /// MIME type describing the encoded media payload.
481    #[serde_as(deserialize_as = "DefaultOnError")]
482    #[schemars(extend("x-deserialize-default-on-error" = true))]
483    #[serde(default)]
484    pub mime_type: Option<String>,
485    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
486    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
487    /// these keys.
488    ///
489    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
490    #[serde_as(deserialize_as = "DefaultOnError")]
491    #[schemars(extend("x-deserialize-default-on-error" = true))]
492    #[serde(default)]
493    #[serde(rename = "_meta")]
494    pub meta: Option<Meta>,
495}
496
497impl BlobResourceContents {
498    /// Builds [`BlobResourceContents`] with its required content payload; optional annotations and metadata start unset.
499    #[must_use]
500    pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
501        Self {
502            blob: blob.into(),
503            mime_type: None,
504            uri: uri.into(),
505            meta: None,
506        }
507    }
508
509    /// Sets or clears the optional `mimeType` field.
510    #[must_use]
511    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
512        self.mime_type = mime_type.into_option();
513        self
514    }
515
516    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
517    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
518    /// these keys.
519    ///
520    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
521    #[must_use]
522    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
523        self.meta = meta.into_option();
524        self
525    }
526}
527
528/// A resource that the server is capable of reading, included in a prompt or tool call result.
529#[serde_as]
530#[skip_serializing_none]
531#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
532#[serde(rename_all = "camelCase")]
533#[non_exhaustive]
534pub struct ResourceLink {
535    /// Human-readable name shown for this protocol object.
536    pub name: String,
537    /// URI associated with this resource or media payload.
538    #[schemars(url)]
539    pub uri: String,
540    /// Optional display title for end-user UI.
541    #[serde_as(deserialize_as = "DefaultOnError")]
542    #[schemars(extend("x-deserialize-default-on-error" = true))]
543    #[serde(default)]
544    pub title: Option<String>,
545    /// Optional human-readable details shown with this protocol object.
546    #[serde_as(deserialize_as = "DefaultOnError")]
547    #[schemars(extend("x-deserialize-default-on-error" = true))]
548    #[serde(default)]
549    pub description: Option<String>,
550    /// Optional set of sized icons that the client can display in a user interface.
551    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
552    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
553    #[serde(default)]
554    pub icons: Option<Vec<Icon>>,
555    /// MIME type describing the encoded media payload.
556    #[serde_as(deserialize_as = "DefaultOnError")]
557    #[schemars(extend("x-deserialize-default-on-error" = true))]
558    #[serde(default)]
559    pub mime_type: Option<String>,
560    /// Optional size of the linked resource in bytes, if known.
561    #[serde_as(deserialize_as = "DefaultOnError")]
562    #[schemars(extend("x-deserialize-default-on-error" = true))]
563    #[serde(default)]
564    pub size: Option<i64>,
565    /// Optional annotations that help clients decide how to display or route this content.
566    #[serde_as(deserialize_as = "DefaultOnError")]
567    #[schemars(extend("x-deserialize-default-on-error" = true))]
568    #[serde(default)]
569    pub annotations: Option<Annotations>,
570    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
571    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
572    /// these keys.
573    ///
574    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
575    #[serde_as(deserialize_as = "DefaultOnError")]
576    #[schemars(extend("x-deserialize-default-on-error" = true))]
577    #[serde(default)]
578    #[serde(rename = "_meta")]
579    pub meta: Option<Meta>,
580}
581
582impl ResourceLink {
583    /// Builds [`ResourceLink`] with its required content payload; optional annotations and metadata start unset.
584    #[must_use]
585    pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
586        Self {
587            annotations: None,
588            description: None,
589            icons: None,
590            mime_type: None,
591            name: name.into(),
592            size: None,
593            title: None,
594            uri: uri.into(),
595            meta: None,
596        }
597    }
598
599    /// Sets or clears the optional `annotations` field.
600    #[must_use]
601    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
602        self.annotations = annotations.into_option();
603        self
604    }
605
606    /// Sets or clears the optional `description` field.
607    #[must_use]
608    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
609        self.description = description.into_option();
610        self
611    }
612
613    /// Sets or clears the optional `icons` field.
614    #[must_use]
615    pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
616        self.icons = icons.into_option();
617        self
618    }
619
620    /// Sets or clears the optional `mimeType` field.
621    #[must_use]
622    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
623        self.mime_type = mime_type.into_option();
624        self
625    }
626
627    /// Sets or clears the optional `size` field.
628    #[must_use]
629    pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
630        self.size = size.into_option();
631        self
632    }
633
634    /// Sets or clears the optional `title` field.
635    #[must_use]
636    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
637        self.title = title.into_option();
638        self
639    }
640
641    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
642    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
643    /// these keys.
644    ///
645    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
646    #[must_use]
647    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
648        self.meta = meta.into_option();
649        self
650    }
651}
652
653/// An optionally-sized icon that can be displayed in a user interface.
654#[serde_as]
655#[skip_serializing_none]
656#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
657#[serde(rename_all = "camelCase")]
658#[non_exhaustive]
659pub struct Icon {
660    /// A standard URI pointing to an icon resource.
661    #[schemars(url)]
662    pub src: String,
663    /// Optional MIME type override if the source MIME type is missing or generic.
664    #[serde_as(deserialize_as = "DefaultOnError")]
665    #[schemars(extend("x-deserialize-default-on-error" = true))]
666    #[serde(default)]
667    pub mime_type: Option<String>,
668    /// Optional sizes at which the icon can be used.
669    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
670    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
671    #[serde(default)]
672    pub sizes: Option<Vec<String>>,
673    /// Optional theme this icon is designed for.
674    #[serde_as(deserialize_as = "DefaultOnError")]
675    #[schemars(extend("x-deserialize-default-on-error" = true))]
676    #[serde(default)]
677    pub theme: Option<IconTheme>,
678}
679
680impl Icon {
681    /// Builds [`Icon`] with the required source URI; optional display hints start unset.
682    #[must_use]
683    pub fn new(src: impl Into<String>) -> Self {
684        Self {
685            src: src.into(),
686            mime_type: None,
687            sizes: None,
688            theme: None,
689        }
690    }
691
692    /// Sets or clears the optional `mimeType` field.
693    #[must_use]
694    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
695        self.mime_type = mime_type.into_option();
696        self
697    }
698
699    /// Sets or clears the optional `sizes` field.
700    #[must_use]
701    pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
702        self.sizes = sizes.into_option();
703        self
704    }
705
706    /// Sets or clears the optional `theme` field.
707    #[must_use]
708    pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
709        self.theme = theme.into_option();
710        self
711    }
712}
713
714/// Theme an icon is designed for.
715#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
716#[serde(rename_all = "camelCase")]
717#[non_exhaustive]
718pub enum IconTheme {
719    /// Icon designed for light backgrounds.
720    Light,
721    /// Icon designed for dark backgrounds.
722    Dark,
723    /// Custom or future icon theme.
724    ///
725    /// Values beginning with `_` are reserved for implementation-specific
726    /// extensions. Unknown values that do not begin with `_` are reserved for
727    /// future ACP variants.
728    #[serde(untagged)]
729    Other(String),
730}
731
732/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
733#[serde_as]
734#[skip_serializing_none]
735#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
736#[serde(rename_all = "camelCase")]
737#[non_exhaustive]
738pub struct Annotations {
739    /// Intended recipients for this content, such as the user or assistant.
740    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
741    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
742    #[serde(default)]
743    pub audience: Option<Vec<Role>>,
744    /// Timestamp indicating when the underlying resource was last modified.
745    ///
746    /// Must be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z").
747    #[serde_as(deserialize_as = "DefaultOnError")]
748    #[schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time"))]
749    #[serde(default)]
750    pub last_modified: Option<String>,
751    /// Relative importance of this content when clients choose what to surface.
752    #[serde_as(deserialize_as = "DefaultOnError")]
753    #[schemars(extend("x-deserialize-default-on-error" = true))]
754    #[schemars(range(min = 0, max = 1))]
755    #[serde(default)]
756    pub priority: Option<f64>,
757    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
758    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
759    /// these keys.
760    ///
761    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
762    #[serde_as(deserialize_as = "DefaultOnError")]
763    #[schemars(extend("x-deserialize-default-on-error" = true))]
764    #[serde(default)]
765    #[serde(rename = "_meta")]
766    pub meta: Option<Meta>,
767}
768
769impl Annotations {
770    /// Creates annotations with no audience, priority, or timestamp hints set.
771    #[must_use]
772    pub fn new() -> Self {
773        Self::default()
774    }
775
776    /// Sets or clears the optional `audience` field.
777    #[must_use]
778    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
779        self.audience = audience.into_option();
780        self
781    }
782
783    /// Sets or clears the optional `lastModified` field.
784    #[must_use]
785    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
786        self.last_modified = last_modified.into_option();
787        self
788    }
789
790    /// Sets or clears the optional `priority` field.
791    #[must_use]
792    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
793        self.priority = priority.into_option();
794        self
795    }
796
797    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
798    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
799    /// these keys.
800    ///
801    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
802    #[must_use]
803    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
804        self.meta = meta.into_option();
805        self
806    }
807}
808
809/// The sender or recipient of messages and data in a conversation.
810#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
811#[serde(rename_all = "camelCase")]
812#[non_exhaustive]
813pub enum Role {
814    /// The assistant side of a conversation.
815    Assistant,
816    /// The user side of a conversation.
817    User,
818    /// Custom or future role.
819    ///
820    /// Values beginning with `_` are reserved for implementation-specific
821    /// extensions. Unknown values that do not begin with `_` are reserved for
822    /// future ACP variants.
823    #[serde(untagged)]
824    Other(String),
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830
831    #[test]
832    fn test_text_content_roundtrip() {
833        let content = TextContent::new("hello world");
834        let json = serde_json::to_value(&content).unwrap();
835        let parsed: TextContent = serde_json::from_value(json).unwrap();
836        assert_eq!(content, parsed);
837    }
838
839    #[test]
840    fn test_text_content_omits_optional_fields() {
841        let content = TextContent::new("hello");
842        let json = serde_json::to_value(&content).unwrap();
843        assert!(!json.as_object().unwrap().contains_key("annotations"));
844        assert!(!json.as_object().unwrap().contains_key("meta"));
845    }
846
847    #[test]
848    fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
849        let missing: TextContent = serde_json::from_value(serde_json::json!({
850            "text": "hello"
851        }))
852        .unwrap();
853        assert_eq!(missing.meta, None);
854
855        let malformed: TextContent = serde_json::from_value(serde_json::json!({
856            "text": "hello",
857            "_meta": false
858        }))
859        .unwrap();
860        assert_eq!(malformed.meta, None);
861    }
862
863    #[test]
864    fn test_text_content_from_string() {
865        let block: ContentBlock = "hello".into();
866        match block {
867            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
868            _ => panic!("Expected Text variant"),
869        }
870    }
871
872    #[test]
873    fn role_preserves_unknown_variant() {
874        let role: Role = serde_json::from_str("\"critic\"").unwrap();
875        assert_eq!(role, Role::Other("critic".to_string()));
876        assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
877    }
878
879    #[test]
880    fn icon_theme_preserves_unknown_variant() {
881        let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
882        assert_eq!(theme, IconTheme::Other("contrast".to_string()));
883        assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
884    }
885
886    #[test]
887    fn content_block_preserves_unknown_variant() {
888        let block: ContentBlock = serde_json::from_value(serde_json::json!({
889            "type": "_widget",
890            "title": "Status",
891            "state": {"ok": true}
892        }))
893        .unwrap();
894
895        let ContentBlock::Other(unknown) = block else {
896            panic!("expected unknown content block");
897        };
898
899        assert_eq!(unknown.type_, "_widget");
900        assert_eq!(
901            unknown.fields.get("title"),
902            Some(&serde_json::json!("Status"))
903        );
904        assert_eq!(
905            serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
906            serde_json::json!({
907                "type": "_widget",
908                "title": "Status",
909                "state": {"ok": true}
910            })
911        );
912    }
913
914    #[test]
915    fn content_block_does_not_hide_malformed_known_variant() {
916        assert!(
917            serde_json::from_value::<ContentBlock>(serde_json::json!({
918                "type": "text"
919            }))
920            .is_err()
921        );
922    }
923
924    #[test]
925    fn test_image_content_roundtrip() {
926        let content = ImageContent::new("base64data", "image/png");
927        let json = serde_json::to_value(&content).unwrap();
928        let parsed: ImageContent = serde_json::from_value(json).unwrap();
929        assert_eq!(content, parsed);
930    }
931
932    #[test]
933    fn test_image_content_omits_optional_fields() {
934        let content = ImageContent::new("data", "image/png");
935        let json = serde_json::to_value(&content).unwrap();
936        assert!(!json.as_object().unwrap().contains_key("uri"));
937        assert!(!json.as_object().unwrap().contains_key("annotations"));
938        assert!(!json.as_object().unwrap().contains_key("meta"));
939    }
940
941    #[test]
942    fn test_image_content_with_uri() {
943        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
944        let json = serde_json::to_value(&content).unwrap();
945        assert_eq!(json["uri"], "https://example.com/image.png");
946    }
947
948    #[test]
949    fn test_audio_content_roundtrip() {
950        let content = AudioContent::new("base64audio", "audio/mp3");
951        let json = serde_json::to_value(&content).unwrap();
952        let parsed: AudioContent = serde_json::from_value(json).unwrap();
953        assert_eq!(content, parsed);
954    }
955
956    #[test]
957    fn test_audio_content_omits_optional_fields() {
958        let content = AudioContent::new("data", "audio/mp3");
959        let json = serde_json::to_value(&content).unwrap();
960        assert!(!json.as_object().unwrap().contains_key("annotations"));
961        assert!(!json.as_object().unwrap().contains_key("meta"));
962    }
963
964    #[test]
965    fn resource_link_icons_roundtrip() {
966        let icon = Icon::new("https://example.com/icon.png")
967            .mime_type("image/png")
968            .sizes(vec!["48x48".to_string(), "any".to_string()])
969            .theme(IconTheme::Dark);
970        let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
971
972        let json = serde_json::to_value(&link).unwrap();
973        assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
974        assert_eq!(json["icons"][0]["mimeType"], "image/png");
975        assert_eq!(json["icons"][0]["sizes"][0], "48x48");
976        assert_eq!(json["icons"][0]["theme"], "dark");
977
978        let parsed: ResourceLink = serde_json::from_value(json).unwrap();
979        assert_eq!(link, parsed);
980    }
981
982    #[test]
983    fn annotations_priority_schema_matches_mcp_bounds() {
984        let schema = schemars::schema_for!(Annotations);
985        let json = serde_json::to_value(schema).unwrap();
986
987        assert_eq!(json["properties"]["priority"]["minimum"], 0);
988        assert_eq!(json["properties"]["priority"]["maximum"], 1);
989        assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
990    }
991
992    #[test]
993    fn content_schema_matches_mcp_string_formats() {
994        let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
995        assert_eq!(image["properties"]["data"]["format"], "byte");
996        assert_eq!(image["properties"]["uri"]["format"], "uri");
997
998        let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
999        assert_eq!(audio["properties"]["data"]["format"], "byte");
1000
1001        let text_resource =
1002            serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1003        assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1004
1005        let blob_resource =
1006            serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1007        assert_eq!(blob_resource["properties"]["blob"]["format"], "byte");
1008        assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1009
1010        let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1011        assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1012
1013        let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1014        assert_eq!(icon["properties"]["src"]["format"], "uri");
1015    }
1016}