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