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 /// Optional annotations that help clients decide how to display or route this content.
69 #[serde_as(deserialize_as = "DefaultOnError")]
70 #[schemars(extend("x-deserialize-default-on-error" = true))]
71 #[serde(default)]
72 pub annotations: Option<Annotations>,
73 /// Text payload carried by this content block.
74 pub text: String,
75 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
76 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
77 /// these keys.
78 ///
79 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
80 #[serde(rename = "_meta")]
81 pub meta: Option<Meta>,
82}
83
84impl TextContent {
85 /// Builds [`TextContent`] with its required content payload; optional annotations and metadata start unset.
86 #[must_use]
87 pub fn new(text: impl Into<String>) -> Self {
88 Self {
89 annotations: None,
90 text: text.into(),
91 meta: None,
92 }
93 }
94
95 /// Sets or clears the optional `annotations` field.
96 #[must_use]
97 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
98 self.annotations = annotations.into_option();
99 self
100 }
101
102 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
103 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
104 /// these keys.
105 ///
106 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
107 #[must_use]
108 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
109 self.meta = meta.into_option();
110 self
111 }
112}
113
114impl<T: Into<String>> From<T> for ContentBlock {
115 fn from(value: T) -> Self {
116 Self::Text(TextContent::new(value))
117 }
118}
119
120/// An image provided to or from an LLM.
121#[serde_as]
122#[skip_serializing_none]
123#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
124#[serde(rename_all = "camelCase")]
125#[non_exhaustive]
126pub struct ImageContent {
127 /// Optional annotations that help clients decide how to display or route this content.
128 #[serde_as(deserialize_as = "DefaultOnError")]
129 #[schemars(extend("x-deserialize-default-on-error" = true))]
130 #[serde(default)]
131 pub annotations: Option<Annotations>,
132 /// Base64-encoded media payload.
133 pub data: String,
134 /// MIME type describing the encoded media payload.
135 pub mime_type: String,
136 /// URI associated with this resource or media payload.
137 pub uri: Option<String>,
138 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
139 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
140 /// these keys.
141 ///
142 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
143 #[serde(rename = "_meta")]
144 pub meta: Option<Meta>,
145}
146
147impl ImageContent {
148 /// Builds [`ImageContent`] with its required content payload; optional annotations and metadata start unset.
149 #[must_use]
150 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
151 Self {
152 annotations: None,
153 data: data.into(),
154 mime_type: mime_type.into(),
155 uri: None,
156 meta: None,
157 }
158 }
159
160 /// Sets or clears the optional `annotations` field.
161 #[must_use]
162 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
163 self.annotations = annotations.into_option();
164 self
165 }
166
167 /// Sets or clears the optional `uri` field.
168 #[must_use]
169 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
170 self.uri = uri.into_option();
171 self
172 }
173
174 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
175 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
176 /// these keys.
177 ///
178 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
179 #[must_use]
180 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
181 self.meta = meta.into_option();
182 self
183 }
184}
185
186/// Audio provided to or from an LLM.
187#[serde_as]
188#[skip_serializing_none]
189#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct AudioContent {
193 /// Optional annotations that help clients decide how to display or route this content.
194 #[serde_as(deserialize_as = "DefaultOnError")]
195 #[schemars(extend("x-deserialize-default-on-error" = true))]
196 #[serde(default)]
197 pub annotations: Option<Annotations>,
198 /// Base64-encoded media payload.
199 pub data: String,
200 /// MIME type describing the encoded media payload.
201 pub mime_type: String,
202 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
203 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
204 /// these keys.
205 ///
206 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
207 #[serde(rename = "_meta")]
208 pub meta: Option<Meta>,
209}
210
211impl AudioContent {
212 /// Builds [`AudioContent`] with its required content payload; optional annotations and metadata start unset.
213 #[must_use]
214 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
215 Self {
216 annotations: None,
217 data: data.into(),
218 mime_type: mime_type.into(),
219 meta: None,
220 }
221 }
222
223 /// Sets or clears the optional `annotations` field.
224 #[must_use]
225 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
226 self.annotations = annotations.into_option();
227 self
228 }
229
230 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
231 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
232 /// these keys.
233 ///
234 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
235 #[must_use]
236 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
237 self.meta = meta.into_option();
238 self
239 }
240}
241
242/// The contents of a resource, embedded into a prompt or tool call result.
243#[serde_as]
244#[skip_serializing_none]
245#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
246#[non_exhaustive]
247pub struct EmbeddedResource {
248 /// Optional annotations that help clients decide how to display or route this content.
249 #[serde_as(deserialize_as = "DefaultOnError")]
250 #[schemars(extend("x-deserialize-default-on-error" = true))]
251 #[serde(default)]
252 pub annotations: Option<Annotations>,
253 /// Embedded resource payload, either text or binary data.
254 pub resource: EmbeddedResourceResource,
255 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
256 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
257 /// these keys.
258 ///
259 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
260 #[serde(rename = "_meta")]
261 pub meta: Option<Meta>,
262}
263
264impl EmbeddedResource {
265 /// Builds [`EmbeddedResource`] with its required content payload; optional annotations and metadata start unset.
266 #[must_use]
267 pub fn new(resource: EmbeddedResourceResource) -> Self {
268 Self {
269 annotations: None,
270 resource,
271 meta: None,
272 }
273 }
274
275 /// Sets or clears the optional `annotations` field.
276 #[must_use]
277 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
278 self.annotations = annotations.into_option();
279 self
280 }
281
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 #[must_use]
288 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
289 self.meta = meta.into_option();
290 self
291 }
292}
293
294/// Resource content that can be embedded in a message.
295#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
296#[serde(untagged)]
297#[non_exhaustive]
298pub enum EmbeddedResourceResource {
299 /// Text resource contents embedded directly in the message.
300 TextResourceContents(TextResourceContents),
301 /// Binary resource contents embedded directly in the message.
302 BlobResourceContents(BlobResourceContents),
303}
304
305/// Text-based resource contents.
306#[skip_serializing_none]
307#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
308#[serde(rename_all = "camelCase")]
309#[non_exhaustive]
310pub struct TextResourceContents {
311 /// MIME type describing the encoded media payload.
312 pub mime_type: Option<String>,
313 /// Text payload carried by this content block.
314 pub text: String,
315 /// URI associated with this resource or media payload.
316 pub uri: String,
317 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
318 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
319 /// these keys.
320 ///
321 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
322 #[serde(rename = "_meta")]
323 pub meta: Option<Meta>,
324}
325
326impl TextResourceContents {
327 /// Builds [`TextResourceContents`] with its required content payload; optional annotations and metadata start unset.
328 #[must_use]
329 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
330 Self {
331 mime_type: None,
332 text: text.into(),
333 uri: uri.into(),
334 meta: None,
335 }
336 }
337
338 /// Sets or clears the optional `mimeType` field.
339 #[must_use]
340 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
341 self.mime_type = mime_type.into_option();
342 self
343 }
344
345 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
346 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
347 /// these keys.
348 ///
349 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
350 #[must_use]
351 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
352 self.meta = meta.into_option();
353 self
354 }
355}
356
357/// Binary resource contents.
358#[skip_serializing_none]
359#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
360#[serde(rename_all = "camelCase")]
361#[non_exhaustive]
362pub struct BlobResourceContents {
363 /// Base64-encoded bytes for a binary resource payload.
364 pub blob: String,
365 /// MIME type describing the encoded media payload.
366 pub mime_type: Option<String>,
367 /// URI associated with this resource or media payload.
368 pub uri: String,
369 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
370 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
371 /// these keys.
372 ///
373 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
374 #[serde(rename = "_meta")]
375 pub meta: Option<Meta>,
376}
377
378impl BlobResourceContents {
379 /// Builds [`BlobResourceContents`] with its required content payload; optional annotations and metadata start unset.
380 #[must_use]
381 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
382 Self {
383 blob: blob.into(),
384 mime_type: None,
385 uri: uri.into(),
386 meta: None,
387 }
388 }
389
390 /// Sets or clears the optional `mimeType` field.
391 #[must_use]
392 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
393 self.mime_type = mime_type.into_option();
394 self
395 }
396
397 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
398 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
399 /// these keys.
400 ///
401 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
402 #[must_use]
403 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
404 self.meta = meta.into_option();
405 self
406 }
407}
408
409/// A resource that the server is capable of reading, included in a prompt or tool call result.
410#[serde_as]
411#[skip_serializing_none]
412#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
413#[serde(rename_all = "camelCase")]
414#[non_exhaustive]
415pub struct ResourceLink {
416 /// Optional annotations that help clients decide how to display or route this content.
417 #[serde_as(deserialize_as = "DefaultOnError")]
418 #[schemars(extend("x-deserialize-default-on-error" = true))]
419 #[serde(default)]
420 pub annotations: Option<Annotations>,
421 /// Optional human-readable details shown with this protocol object.
422 pub description: Option<String>,
423 /// MIME type describing the encoded media payload.
424 pub mime_type: Option<String>,
425 /// Human-readable name shown for this protocol object.
426 pub name: String,
427 /// Optional size of the linked resource in bytes, if known.
428 pub size: Option<i64>,
429 /// Optional display title for end-user UI.
430 pub title: Option<String>,
431 /// URI associated with this resource or media payload.
432 pub uri: String,
433 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
434 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
435 /// these keys.
436 ///
437 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
438 #[serde(rename = "_meta")]
439 pub meta: Option<Meta>,
440}
441
442impl ResourceLink {
443 /// Builds [`ResourceLink`] with its required content payload; optional annotations and metadata start unset.
444 #[must_use]
445 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
446 Self {
447 annotations: None,
448 description: None,
449 mime_type: None,
450 name: name.into(),
451 size: None,
452 title: None,
453 uri: uri.into(),
454 meta: None,
455 }
456 }
457
458 /// Sets or clears the optional `annotations` field.
459 #[must_use]
460 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
461 self.annotations = annotations.into_option();
462 self
463 }
464
465 /// Sets or clears the optional `description` field.
466 #[must_use]
467 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
468 self.description = description.into_option();
469 self
470 }
471
472 /// Sets or clears the optional `mimeType` field.
473 #[must_use]
474 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
475 self.mime_type = mime_type.into_option();
476 self
477 }
478
479 /// Sets or clears the optional `size` field.
480 #[must_use]
481 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
482 self.size = size.into_option();
483 self
484 }
485
486 /// Sets or clears the optional `title` field.
487 #[must_use]
488 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
489 self.title = title.into_option();
490 self
491 }
492
493 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
494 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
495 /// these keys.
496 ///
497 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
498 #[must_use]
499 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
500 self.meta = meta.into_option();
501 self
502 }
503}
504
505/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
506#[serde_as]
507#[skip_serializing_none]
508#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
509#[serde(rename_all = "camelCase")]
510#[non_exhaustive]
511pub struct Annotations {
512 /// Intended recipients for this content, such as the user or assistant.
513 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
514 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
515 #[serde(default)]
516 pub audience: Option<Vec<Role>>,
517 /// Timestamp indicating when the underlying resource was last modified.
518 pub last_modified: Option<String>,
519 /// Relative importance of this content when clients choose what to surface.
520 pub priority: Option<f64>,
521 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
522 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
523 /// these keys.
524 ///
525 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
526 #[serde(rename = "_meta")]
527 pub meta: Option<Meta>,
528}
529
530impl Annotations {
531 /// Creates annotations with no audience, priority, or timestamp hints set.
532 #[must_use]
533 pub fn new() -> Self {
534 Self::default()
535 }
536
537 /// Sets or clears the optional `audience` field.
538 #[must_use]
539 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
540 self.audience = audience.into_option();
541 self
542 }
543
544 /// Sets or clears the optional `lastModified` field.
545 #[must_use]
546 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
547 self.last_modified = last_modified.into_option();
548 self
549 }
550
551 /// Sets or clears the optional `priority` field.
552 #[must_use]
553 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
554 self.priority = priority.into_option();
555 self
556 }
557
558 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
559 /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
560 /// these keys.
561 ///
562 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
563 #[must_use]
564 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
565 self.meta = meta.into_option();
566 self
567 }
568}
569
570/// The sender or recipient of messages and data in a conversation.
571#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
572#[serde(rename_all = "camelCase")]
573#[non_exhaustive]
574pub enum Role {
575 /// The assistant side of a conversation.
576 Assistant,
577 /// The user side of a conversation.
578 User,
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn test_text_content_roundtrip() {
587 let content = TextContent::new("hello world");
588 let json = serde_json::to_value(&content).unwrap();
589 let parsed: TextContent = serde_json::from_value(json).unwrap();
590 assert_eq!(content, parsed);
591 }
592
593 #[test]
594 fn test_text_content_omits_optional_fields() {
595 let content = TextContent::new("hello");
596 let json = serde_json::to_value(&content).unwrap();
597 assert!(!json.as_object().unwrap().contains_key("annotations"));
598 assert!(!json.as_object().unwrap().contains_key("meta"));
599 }
600
601 #[test]
602 fn test_text_content_from_string() {
603 let block: ContentBlock = "hello".into();
604 match block {
605 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
606 _ => panic!("Expected Text variant"),
607 }
608 }
609
610 #[test]
611 fn test_image_content_roundtrip() {
612 let content = ImageContent::new("base64data", "image/png");
613 let json = serde_json::to_value(&content).unwrap();
614 let parsed: ImageContent = serde_json::from_value(json).unwrap();
615 assert_eq!(content, parsed);
616 }
617
618 #[test]
619 fn test_image_content_omits_optional_fields() {
620 let content = ImageContent::new("data", "image/png");
621 let json = serde_json::to_value(&content).unwrap();
622 assert!(!json.as_object().unwrap().contains_key("uri"));
623 assert!(!json.as_object().unwrap().contains_key("annotations"));
624 assert!(!json.as_object().unwrap().contains_key("meta"));
625 }
626
627 #[test]
628 fn test_image_content_with_uri() {
629 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
630 let json = serde_json::to_value(&content).unwrap();
631 assert_eq!(json["uri"], "https://example.com/image.png");
632 }
633
634 #[test]
635 fn test_audio_content_roundtrip() {
636 let content = AudioContent::new("base64audio", "audio/mp3");
637 let json = serde_json::to_value(&content).unwrap();
638 let parsed: AudioContent = serde_json::from_value(json).unwrap();
639 assert_eq!(content, parsed);
640 }
641
642 #[test]
643 fn test_audio_content_omits_optional_fields() {
644 let content = AudioContent::new("data", "audio/mp3");
645 let json = serde_json::to_value(&content).unwrap();
646 assert!(!json.as_object().unwrap().contains_key("annotations"));
647 assert!(!json.as_object().unwrap().contains_key("meta"));
648 }
649}