Skip to main content

nika_core/mcp/
content.rs

1//! ContentBlock and ResourceContent — MCP content protocol types.
2//!
3//! These types represent content exchanged via MCP tool calls.
4//! Extracted from nika-mcp to break the nika-media → nika-mcp dependency.
5
6use serde::{Deserialize, Serialize};
7
8/// Content block from MCP tool call result.
9///
10/// Tagged by `type` field for JSON serialization. Each variant represents
11/// a different content type that an MCP server can return.
12///
13/// Uses `#[serde(tag = "type")]` rather than a newtype wrapper because
14/// the tag must appear as a field in the JSON map for deserialization.
15#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum ContentBlock {
18    /// Plain text content
19    Text { text: String },
20
21    /// Base64-encoded image with MIME type
22    Image {
23        data: String,
24        #[serde(rename = "mimeType")]
25        mime_type: String,
26    },
27
28    /// Base64-encoded audio with MIME type
29    Audio {
30        data: String,
31        #[serde(rename = "mimeType")]
32        mime_type: String,
33    },
34
35    /// Embedded resource content (text or blob)
36    Resource(ResourceContent),
37
38    /// Resource link (URI reference without inline data)
39    ResourceLink {
40        uri: String,
41        #[serde(skip_serializing_if = "Option::is_none")]
42        name: Option<String>,
43        #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
44        mime_type: Option<String>,
45    },
46}
47
48impl ContentBlock {
49    /// Create a text content block.
50    pub fn text(text: impl Into<String>) -> Self {
51        Self::Text { text: text.into() }
52    }
53
54    /// Create an image content block.
55    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
56        Self::Image {
57            data: data.into(),
58            mime_type: mime_type.into(),
59        }
60    }
61
62    /// Create an audio content block.
63    pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
64        Self::Audio {
65            data: data.into(),
66            mime_type: mime_type.into(),
67        }
68    }
69
70    /// Create a resource content block.
71    pub fn resource(resource: ResourceContent) -> Self {
72        Self::Resource(resource)
73    }
74
75    /// Create a resource link content block.
76    pub fn resource_link(
77        uri: impl Into<String>,
78        name: Option<String>,
79        mime_type: Option<String>,
80    ) -> Self {
81        Self::ResourceLink {
82            uri: uri.into(),
83            name,
84            mime_type,
85        }
86    }
87
88    /// Check if this is a text block.
89    pub fn is_text(&self) -> bool {
90        matches!(self, Self::Text { .. })
91    }
92
93    /// Check if this is an image block.
94    pub fn is_image(&self) -> bool {
95        matches!(self, Self::Image { .. })
96    }
97
98    /// Check if this is an audio block.
99    pub fn is_audio(&self) -> bool {
100        matches!(self, Self::Audio { .. })
101    }
102
103    /// Check if this is a resource block.
104    pub fn is_resource(&self) -> bool {
105        matches!(self, Self::Resource(_))
106    }
107
108    /// Check if this is a resource link block.
109    pub fn is_resource_link(&self) -> bool {
110        matches!(self, Self::ResourceLink { .. })
111    }
112}
113
114/// Resource content from MCP server.
115///
116/// Represents a resource that can be read from the MCP server.
117#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
118pub struct ResourceContent {
119    /// Resource URI (e.g., "file:///path/to/file", "neo4j://entity/qr-code")
120    pub uri: String,
121
122    /// MIME type of the resource content
123    #[serde(default, rename = "mimeType", skip_serializing_if = "Option::is_none")]
124    pub mime_type: Option<String>,
125
126    /// Resource text content (if loaded)
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub text: Option<String>,
129
130    /// Resource binary content as base64 (if loaded)
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub blob: Option<String>,
133}
134
135impl ResourceContent {
136    /// Create a new resource content with URI.
137    pub fn new(uri: impl Into<String>) -> Self {
138        Self {
139            uri: uri.into(),
140            mime_type: None,
141            text: None,
142            blob: None,
143        }
144    }
145
146    /// Set the MIME type.
147    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
148        self.mime_type = Some(mime_type.into());
149        self
150    }
151
152    /// Set the text content.
153    pub fn with_text(mut self, text: impl Into<String>) -> Self {
154        self.text = Some(text.into());
155        self
156    }
157
158    /// Set the blob content (base64-encoded binary).
159    pub fn with_blob(mut self, blob: impl Into<String>) -> Self {
160        self.blob = Some(blob.into());
161        self
162    }
163
164    /// Set the MIME type if value is Some.
165    pub fn with_optional_mime(mut self, mime_type: Option<String>) -> Self {
166        if mime_type.is_some() {
167            self.mime_type = mime_type;
168        }
169        self
170    }
171}