Skip to main content

agent_client_protocol_schema/v1/
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 schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
15
16use crate::{IntoOption, Meta, SkipListener};
17
18/// Content blocks represent displayable information in the Agent Client Protocol.
19///
20/// They provide a structured way to handle various types of user-facing content—whether
21/// it's text from language models, images for analysis, or embedded resources for context.
22///
23/// Content blocks appear in:
24/// - User prompts sent via `session/prompt`
25/// - Language model output streamed through `session/update` notifications
26/// - Progress updates and results from tool calls
27///
28/// This structure is compatible with the Model Context Protocol (MCP), enabling
29/// agents to seamlessly forward content from MCP tool outputs without transformation.
30///
31/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
32#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
33#[serde(tag = "type", rename_all = "snake_case")]
34#[schemars(extend("discriminator" = {"propertyName": "type"}))]
35#[non_exhaustive]
36pub enum ContentBlock {
37    /// Text content. May be plain text or formatted with Markdown.
38    ///
39    /// All agents MUST support text content blocks in prompts.
40    /// Clients SHOULD render this text as Markdown.
41    Text(TextContent),
42    /// Images for visual context or analysis.
43    ///
44    /// Requires the `image` prompt capability when included in prompts.
45    Image(ImageContent),
46    /// Audio data for transcription or analysis.
47    ///
48    /// Requires the `audio` prompt capability when included in prompts.
49    Audio(AudioContent),
50    /// References to resources that the agent can access.
51    ///
52    /// All agents MUST support resource links in prompts.
53    ResourceLink(ResourceLink),
54    /// Complete resource contents embedded directly in the message.
55    ///
56    /// Preferred for including context as it avoids extra round-trips.
57    ///
58    /// Requires the `embeddedContext` prompt capability when included in prompts.
59    Resource(EmbeddedResource),
60}
61
62/// Text provided to or from an LLM.
63#[serde_as]
64#[skip_serializing_none]
65#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
66#[non_exhaustive]
67pub struct TextContent {
68    #[serde_as(deserialize_as = "DefaultOnError")]
69    #[schemars(extend("x-deserialize-default-on-error" = true))]
70    #[serde(default)]
71    pub annotations: Option<Annotations>,
72    pub text: String,
73    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
74    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
75    /// these keys.
76    ///
77    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
78    #[serde(rename = "_meta")]
79    pub meta: Option<Meta>,
80}
81
82impl TextContent {
83    #[must_use]
84    pub fn new(text: impl Into<String>) -> Self {
85        Self {
86            annotations: None,
87            text: text.into(),
88            meta: None,
89        }
90    }
91
92    #[must_use]
93    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
94        self.annotations = annotations.into_option();
95        self
96    }
97
98    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
99    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
100    /// these keys.
101    ///
102    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
103    #[must_use]
104    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
105        self.meta = meta.into_option();
106        self
107    }
108}
109
110impl<T: Into<String>> From<T> for ContentBlock {
111    fn from(value: T) -> Self {
112        Self::Text(TextContent::new(value))
113    }
114}
115
116/// An image provided to or from an LLM.
117#[serde_as]
118#[skip_serializing_none]
119#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
120#[serde(rename_all = "camelCase")]
121#[non_exhaustive]
122pub struct ImageContent {
123    #[serde_as(deserialize_as = "DefaultOnError")]
124    #[schemars(extend("x-deserialize-default-on-error" = true))]
125    #[serde(default)]
126    pub annotations: Option<Annotations>,
127    pub data: String,
128    pub mime_type: String,
129    pub uri: Option<String>,
130    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
131    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
132    /// these keys.
133    ///
134    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
135    #[serde(rename = "_meta")]
136    pub meta: Option<Meta>,
137}
138
139impl ImageContent {
140    #[must_use]
141    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
142        Self {
143            annotations: None,
144            data: data.into(),
145            mime_type: mime_type.into(),
146            uri: None,
147            meta: None,
148        }
149    }
150
151    #[must_use]
152    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
153        self.annotations = annotations.into_option();
154        self
155    }
156
157    #[must_use]
158    pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
159        self.uri = uri.into_option();
160        self
161    }
162
163    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
164    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
165    /// these keys.
166    ///
167    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
168    #[must_use]
169    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
170        self.meta = meta.into_option();
171        self
172    }
173}
174
175/// Audio provided to or from an LLM.
176#[serde_as]
177#[skip_serializing_none]
178#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
179#[serde(rename_all = "camelCase")]
180#[non_exhaustive]
181pub struct AudioContent {
182    #[serde_as(deserialize_as = "DefaultOnError")]
183    #[schemars(extend("x-deserialize-default-on-error" = true))]
184    #[serde(default)]
185    pub annotations: Option<Annotations>,
186    pub data: String,
187    pub mime_type: String,
188    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
189    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
190    /// these keys.
191    ///
192    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
193    #[serde(rename = "_meta")]
194    pub meta: Option<Meta>,
195}
196
197impl AudioContent {
198    #[must_use]
199    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
200        Self {
201            annotations: None,
202            data: data.into(),
203            mime_type: mime_type.into(),
204            meta: None,
205        }
206    }
207
208    #[must_use]
209    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
210        self.annotations = annotations.into_option();
211        self
212    }
213
214    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
215    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
216    /// these keys.
217    ///
218    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
219    #[must_use]
220    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
221        self.meta = meta.into_option();
222        self
223    }
224}
225
226/// The contents of a resource, embedded into a prompt or tool call result.
227#[serde_as]
228#[skip_serializing_none]
229#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
230#[non_exhaustive]
231pub struct EmbeddedResource {
232    #[serde_as(deserialize_as = "DefaultOnError")]
233    #[schemars(extend("x-deserialize-default-on-error" = true))]
234    #[serde(default)]
235    pub annotations: Option<Annotations>,
236    pub resource: EmbeddedResourceResource,
237    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
238    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
239    /// these keys.
240    ///
241    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
242    #[serde(rename = "_meta")]
243    pub meta: Option<Meta>,
244}
245
246impl EmbeddedResource {
247    #[must_use]
248    pub fn new(resource: EmbeddedResourceResource) -> Self {
249        Self {
250            annotations: None,
251            resource,
252            meta: None,
253        }
254    }
255
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    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
263    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
264    /// these keys.
265    ///
266    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
267    #[must_use]
268    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
269        self.meta = meta.into_option();
270        self
271    }
272}
273
274/// Resource content that can be embedded in a message.
275#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
276#[serde(untagged)]
277#[non_exhaustive]
278pub enum EmbeddedResourceResource {
279    TextResourceContents(TextResourceContents),
280    BlobResourceContents(BlobResourceContents),
281}
282
283/// Text-based resource contents.
284#[skip_serializing_none]
285#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
286#[serde(rename_all = "camelCase")]
287#[non_exhaustive]
288pub struct TextResourceContents {
289    pub mime_type: Option<String>,
290    pub text: String,
291    pub uri: String,
292    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
293    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
294    /// these keys.
295    ///
296    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
297    #[serde(rename = "_meta")]
298    pub meta: Option<Meta>,
299}
300
301impl TextResourceContents {
302    #[must_use]
303    pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
304        Self {
305            mime_type: None,
306            text: text.into(),
307            uri: uri.into(),
308            meta: None,
309        }
310    }
311
312    #[must_use]
313    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
314        self.mime_type = mime_type.into_option();
315        self
316    }
317
318    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
319    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
320    /// these keys.
321    ///
322    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
323    #[must_use]
324    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
325        self.meta = meta.into_option();
326        self
327    }
328}
329
330/// Binary resource contents.
331#[skip_serializing_none]
332#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
333#[serde(rename_all = "camelCase")]
334#[non_exhaustive]
335pub struct BlobResourceContents {
336    pub blob: String,
337    pub mime_type: Option<String>,
338    pub uri: String,
339    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
340    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
341    /// these keys.
342    ///
343    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
344    #[serde(rename = "_meta")]
345    pub meta: Option<Meta>,
346}
347
348impl BlobResourceContents {
349    #[must_use]
350    pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
351        Self {
352            blob: blob.into(),
353            mime_type: None,
354            uri: uri.into(),
355            meta: None,
356        }
357    }
358
359    #[must_use]
360    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
361        self.mime_type = mime_type.into_option();
362        self
363    }
364
365    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
366    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
367    /// these keys.
368    ///
369    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
370    #[must_use]
371    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
372        self.meta = meta.into_option();
373        self
374    }
375}
376
377/// A resource that the server is capable of reading, included in a prompt or tool call result.
378#[serde_as]
379#[skip_serializing_none]
380#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
381#[serde(rename_all = "camelCase")]
382#[non_exhaustive]
383pub struct ResourceLink {
384    #[serde_as(deserialize_as = "DefaultOnError")]
385    #[schemars(extend("x-deserialize-default-on-error" = true))]
386    #[serde(default)]
387    pub annotations: Option<Annotations>,
388    pub description: Option<String>,
389    pub mime_type: Option<String>,
390    pub name: String,
391    pub size: Option<i64>,
392    pub title: Option<String>,
393    pub uri: String,
394    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
395    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
396    /// these keys.
397    ///
398    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
399    #[serde(rename = "_meta")]
400    pub meta: Option<Meta>,
401}
402
403impl ResourceLink {
404    #[must_use]
405    pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
406        Self {
407            annotations: None,
408            description: None,
409            mime_type: None,
410            name: name.into(),
411            size: None,
412            title: None,
413            uri: uri.into(),
414            meta: None,
415        }
416    }
417
418    #[must_use]
419    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
420        self.annotations = annotations.into_option();
421        self
422    }
423
424    #[must_use]
425    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
426        self.description = description.into_option();
427        self
428    }
429
430    #[must_use]
431    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
432        self.mime_type = mime_type.into_option();
433        self
434    }
435
436    #[must_use]
437    pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
438        self.size = size.into_option();
439        self
440    }
441
442    #[must_use]
443    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
444        self.title = title.into_option();
445        self
446    }
447
448    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
449    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
450    /// these keys.
451    ///
452    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
453    #[must_use]
454    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
455        self.meta = meta.into_option();
456        self
457    }
458}
459
460/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
461#[serde_as]
462#[skip_serializing_none]
463#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
464#[serde(rename_all = "camelCase")]
465#[non_exhaustive]
466pub struct Annotations {
467    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
468    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
469    #[serde(default)]
470    pub audience: Option<Vec<Role>>,
471    pub last_modified: Option<String>,
472    pub priority: Option<f64>,
473    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
474    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
475    /// these keys.
476    ///
477    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
478    #[serde(rename = "_meta")]
479    pub meta: Option<Meta>,
480}
481
482impl Annotations {
483    #[must_use]
484    pub fn new() -> Self {
485        Self::default()
486    }
487
488    #[must_use]
489    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
490        self.audience = audience.into_option();
491        self
492    }
493
494    #[must_use]
495    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
496        self.last_modified = last_modified.into_option();
497        self
498    }
499
500    #[must_use]
501    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
502        self.priority = priority.into_option();
503        self
504    }
505
506    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
507    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
508    /// these keys.
509    ///
510    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
511    #[must_use]
512    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
513        self.meta = meta.into_option();
514        self
515    }
516}
517
518/// The sender or recipient of messages and data in a conversation.
519#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
520#[serde(rename_all = "camelCase")]
521#[non_exhaustive]
522pub enum Role {
523    Assistant,
524    User,
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_text_content_roundtrip() {
533        let content = TextContent::new("hello world");
534        let json = serde_json::to_value(&content).unwrap();
535        let parsed: TextContent = serde_json::from_value(json).unwrap();
536        assert_eq!(content, parsed);
537    }
538
539    #[test]
540    fn test_text_content_omits_optional_fields() {
541        let content = TextContent::new("hello");
542        let json = serde_json::to_value(&content).unwrap();
543        assert!(!json.as_object().unwrap().contains_key("annotations"));
544        assert!(!json.as_object().unwrap().contains_key("meta"));
545    }
546
547    #[test]
548    fn test_text_content_from_string() {
549        let block: ContentBlock = "hello".into();
550        match block {
551            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
552            _ => panic!("Expected Text variant"),
553        }
554    }
555
556    #[test]
557    fn test_image_content_roundtrip() {
558        let content = ImageContent::new("base64data", "image/png");
559        let json = serde_json::to_value(&content).unwrap();
560        let parsed: ImageContent = serde_json::from_value(json).unwrap();
561        assert_eq!(content, parsed);
562    }
563
564    #[test]
565    fn test_image_content_omits_optional_fields() {
566        let content = ImageContent::new("data", "image/png");
567        let json = serde_json::to_value(&content).unwrap();
568        assert!(!json.as_object().unwrap().contains_key("uri"));
569        assert!(!json.as_object().unwrap().contains_key("annotations"));
570        assert!(!json.as_object().unwrap().contains_key("meta"));
571    }
572
573    #[test]
574    fn test_image_content_with_uri() {
575        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
576        let json = serde_json::to_value(&content).unwrap();
577        assert_eq!(json["uri"], "https://example.com/image.png");
578    }
579
580    #[test]
581    fn test_audio_content_roundtrip() {
582        let content = AudioContent::new("base64audio", "audio/mp3");
583        let json = serde_json::to_value(&content).unwrap();
584        let parsed: AudioContent = serde_json::from_value(json).unwrap();
585        assert_eq!(content, parsed);
586    }
587
588    #[test]
589    fn test_audio_content_omits_optional_fields() {
590        let content = AudioContent::new("data", "audio/mp3");
591        let json = serde_json::to_value(&content).unwrap();
592        assert!(!json.as_object().unwrap().contains_key("annotations"));
593        assert!(!json.as_object().unwrap().contains_key("meta"));
594    }
595}