Skip to main content

rmcp/model/
content.rs

1//! Content types that flow between agents, tools, prompts, and LLMs.
2//!
3//! The core union is [`ContentBlock`] (text | image | audio | resource_link | resource),
4//! matching the MCP 2025-11-25 `ContentBlock` definition. Each variant carries optional
5//! [`Annotations`] and `_meta` inline.
6//!
7//! [`SamplingMessageContentBlock`] extends the union with `tool_use` and `tool_result`
8//! variants for sampling messages (SEP-1577).
9
10// ToolUseContent/ToolResultContent are SEP-2577-deprecated; internal references are expected.
11#![expect(deprecated)]
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14
15use super::{Annotations, MetaObject, resource::ResourceContents};
16
17// ---------------------------------------------------------------------------
18// Flat content structs
19// ---------------------------------------------------------------------------
20
21/// Text content block (spec `TextContent`).
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[non_exhaustive]
26pub struct TextContent {
27    /// The text content of the message.
28    pub text: String,
29    /// Optional protocol-level metadata for this content block.
30    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
31    pub meta: Option<MetaObject>,
32    /// Optional annotations describing how the client should use this content.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub annotations: Option<Annotations>,
35}
36
37impl TextContent {
38    pub fn new(text: impl Into<String>) -> Self {
39        Self {
40            text: text.into(),
41            meta: None,
42            annotations: None,
43        }
44    }
45
46    pub fn with_meta(mut self, meta: MetaObject) -> Self {
47        self.meta = Some(meta);
48        self
49    }
50
51    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
52        self.annotations = Some(annotations);
53        self
54    }
55}
56
57/// Image content with base64-encoded data (spec `ImageContent`).
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
61#[non_exhaustive]
62pub struct ImageContent {
63    /// The base64-encoded image data.
64    pub data: String,
65    /// The MIME type of the image (e.g. `image/png`).
66    pub mime_type: String,
67    /// Optional protocol-level metadata for this content block.
68    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
69    pub meta: Option<MetaObject>,
70    /// Optional annotations describing how the client should use this content.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub annotations: Option<Annotations>,
73}
74
75impl ImageContent {
76    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
77        Self {
78            data: data.into(),
79            mime_type: mime_type.into(),
80            meta: None,
81            annotations: None,
82        }
83    }
84
85    pub fn with_meta(mut self, meta: MetaObject) -> Self {
86        self.meta = Some(meta);
87        self
88    }
89
90    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
91        self.annotations = Some(annotations);
92        self
93    }
94}
95
96/// Audio content with base64-encoded data (spec `AudioContent`).
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100#[non_exhaustive]
101pub struct AudioContent {
102    /// The base64-encoded audio data.
103    pub data: String,
104    /// The MIME type of the audio (e.g. `audio/wav`).
105    pub mime_type: String,
106    /// Optional protocol-level metadata for this content block.
107    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
108    pub meta: Option<MetaObject>,
109    /// Optional annotations describing how the client should use this content.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub annotations: Option<Annotations>,
112}
113
114impl AudioContent {
115    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
116        Self {
117            data: data.into(),
118            mime_type: mime_type.into(),
119            meta: None,
120            annotations: None,
121        }
122    }
123
124    pub fn with_meta(mut self, meta: MetaObject) -> Self {
125        self.meta = Some(meta);
126        self
127    }
128
129    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
130        self.annotations = Some(annotations);
131        self
132    }
133}
134
135/// Embedded resource content (spec `EmbeddedResource`).
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
139#[non_exhaustive]
140pub struct EmbeddedResource {
141    /// The embedded resource contents (text or blob).
142    pub resource: ResourceContents,
143    /// Optional protocol-level metadata for this content block.
144    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
145    pub meta: Option<MetaObject>,
146    /// Optional annotations describing how the client should use this content.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub annotations: Option<Annotations>,
149}
150
151impl EmbeddedResource {
152    pub fn new(resource: ResourceContents) -> Self {
153        Self {
154            resource,
155            meta: None,
156            annotations: None,
157        }
158    }
159
160    pub fn get_text(&self) -> String {
161        match &self.resource {
162            ResourceContents::TextResourceContents { text, .. } => text.clone(),
163            _ => String::new(),
164        }
165    }
166
167    pub fn with_meta(mut self, meta: MetaObject) -> Self {
168        self.meta = Some(meta);
169        self
170    }
171
172    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
173        self.annotations = Some(annotations);
174        self
175    }
176}
177
178/// Tool call request from assistant (SEP-1577).
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
182#[non_exhaustive]
183#[deprecated(
184    since = "2.0.0",
185    note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
186)]
187pub struct ToolUseContent {
188    pub id: String,
189    pub name: String,
190    pub input: super::JsonObject,
191    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
192    pub meta: Option<MetaObject>,
193}
194
195/// Tool execution result in user message (SEP-1577).
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
199#[non_exhaustive]
200#[deprecated(
201    since = "2.0.0",
202    note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
203)]
204pub struct ToolResultContent {
205    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
206    pub meta: Option<MetaObject>,
207    pub tool_use_id: String,
208    pub content: Vec<ContentBlock>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub structured_content: Option<Value>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub is_error: Option<bool>,
213}
214
215impl ToolUseContent {
216    pub fn new(id: impl Into<String>, name: impl Into<String>, input: super::JsonObject) -> Self {
217        Self {
218            id: id.into(),
219            name: name.into(),
220            input,
221            meta: None,
222        }
223    }
224}
225
226impl ToolResultContent {
227    pub fn new(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
228        Self {
229            meta: None,
230            tool_use_id: tool_use_id.into(),
231            content,
232            structured_content: None,
233            is_error: None,
234        }
235    }
236
237    pub fn error(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
238        Self {
239            meta: None,
240            tool_use_id: tool_use_id.into(),
241            content,
242            structured_content: None,
243            is_error: Some(true),
244        }
245    }
246}
247
248// ---------------------------------------------------------------------------
249// ContentBlock — the unified content union (spec `ContentBlock`)
250// ---------------------------------------------------------------------------
251
252/// Unified content block union (spec `ContentBlock`).
253///
254/// `text | image | audio | resource_link | resource`
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256#[serde(tag = "type", rename_all = "snake_case")]
257#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
258#[non_exhaustive]
259pub enum ContentBlock {
260    Text(TextContent),
261    Image(ImageContent),
262    Audio(AudioContent),
263    Resource(EmbeddedResource),
264    ResourceLink(super::resource::Resource),
265}
266
267impl ContentBlock {
268    pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
269        let json = serde_json::to_string(&json).map_err(|e| {
270            crate::ErrorData::internal_error(
271                "fail to serialize response to json",
272                Some(json!(
273                    {"reason": e.to_string()}
274                )),
275            )
276        })?;
277        Ok(ContentBlock::text(json))
278    }
279
280    pub fn text(text: impl Into<String>) -> Self {
281        ContentBlock::Text(TextContent::new(text))
282    }
283
284    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
285        ContentBlock::Image(ImageContent::new(data, mime_type))
286    }
287
288    pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
289        ContentBlock::Audio(AudioContent::new(data, mime_type))
290    }
291
292    pub fn resource(resource: ResourceContents) -> Self {
293        ContentBlock::Resource(EmbeddedResource::new(resource))
294    }
295
296    pub fn embedded_text(uri: impl Into<String>, content: impl Into<String>) -> Self {
297        ContentBlock::Resource(EmbeddedResource::new(
298            ResourceContents::TextResourceContents {
299                uri: uri.into(),
300                mime_type: Some("text/plain".to_string()),
301                text: content.into(),
302                meta: None,
303            },
304        ))
305    }
306
307    pub fn resource_link(resource: super::resource::Resource) -> Self {
308        ContentBlock::ResourceLink(resource)
309    }
310
311    pub fn as_text(&self) -> Option<&TextContent> {
312        match self {
313            ContentBlock::Text(text) => Some(text),
314            _ => None,
315        }
316    }
317
318    pub fn as_image(&self) -> Option<&ImageContent> {
319        match self {
320            ContentBlock::Image(image) => Some(image),
321            _ => None,
322        }
323    }
324
325    pub fn as_resource(&self) -> Option<&EmbeddedResource> {
326        match self {
327            ContentBlock::Resource(resource) => Some(resource),
328            _ => None,
329        }
330    }
331
332    pub fn as_resource_link(&self) -> Option<&super::resource::Resource> {
333        match self {
334            ContentBlock::ResourceLink(link) => Some(link),
335            _ => None,
336        }
337    }
338
339    pub fn as_audio(&self) -> Option<&AudioContent> {
340        match self {
341            ContentBlock::Audio(audio) => Some(audio),
342            _ => None,
343        }
344    }
345}
346
347// ---------------------------------------------------------------------------
348// JsonContent (unchanged)
349// ---------------------------------------------------------------------------
350
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352pub struct JsonContent<S: Serialize>(S);
353
354// ---------------------------------------------------------------------------
355// IntoContents
356// ---------------------------------------------------------------------------
357
358/// Types that can be converted into a list of content blocks.
359pub trait IntoContents {
360    fn into_contents(self) -> Vec<ContentBlock>;
361}
362
363impl IntoContents for ContentBlock {
364    fn into_contents(self) -> Vec<ContentBlock> {
365        vec![self]
366    }
367}
368
369impl IntoContents for String {
370    fn into_contents(self) -> Vec<ContentBlock> {
371        vec![ContentBlock::text(self)]
372    }
373}
374
375impl IntoContents for () {
376    fn into_contents(self) -> Vec<ContentBlock> {
377        vec![]
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use serde_json;
384
385    use super::*;
386
387    #[test]
388    fn test_image_content_serialization() {
389        let image = ImageContent::new("base64data", "image/png");
390        let json = serde_json::to_string(&image).unwrap();
391        assert!(json.contains("mimeType"));
392        assert!(!json.contains("mime_type"));
393    }
394
395    #[test]
396    fn test_audio_content_serialization() {
397        let audio = AudioContent::new("base64audiodata", "audio/wav");
398        let json = serde_json::to_string(&audio).unwrap();
399        assert!(json.contains("mimeType"));
400        assert!(!json.contains("mime_type"));
401    }
402
403    #[test]
404    fn test_audio_content_has_meta() {
405        let audio = AudioContent::new("data", "audio/wav").with_meta(MetaObject::default());
406        let json = serde_json::to_value(&audio).unwrap();
407        assert!(json.get("_meta").is_some());
408    }
409
410    #[test]
411    fn test_resource_link_serialization() {
412        use super::super::resource::Resource;
413
414        let resource_link = ContentBlock::ResourceLink(Resource {
415            uri: "file:///test.txt".to_string(),
416            name: "test.txt".to_string(),
417            title: None,
418            description: Some("A test file".to_string()),
419            mime_type: Some("text/plain".to_string()),
420            size: Some(100),
421            icons: None,
422            meta: None,
423            annotations: None,
424        });
425
426        let json = serde_json::to_string(&resource_link).unwrap();
427        assert!(json.contains("\"type\":\"resource_link\""));
428        assert!(json.contains("\"uri\":\"file:///test.txt\""));
429        assert!(json.contains("\"name\":\"test.txt\""));
430    }
431
432    #[test]
433    fn test_resource_link_deserialization() {
434        let json = r#"{
435            "type": "resource_link",
436            "uri": "file:///example.txt",
437            "name": "example.txt",
438            "description": "Example file",
439            "mimeType": "text/plain"
440        }"#;
441
442        let content: ContentBlock = serde_json::from_str(json).unwrap();
443
444        if let ContentBlock::ResourceLink(resource) = content {
445            assert_eq!(resource.uri, "file:///example.txt");
446            assert_eq!(resource.name, "example.txt");
447            assert_eq!(resource.description, Some("Example file".to_string()));
448            assert_eq!(resource.mime_type, Some("text/plain".to_string()));
449        } else {
450            panic!("Expected ResourceLink variant");
451        }
452    }
453
454    #[test]
455    fn test_content_block_text_with_annotations() {
456        let block = ContentBlock::Text(
457            TextContent::new("hello").with_annotations(Annotations::default().with_priority(0.8)),
458        );
459        let json = serde_json::to_value(&block).unwrap();
460        assert_eq!(json["type"], "text");
461        assert_eq!(json["text"], "hello");
462        assert_eq!(json["annotations"]["priority"], 0.8_f32);
463    }
464}