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