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