1#![expect(deprecated)]
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14
15use super::{Annotations, MetaObject, resource::ResourceContents};
16
17#[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 pub text: String,
29 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
31 pub meta: Option<MetaObject>,
32 #[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#[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 pub data: String,
65 pub mime_type: String,
67 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
69 pub meta: Option<MetaObject>,
70 #[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#[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 pub data: String,
104 pub mime_type: String,
106 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
108 pub meta: Option<MetaObject>,
109 #[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#[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 pub resource: ResourceContents,
143 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
145 pub meta: Option<MetaObject>,
146 #[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352pub struct JsonContent<S: Serialize>(S);
353
354pub 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}