//! Content types that flow between agents, tools, prompts, and LLMs. //! //! The core union is [`ContentBlock`] (text | image | audio | resource_link | resource), //! matching the MCP 2025-11-25 `ContentBlock` definition. Each variant carries optional //! [`Annotations`] and `_meta` inline. //! //! [`SamplingMessageContentBlock`] extends the union with `tool_use` and `tool_result` //! variants for sampling messages (SEP-1577). // ToolUseContent/ToolResultContent are SEP-2577-deprecated; internal references are expected. #![expect(deprecated)] use serde::{Deserialize, Serialize}; use serde_json::json; use super::{Annotations, Meta, resource::ResourceContents}; // --------------------------------------------------------------------------- // Flat content structs // --------------------------------------------------------------------------- /// Text content block (spec `TextContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct TextContent { /// The text content of the message. pub text: String, /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } impl TextContent { pub fn new(text: impl Into) -> Self { Self { text: text.into(), meta: None, annotations: None, } } pub fn with_meta(mut self, meta: Meta) -> Self { self.meta = Some(meta); self } pub fn with_annotations(mut self, annotations: Annotations) -> Self { self.annotations = Some(annotations); self } } /// Image content with base64-encoded data (spec `ImageContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ImageContent { /// The base64-encoded image data. pub data: String, /// The MIME type of the image (e.g. `image/png`). pub mime_type: String, /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } impl ImageContent { pub fn new(data: impl Into, mime_type: impl Into) -> Self { Self { data: data.into(), mime_type: mime_type.into(), meta: None, annotations: None, } } pub fn with_meta(mut self, meta: Meta) -> Self { self.meta = Some(meta); self } pub fn with_annotations(mut self, annotations: Annotations) -> Self { self.annotations = Some(annotations); self } } /// Audio content with base64-encoded data (spec `AudioContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct AudioContent { /// The base64-encoded audio data. pub data: String, /// The MIME type of the audio (e.g. `audio/wav`). pub mime_type: String, /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } impl AudioContent { pub fn new(data: impl Into, mime_type: impl Into) -> Self { Self { data: data.into(), mime_type: mime_type.into(), meta: None, annotations: None, } } pub fn with_meta(mut self, meta: Meta) -> Self { self.meta = Some(meta); self } pub fn with_annotations(mut self, annotations: Annotations) -> Self { self.annotations = Some(annotations); self } } /// Embedded resource content (spec `EmbeddedResource`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct EmbeddedResource { /// The embedded resource contents (text or blob). pub resource: ResourceContents, /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } impl EmbeddedResource { pub fn new(resource: ResourceContents) -> Self { Self { resource, meta: None, annotations: None, } } pub fn get_text(&self) -> String { match &self.resource { ResourceContents::TextResourceContents { text, .. } => text.clone(), _ => String::new(), } } pub fn with_meta(mut self, meta: Meta) -> Self { self.meta = Some(meta); self } pub fn with_annotations(mut self, annotations: Annotations) -> Self { self.annotations = Some(annotations); self } } /// Tool call request from assistant (SEP-1577). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] #[deprecated( since = "2.0.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub struct ToolUseContent { pub id: String, pub name: String, pub input: super::JsonObject, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, } /// Tool execution result in user message (SEP-1577). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] #[deprecated( since = "2.0.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub struct ToolResultContent { #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, pub tool_use_id: String, pub content: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub structured_content: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, } impl ToolUseContent { pub fn new(id: impl Into, name: impl Into, input: super::JsonObject) -> Self { Self { id: id.into(), name: name.into(), input, meta: None, } } } impl ToolResultContent { pub fn new(tool_use_id: impl Into, content: Vec) -> Self { Self { meta: None, tool_use_id: tool_use_id.into(), content, structured_content: None, is_error: None, } } pub fn error(tool_use_id: impl Into, content: Vec) -> Self { Self { meta: None, tool_use_id: tool_use_id.into(), content, structured_content: None, is_error: Some(true), } } } // --------------------------------------------------------------------------- // ContentBlock — the unified content union (spec `ContentBlock`) // --------------------------------------------------------------------------- /// Unified content block union (spec `ContentBlock`). /// /// `text | image | audio | resource_link | resource` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub enum ContentBlock { Text(TextContent), Image(ImageContent), Audio(AudioContent), Resource(EmbeddedResource), ResourceLink(super::resource::Resource), } impl ContentBlock { pub fn json(json: S) -> Result { let json = serde_json::to_string(&json).map_err(|e| { crate::ErrorData::internal_error( "fail to serialize response to json", Some(json!( {"reason": e.to_string()} )), ) })?; Ok(ContentBlock::text(json)) } pub fn text(text: impl Into) -> Self { ContentBlock::Text(TextContent::new(text)) } pub fn image(data: impl Into, mime_type: impl Into) -> Self { ContentBlock::Image(ImageContent::new(data, mime_type)) } pub fn audio(data: impl Into, mime_type: impl Into) -> Self { ContentBlock::Audio(AudioContent::new(data, mime_type)) } pub fn resource(resource: ResourceContents) -> Self { ContentBlock::Resource(EmbeddedResource::new(resource)) } pub fn embedded_text(uri: impl Into, content: impl Into) -> Self { ContentBlock::Resource(EmbeddedResource::new( ResourceContents::TextResourceContents { uri: uri.into(), mime_type: Some("text".to_string()), text: content.into(), meta: None, }, )) } pub fn resource_link(resource: super::resource::Resource) -> Self { ContentBlock::ResourceLink(resource) } pub fn as_text(&self) -> Option<&TextContent> { match self { ContentBlock::Text(text) => Some(text), _ => None, } } pub fn as_image(&self) -> Option<&ImageContent> { match self { ContentBlock::Image(image) => Some(image), _ => None, } } pub fn as_resource(&self) -> Option<&EmbeddedResource> { match self { ContentBlock::Resource(resource) => Some(resource), _ => None, } } pub fn as_resource_link(&self) -> Option<&super::resource::Resource> { match self { ContentBlock::ResourceLink(link) => Some(link), _ => None, } } pub fn as_audio(&self) -> Option<&AudioContent> { match self { ContentBlock::Audio(audio) => Some(audio), _ => None, } } } // --------------------------------------------------------------------------- // JsonContent (unchanged) // --------------------------------------------------------------------------- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct JsonContent(S); // --------------------------------------------------------------------------- // IntoContents // --------------------------------------------------------------------------- /// Types that can be converted into a list of content blocks. pub trait IntoContents { fn into_contents(self) -> Vec; } impl IntoContents for ContentBlock { fn into_contents(self) -> Vec { vec![self] } } impl IntoContents for String { fn into_contents(self) -> Vec { vec![ContentBlock::text(self)] } } impl IntoContents for () { fn into_contents(self) -> Vec { vec![] } } #[cfg(test)] mod tests { use serde_json; use super::*; #[test] fn test_image_content_serialization() { let image = ImageContent::new("base64data", "image/png"); let json = serde_json::to_string(&image).unwrap(); assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } #[test] fn test_audio_content_serialization() { let audio = AudioContent::new("base64audiodata", "audio/wav"); let json = serde_json::to_string(&audio).unwrap(); assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } #[test] fn test_audio_content_has_meta() { let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default()); let json = serde_json::to_value(&audio).unwrap(); assert!(json.get("_meta").is_some()); } #[test] fn test_resource_link_serialization() { use super::super::resource::Resource; let resource_link = ContentBlock::ResourceLink(Resource { uri: "file:///test.txt".to_string(), name: "test.txt".to_string(), title: None, description: Some("A test file".to_string()), mime_type: Some("text/plain".to_string()), size: Some(100), icons: None, meta: None, annotations: None, }); let json = serde_json::to_string(&resource_link).unwrap(); assert!(json.contains("\"type\":\"resource_link\"")); assert!(json.contains("\"uri\":\"file:///test.txt\"")); assert!(json.contains("\"name\":\"test.txt\"")); } #[test] fn test_resource_link_deserialization() { let json = r#"{ "type": "resource_link", "uri": "file:///example.txt", "name": "example.txt", "description": "Example file", "mimeType": "text/plain" }"#; let content: ContentBlock = serde_json::from_str(json).unwrap(); if let ContentBlock::ResourceLink(resource) = content { assert_eq!(resource.uri, "file:///example.txt"); assert_eq!(resource.name, "example.txt"); assert_eq!(resource.description, Some("Example file".to_string())); assert_eq!(resource.mime_type, Some("text/plain".to_string())); } else { panic!("Expected ResourceLink variant"); } } #[test] fn test_content_block_text_with_annotations() { let block = ContentBlock::Text( TextContent::new("hello").with_annotations(Annotations::default().with_priority(0.8)), ); let json = serde_json::to_value(&block).unwrap(); assert_eq!(json["type"], "text"); assert_eq!(json["text"], "hello"); assert_eq!(json["annotations"]["priority"], 0.8_f32); } }