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