Skip to main content

turbomcp_types/
content.rs

1//! Content types for MCP messages (MCP 2025-11-25).
2//!
3//! This module defines the content type unions used in the MCP protocol:
4//!
5//! - [`Content`] (`ContentBlock`): Used in tool call results and prompt messages.
6//!   Variants: Text, Image, Audio, ResourceLink, Resource (EmbeddedResource).
7//!
8//! - [`SamplingContent`] (`SamplingMessageContentBlock`): Used in sampling messages.
9//!   Variants: Text, Image, Audio, ToolUse, ToolResult.
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14#[cfg(not(feature = "std"))]
15use alloc::collections::BTreeMap as HashMap;
16#[cfg(feature = "std")]
17use std::collections::HashMap;
18
19/// Role in a conversation or prompt message.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Role {
23    /// User role (human or client)
24    #[default]
25    User,
26    /// Assistant role (AI or server)
27    Assistant,
28}
29
30impl core::fmt::Display for Role {
31    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32        match self {
33            Self::User => f.write_str("user"),
34            Self::Assistant => f.write_str("assistant"),
35        }
36    }
37}
38
39// =============================================================================
40// ContentBlock — used in tool results and prompt messages
41// =============================================================================
42
43/// Content block in MCP messages (`ContentBlock` per spec).
44///
45/// Used in `CallToolResult.content` and `PromptMessage.content`.
46///
47/// Per MCP 2025-11-25, the union is:
48/// `TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource`
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50#[serde(tag = "type")]
51pub enum Content {
52    /// Text content
53    #[serde(rename = "text")]
54    Text(TextContent),
55    /// Image content (base64 encoded)
56    #[serde(rename = "image")]
57    Image(ImageContent),
58    /// Audio content (base64 encoded)
59    #[serde(rename = "audio")]
60    Audio(AudioContent),
61    /// Resource link (reference to a resource without embedding)
62    #[serde(rename = "resource_link")]
63    ResourceLink(ResourceLink),
64    /// Embedded resource content
65    #[serde(rename = "resource")]
66    Resource(EmbeddedResource),
67}
68
69impl Default for Content {
70    fn default() -> Self {
71        Self::text("")
72    }
73}
74
75impl Content {
76    /// Create text content.
77    #[must_use]
78    pub fn text(text: impl Into<String>) -> Self {
79        Self::Text(TextContent {
80            text: text.into(),
81            annotations: None,
82            meta: None,
83        })
84    }
85
86    /// Create image content from base64 data.
87    #[must_use]
88    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
89        Self::Image(ImageContent {
90            data: data.into(),
91            mime_type: mime_type.into(),
92            annotations: None,
93            meta: None,
94        })
95    }
96
97    /// Create audio content from base64 data.
98    #[must_use]
99    pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
100        Self::Audio(AudioContent {
101            data: data.into(),
102            mime_type: mime_type.into(),
103            annotations: None,
104            meta: None,
105        })
106    }
107
108    /// Create a resource link.
109    #[must_use]
110    pub fn resource_link(resource: crate::definitions::Resource) -> Self {
111        Self::ResourceLink(ResourceLink {
112            uri: resource.uri,
113            name: resource.name,
114            description: resource.description,
115            title: resource.title,
116            icons: resource.icons,
117            mime_type: resource.mime_type,
118            annotations: resource.annotations,
119            size: resource.size,
120            meta: resource.meta,
121        })
122    }
123
124    /// Create embedded resource content.
125    #[must_use]
126    pub fn resource(uri: impl Into<String>, text: impl Into<String>) -> Self {
127        Self::Resource(EmbeddedResource {
128            resource: ResourceContents::Text(TextResourceContents {
129                uri: uri.into(),
130                mime_type: Some("text/plain".into()),
131                text: text.into(),
132                meta: None,
133            }),
134            annotations: None,
135            meta: None,
136        })
137    }
138
139    /// Check if this is text content.
140    #[must_use]
141    pub fn is_text(&self) -> bool {
142        matches!(self, Self::Text(_))
143    }
144
145    /// Get the text if this is text content.
146    #[must_use]
147    pub fn as_text(&self) -> Option<&str> {
148        match self {
149            Self::Text(t) => Some(&t.text),
150            _ => None,
151        }
152    }
153
154    /// Check if this is image content.
155    #[must_use]
156    pub fn is_image(&self) -> bool {
157        matches!(self, Self::Image(_))
158    }
159
160    /// Check if this is audio content.
161    #[must_use]
162    pub fn is_audio(&self) -> bool {
163        matches!(self, Self::Audio(_))
164    }
165
166    /// Check if this is a resource link.
167    #[must_use]
168    pub fn is_resource_link(&self) -> bool {
169        matches!(self, Self::ResourceLink(_))
170    }
171
172    /// Check if this is resource content.
173    #[must_use]
174    pub fn is_resource(&self) -> bool {
175        matches!(self, Self::Resource(_))
176    }
177
178    /// Add annotations to this content.
179    #[must_use]
180    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
181        match &mut self {
182            Self::Text(t) => t.annotations = Some(annotations),
183            Self::Image(i) => i.annotations = Some(annotations),
184            Self::Audio(a) => a.annotations = Some(annotations),
185            Self::ResourceLink(r) => {
186                r.annotations = Some(crate::definitions::ResourceAnnotations {
187                    audience: annotations.audience,
188                    priority: annotations.priority,
189                    last_modified: annotations.last_modified,
190                })
191            }
192            Self::Resource(r) => r.annotations = Some(annotations),
193        }
194        self
195    }
196}
197
198// =============================================================================
199// SamplingMessageContentBlock — used in sampling messages
200// =============================================================================
201
202/// Content block for sampling messages (`SamplingMessageContentBlock` per spec).
203///
204/// Per MCP 2025-11-25, the union is:
205/// `TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent`
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
207#[serde(tag = "type")]
208pub enum SamplingContent {
209    /// Text content
210    #[serde(rename = "text")]
211    Text(TextContent),
212    /// Image content (base64 encoded)
213    #[serde(rename = "image")]
214    Image(ImageContent),
215    /// Audio content (base64 encoded)
216    #[serde(rename = "audio")]
217    Audio(AudioContent),
218    /// Tool use content (assistant requesting tool invocation)
219    #[serde(rename = "tool_use")]
220    ToolUse(ToolUseContent),
221    /// Tool result content (result of a tool invocation)
222    #[serde(rename = "tool_result")]
223    ToolResult(ToolResultContent),
224}
225
226impl Default for SamplingContent {
227    fn default() -> Self {
228        Self::text("")
229    }
230}
231
232impl SamplingContent {
233    /// Create text content.
234    #[must_use]
235    pub fn text(text: impl Into<String>) -> Self {
236        Self::Text(TextContent {
237            text: text.into(),
238            annotations: None,
239            meta: None,
240        })
241    }
242
243    /// Get the text if this is text content.
244    #[must_use]
245    pub fn as_text(&self) -> Option<&str> {
246        match self {
247            Self::Text(t) => Some(&t.text),
248            _ => None,
249        }
250    }
251}
252
253/// Wrapper that deserializes as either a single content block or an array.
254///
255/// Per MCP 2025-11-25, `SamplingMessage.content` is
256/// `SamplingMessageContentBlock | SamplingMessageContentBlock[]`.
257///
258/// `Serialize` and `Deserialize` are implemented manually below to handle
259/// the single-vs-array polymorphism (single serializes as object, array as array).
260#[derive(Debug, Clone, PartialEq)]
261pub enum SamplingContentBlock {
262    /// A single content block.
263    Single(SamplingContent),
264    /// Multiple content blocks.
265    Multiple(Vec<SamplingContent>),
266}
267
268impl Default for SamplingContentBlock {
269    fn default() -> Self {
270        Self::Single(SamplingContent::default())
271    }
272}
273
274impl SamplingContentBlock {
275    /// Get the text of the first text content block, if any.
276    #[must_use]
277    pub fn as_text(&self) -> Option<&str> {
278        match self {
279            Self::Single(c) => c.as_text(),
280            Self::Multiple(v) => v.iter().find_map(|c| c.as_text()),
281        }
282    }
283
284    /// Collect all content blocks into a `Vec` of references.
285    ///
286    /// Note: this allocates. For iteration, use `iter()` instead.
287    #[must_use]
288    pub fn to_vec(&self) -> Vec<&SamplingContent> {
289        match self {
290            Self::Single(c) => vec![c],
291            Self::Multiple(v) => v.iter().collect(),
292        }
293    }
294}
295
296impl Serialize for SamplingContentBlock {
297    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
298        match self {
299            Self::Single(c) => c.serialize(serializer),
300            Self::Multiple(v) => v.serialize(serializer),
301        }
302    }
303}
304
305impl<'de> Deserialize<'de> for SamplingContentBlock {
306    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
307        let value = Value::deserialize(deserializer)?;
308        if value.is_array() {
309            let v: Vec<SamplingContent> =
310                serde_json::from_value(value).map_err(serde::de::Error::custom)?;
311            Ok(Self::Multiple(v))
312        } else {
313            let c: SamplingContent =
314                serde_json::from_value(value).map_err(serde::de::Error::custom)?;
315            Ok(Self::Single(c))
316        }
317    }
318}
319
320impl From<SamplingContent> for SamplingContentBlock {
321    fn from(c: SamplingContent) -> Self {
322        Self::Single(c)
323    }
324}
325
326impl From<Vec<SamplingContent>> for SamplingContentBlock {
327    fn from(v: Vec<SamplingContent>) -> Self {
328        Self::Multiple(v)
329    }
330}
331
332// =============================================================================
333// Individual content types
334// =============================================================================
335
336/// Text content with optional annotations.
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
338pub struct TextContent {
339    /// The text content
340    pub text: String,
341    /// Optional annotations (audience, priority, etc.)
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub annotations: Option<Annotations>,
344    /// Extension metadata
345    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
346    pub meta: Option<HashMap<String, Value>>,
347}
348
349impl TextContent {
350    /// Create new text content.
351    #[must_use]
352    pub fn new(text: impl Into<String>) -> Self {
353        Self {
354            text: text.into(),
355            annotations: None,
356            meta: None,
357        }
358    }
359}
360
361/// Image content (base64 encoded).
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
363pub struct ImageContent {
364    /// Base64-encoded image data
365    pub data: String,
366    /// MIME type of the image
367    #[serde(rename = "mimeType")]
368    pub mime_type: String,
369    /// Optional annotations
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub annotations: Option<Annotations>,
372    /// Extension metadata
373    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
374    pub meta: Option<HashMap<String, Value>>,
375}
376
377/// Audio content (base64 encoded).
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub struct AudioContent {
380    /// Base64-encoded audio data
381    pub data: String,
382    /// MIME type of the audio
383    #[serde(rename = "mimeType")]
384    pub mime_type: String,
385    /// Optional annotations
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub annotations: Option<Annotations>,
388    /// Extension metadata
389    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
390    pub meta: Option<HashMap<String, Value>>,
391}
392
393/// Tool use content in a sampling message (assistant requesting tool invocation).
394///
395/// New in MCP 2025-11-25. Part of `SamplingMessageContentBlock`.
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
397pub struct ToolUseContent {
398    /// Unique ID for this tool use.
399    pub id: String,
400    /// Name of the tool to invoke.
401    pub name: String,
402    /// Input arguments for the tool.
403    pub input: HashMap<String, Value>,
404    /// Extension metadata
405    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
406    pub meta: Option<HashMap<String, Value>>,
407}
408
409/// Tool result content in a sampling message (result of a tool invocation).
410///
411/// New in MCP 2025-11-25. Part of `SamplingMessageContentBlock`.
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
413pub struct ToolResultContent {
414    /// ID of the tool use this result corresponds to.
415    #[serde(rename = "toolUseId")]
416    pub tool_use_id: String,
417    /// Content blocks from the tool result.
418    pub content: Vec<Content>,
419    /// Structured content conforming to the tool's output schema.
420    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
421    pub structured_content: Option<Value>,
422    /// Whether the tool execution resulted in an error.
423    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
424    pub is_error: Option<bool>,
425    /// Extension metadata
426    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
427    pub meta: Option<HashMap<String, Value>>,
428}
429
430/// A resource link (reference without embedding contents).
431///
432/// New in MCP 2025-11-25. Extends `Resource` with `type: "resource_link"`.
433/// Resource links returned by tools are not guaranteed to appear in `resources/list`.
434#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
435pub struct ResourceLink {
436    /// Resource URI
437    pub uri: String,
438    /// Resource name
439    pub name: String,
440    /// Resource description
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub description: Option<String>,
443    /// Human-readable title
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub title: Option<String>,
446    /// Resource icons
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub icons: Option<Vec<crate::definitions::Icon>>,
449    /// MIME type of the resource content
450    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
451    pub mime_type: Option<String>,
452    /// Resource annotations
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub annotations: Option<crate::definitions::ResourceAnnotations>,
455    /// Size in bytes (if known)
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub size: Option<u64>,
458    /// Extension metadata
459    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
460    pub meta: Option<HashMap<String, Value>>,
461}
462
463/// Embedded resource content in a message.
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
465pub struct EmbeddedResource {
466    /// The actual resource contents
467    pub resource: ResourceContents,
468    /// Optional annotations
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub annotations: Option<Annotations>,
471    /// Extension metadata
472    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
473    pub meta: Option<HashMap<String, Value>>,
474}
475
476// =============================================================================
477// Resource contents
478// =============================================================================
479
480/// Contents of a resource (text or binary).
481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
482#[serde(untagged)]
483pub enum ResourceContents {
484    /// Text resource content
485    Text(TextResourceContents),
486    /// Binary resource content
487    Blob(BlobResourceContents),
488}
489
490impl ResourceContents {
491    /// Get the URI of this resource content.
492    #[must_use]
493    pub fn uri(&self) -> &str {
494        match self {
495            Self::Text(t) => &t.uri,
496            Self::Blob(b) => &b.uri,
497        }
498    }
499
500    /// Get the text content, if this is a text resource.
501    #[must_use]
502    pub fn text(&self) -> Option<&str> {
503        match self {
504            Self::Text(t) => Some(&t.text),
505            Self::Blob(_) => None,
506        }
507    }
508
509    /// Get the blob (base64) content, if this is a binary resource.
510    #[must_use]
511    pub fn blob(&self) -> Option<&str> {
512        match self {
513            Self::Text(_) => None,
514            Self::Blob(b) => Some(&b.blob),
515        }
516    }
517
518    /// Get the MIME type, if set.
519    #[must_use]
520    pub fn mime_type(&self) -> Option<&str> {
521        match self {
522            Self::Text(t) => t.mime_type.as_deref(),
523            Self::Blob(b) => b.mime_type.as_deref(),
524        }
525    }
526}
527
528/// Textual resource contents.
529#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
530pub struct TextResourceContents {
531    /// Resource URI
532    pub uri: String,
533    /// MIME type
534    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
535    pub mime_type: Option<String>,
536    /// Text content
537    pub text: String,
538    /// Extension metadata
539    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
540    pub meta: Option<HashMap<String, Value>>,
541}
542
543/// Binary resource contents.
544#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
545pub struct BlobResourceContents {
546    /// Resource URI
547    pub uri: String,
548    /// MIME type
549    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
550    pub mime_type: Option<String>,
551    /// Base64-encoded binary data
552    pub blob: String,
553    /// Extension metadata
554    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
555    pub meta: Option<HashMap<String, Value>>,
556}
557
558// =============================================================================
559// Annotations
560// =============================================================================
561
562/// Annotations for content providing metadata.
563///
564/// Per MCP 2025-11-25, annotations indicate:
565/// - Who should see the content (audience)
566/// - Relative importance (priority)
567/// - When it was last modified
568#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
569pub struct Annotations {
570    /// Target audience for this content
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub audience: Option<Vec<Role>>,
573    /// Priority level (0.0 to 1.0, higher = more important)
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub priority: Option<f64>,
576    /// Last modified timestamp (ISO 8601)
577    #[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
578    pub last_modified: Option<String>,
579}
580
581impl Annotations {
582    /// Create annotations for user audience only.
583    #[must_use]
584    pub fn for_user() -> Self {
585        Self {
586            audience: Some(vec![Role::User]),
587            ..Default::default()
588        }
589    }
590
591    /// Create annotations for assistant audience only.
592    #[must_use]
593    pub fn for_assistant() -> Self {
594        Self {
595            audience: Some(vec![Role::Assistant]),
596            ..Default::default()
597        }
598    }
599
600    /// Set the priority level.
601    #[must_use]
602    pub fn with_priority(mut self, priority: f64) -> Self {
603        self.priority = Some(priority);
604        self
605    }
606
607    /// Set the last modified timestamp.
608    #[must_use]
609    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
610        self.last_modified = Some(timestamp.into());
611        self
612    }
613}
614
615// =============================================================================
616// Message (PromptMessage per spec)
617// =============================================================================
618
619/// A message in a prompt (`PromptMessage` per spec).
620///
621/// Per MCP 2025-11-25, `content` is a single `ContentBlock` (not an array).
622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
623pub struct Message {
624    /// The role of this message (user or assistant)
625    pub role: Role,
626    /// The content of this message (single ContentBlock)
627    pub content: Content,
628}
629
630impl Message {
631    /// Create a new message.
632    #[must_use]
633    pub fn new(role: Role, content: Content) -> Self {
634        Self { role, content }
635    }
636
637    /// Create a user message with text content.
638    #[must_use]
639    pub fn user(text: impl Into<String>) -> Self {
640        Self {
641            role: Role::User,
642            content: Content::text(text),
643        }
644    }
645
646    /// Create an assistant message with text content.
647    #[must_use]
648    pub fn assistant(text: impl Into<String>) -> Self {
649        Self {
650            role: Role::Assistant,
651            content: Content::text(text),
652        }
653    }
654
655    /// Check if this is a user message.
656    #[must_use]
657    pub fn is_user(&self) -> bool {
658        self.role == Role::User
659    }
660
661    /// Check if this is an assistant message.
662    #[must_use]
663    pub fn is_assistant(&self) -> bool {
664        self.role == Role::Assistant
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn test_content_text() {
674        let content = Content::text("Hello");
675        assert!(content.is_text());
676        assert_eq!(content.as_text(), Some("Hello"));
677    }
678
679    #[test]
680    fn test_content_image() {
681        let content = Content::image("base64data", "image/png");
682        assert!(content.is_image());
683        assert!(!content.is_text());
684    }
685
686    #[test]
687    fn test_content_serde() {
688        let content = Content::text("Hello");
689        let json = serde_json::to_string(&content).unwrap();
690        assert!(json.contains("\"type\":\"text\""));
691        assert!(json.contains("\"text\":\"Hello\""));
692    }
693
694    #[test]
695    fn test_resource_link_serde() {
696        let link = Content::ResourceLink(ResourceLink {
697            uri: "file:///test.txt".into(),
698            name: "test".into(),
699            description: None,
700            title: None,
701            icons: None,
702            mime_type: Some("text/plain".into()),
703            annotations: None,
704            size: None,
705            meta: None,
706        });
707        let json = serde_json::to_string(&link).unwrap();
708        assert!(json.contains("\"type\":\"resource_link\""));
709        assert!(json.contains("\"uri\":\"file:///test.txt\""));
710
711        // Round-trip
712        let parsed: Content = serde_json::from_str(&json).unwrap();
713        assert!(parsed.is_resource_link());
714    }
715
716    #[test]
717    fn test_sampling_content_tool_use_serde() {
718        let content = SamplingContent::ToolUse(ToolUseContent {
719            id: "tu_1".into(),
720            name: "search".into(),
721            input: [("query".to_string(), Value::String("test".into()))].into(),
722            meta: None,
723        });
724        let json = serde_json::to_string(&content).unwrap();
725        assert!(json.contains("\"type\":\"tool_use\""));
726        assert!(json.contains("\"id\":\"tu_1\""));
727
728        let parsed: SamplingContent = serde_json::from_str(&json).unwrap();
729        assert!(matches!(parsed, SamplingContent::ToolUse(_)));
730    }
731
732    #[test]
733    fn test_sampling_content_block_single() {
734        let block = SamplingContentBlock::Single(SamplingContent::text("hello"));
735        let json = serde_json::to_string(&block).unwrap();
736        // Single should serialize as an object, not array
737        assert!(json.starts_with('{'));
738        let parsed: SamplingContentBlock = serde_json::from_str(&json).unwrap();
739        assert!(matches!(parsed, SamplingContentBlock::Single(_)));
740    }
741
742    #[test]
743    fn test_sampling_content_block_multiple() {
744        let block = SamplingContentBlock::Multiple(vec![
745            SamplingContent::text("hello"),
746            SamplingContent::text("world"),
747        ]);
748        let json = serde_json::to_string(&block).unwrap();
749        // Multiple should serialize as an array
750        assert!(json.starts_with('['));
751        let parsed: SamplingContentBlock = serde_json::from_str(&json).unwrap();
752        assert!(matches!(parsed, SamplingContentBlock::Multiple(v) if v.len() == 2));
753    }
754
755    #[test]
756    fn test_message_user() {
757        let msg = Message::user("Hello");
758        assert!(msg.is_user());
759        assert!(!msg.is_assistant());
760    }
761
762    #[test]
763    fn test_message_assistant() {
764        let msg = Message::assistant("Hi there");
765        assert!(msg.is_assistant());
766        assert!(!msg.is_user());
767    }
768
769    #[test]
770    fn test_annotations_for_user() {
771        let ann = Annotations::for_user().with_priority(1.0);
772        assert_eq!(ann.audience, Some(vec![Role::User]));
773        assert_eq!(ann.priority, Some(1.0));
774    }
775
776    #[test]
777    fn test_content_with_annotations() {
778        let content = Content::text("Hello").with_annotations(Annotations::for_user());
779        if let Content::Text(t) = content {
780            assert!(t.annotations.is_some());
781        } else {
782            panic!("Expected text content");
783        }
784    }
785
786    // C-1: ResourceContents untagged deserialization disambiguation
787    #[test]
788    fn test_resource_contents_text_deser() {
789        let json = r#"{"uri":"file:///test.txt","mimeType":"text/plain","text":"hello"}"#;
790        let rc: ResourceContents = serde_json::from_str(json).unwrap();
791        assert!(matches!(rc, ResourceContents::Text(_)));
792        assert_eq!(rc.uri(), "file:///test.txt");
793        assert_eq!(rc.text(), Some("hello"));
794        assert!(rc.blob().is_none());
795    }
796
797    #[test]
798    fn test_resource_contents_blob_deser() {
799        let json = r#"{"uri":"file:///img.png","mimeType":"image/png","blob":"aGVsbG8="}"#;
800        let rc: ResourceContents = serde_json::from_str(json).unwrap();
801        assert!(matches!(rc, ResourceContents::Blob(_)));
802        assert_eq!(rc.uri(), "file:///img.png");
803        assert_eq!(rc.blob(), Some("aGVsbG8="));
804        assert!(rc.text().is_none());
805    }
806
807    #[test]
808    fn test_resource_contents_round_trip() {
809        let text = ResourceContents::Text(TextResourceContents {
810            uri: "file:///a.txt".into(),
811            mime_type: Some("text/plain".into()),
812            text: "content".into(),
813            meta: None,
814        });
815        let json = serde_json::to_string(&text).unwrap();
816        let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
817        assert_eq!(text, parsed);
818
819        let blob = ResourceContents::Blob(BlobResourceContents {
820            uri: "file:///b.bin".into(),
821            mime_type: Some("application/octet-stream".into()),
822            blob: "AQID".into(),
823            meta: None,
824        });
825        let json = serde_json::to_string(&blob).unwrap();
826        let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
827        assert_eq!(blob, parsed);
828    }
829
830    // C-2: ToolResultContent serde round-trip
831    #[test]
832    fn test_sampling_content_tool_result_serde() {
833        let content = SamplingContent::ToolResult(ToolResultContent {
834            tool_use_id: "tu_1".into(),
835            content: vec![Content::text("result data")],
836            structured_content: Some(serde_json::json!({"key": "value"})),
837            is_error: Some(false),
838            meta: None,
839        });
840        let json = serde_json::to_string(&content).unwrap();
841        assert!(json.contains("\"type\":\"tool_result\""));
842        assert!(json.contains("\"toolUseId\":\"tu_1\""));
843        assert!(json.contains("\"structuredContent\""));
844
845        let parsed: SamplingContent = serde_json::from_str(&json).unwrap();
846        assert!(matches!(parsed, SamplingContent::ToolResult(_)));
847    }
848
849    // H-1: SamplingContentBlock empty array
850    #[test]
851    fn test_sampling_content_block_empty_array() {
852        let parsed: SamplingContentBlock = serde_json::from_str("[]").unwrap();
853        assert!(matches!(parsed, SamplingContentBlock::Multiple(v) if v.is_empty()));
854    }
855
856    // H-2: Single-element array vs single object
857    #[test]
858    fn test_sampling_content_block_single_element_array() {
859        let single_obj = r#"{"type":"text","text":"x"}"#;
860        let single_arr = r#"[{"type":"text","text":"x"}]"#;
861
862        let parsed_obj: SamplingContentBlock = serde_json::from_str(single_obj).unwrap();
863        assert!(matches!(parsed_obj, SamplingContentBlock::Single(_)));
864
865        let parsed_arr: SamplingContentBlock = serde_json::from_str(single_arr).unwrap();
866        assert!(matches!(parsed_arr, SamplingContentBlock::Multiple(v) if v.len() == 1));
867    }
868
869    // H-3: All Content type discriminants round-trip
870    #[test]
871    fn test_content_all_type_discriminants() {
872        let variants: Vec<(&str, Content)> = vec![
873            ("text", Content::text("hi")),
874            ("image", Content::image("data", "image/png")),
875            ("audio", Content::audio("data", "audio/wav")),
876            (
877                "resource_link",
878                Content::ResourceLink(ResourceLink {
879                    uri: "file:///x".into(),
880                    name: "x".into(),
881                    description: None,
882                    title: None,
883                    icons: None,
884                    mime_type: None,
885                    annotations: None,
886                    size: None,
887                    meta: None,
888                }),
889            ),
890            ("resource", Content::resource("file:///x", "text")),
891        ];
892
893        for (expected_type, content) in variants {
894            let json = serde_json::to_string(&content).unwrap();
895            assert!(
896                json.contains(&format!("\"type\":\"{}\"", expected_type)),
897                "Missing type discriminant for {expected_type}: {json}"
898            );
899            let parsed: Content = serde_json::from_str(&json).unwrap();
900            assert_eq!(content, parsed, "Round-trip failed for {expected_type}");
901        }
902    }
903
904    // H-4: _meta field serialization presence/absence
905    #[test]
906    fn test_meta_field_skip_serializing_if_none() {
907        let content = TextContent::new("hello");
908        let json = serde_json::to_string(&content).unwrap();
909        assert!(!json.contains("_meta"), "None meta should be omitted");
910
911        let mut meta = HashMap::new();
912        meta.insert("key".into(), Value::String("val".into()));
913        let content = TextContent {
914            text: "hello".into(),
915            annotations: None,
916            meta: Some(meta),
917        };
918        let json = serde_json::to_string(&content).unwrap();
919        assert!(json.contains("\"_meta\""), "Some meta should be present");
920    }
921
922    // H-4: _meta on ResourceContents
923    #[test]
924    fn test_resource_contents_meta_field() {
925        let mut meta = HashMap::new();
926        meta.insert("k".into(), Value::Bool(true));
927        let rc = ResourceContents::Text(TextResourceContents {
928            uri: "x".into(),
929            mime_type: None,
930            text: "y".into(),
931            meta: Some(meta),
932        });
933        let json = serde_json::to_string(&rc).unwrap();
934        assert!(json.contains("\"_meta\""));
935        let parsed: ResourceContents = serde_json::from_str(&json).unwrap();
936        assert_eq!(rc, parsed);
937    }
938
939    // H-6: SamplingContentBlock to_vec
940    #[test]
941    fn test_sampling_content_block_to_vec() {
942        let single = SamplingContentBlock::Single(SamplingContent::text("a"));
943        assert_eq!(single.to_vec().len(), 1);
944
945        let multi = SamplingContentBlock::Multiple(vec![
946            SamplingContent::text("a"),
947            SamplingContent::text("b"),
948        ]);
949        assert_eq!(multi.to_vec().len(), 2);
950
951        // as_text on Multiple finds first text
952        assert_eq!(multi.as_text(), Some("a"));
953    }
954}