Skip to main content

turbomcp_types/
results.rs

1//! Result types for MCP operations.
2//!
3//! This module provides ergonomic result types for the three core MCP operations:
4//! - `ToolResult` - Results from tool invocations
5//! - `ResourceResult` - Results from resource reads
6//! - `PromptResult` - Results from prompt retrieval
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[cfg(not(feature = "std"))]
12use alloc::{collections::BTreeMap as HashMap, string::String, vec, vec::Vec};
13#[cfg(feature = "std")]
14use std::collections::HashMap;
15
16use crate::content::{
17    BlobResourceContents, Content, Message, ResourceContents, Role, TextResourceContents,
18};
19
20/// Result from calling a tool.
21#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
22pub struct ToolResult {
23    /// Content blocks in the result
24    pub content: Vec<Content>,
25    /// Whether this result represents an error
26    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
27    pub is_error: Option<bool>,
28    /// Structured content conforming to the tool's output schema
29    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
30    pub structured_content: Option<Value>,
31    /// Extension metadata
32    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
33    pub meta: Option<HashMap<String, Value>>,
34}
35
36impl ToolResult {
37    /// Create a text result.
38    #[must_use]
39    pub fn text(text: impl Into<String>) -> Self {
40        Self {
41            content: vec![Content::text(text)],
42            ..Default::default()
43        }
44    }
45
46    /// Create an error result.
47    #[must_use]
48    pub fn error(text: impl Into<String>) -> Self {
49        Self {
50            content: vec![Content::text(text)],
51            is_error: Some(true),
52            ..Default::default()
53        }
54    }
55
56    /// Create a JSON result with structured content.
57    pub fn json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
58        let structured = serde_json::to_value(value)?;
59        let text = serde_json::to_string_pretty(value)?;
60        Ok(Self {
61            content: vec![Content::text(text)],
62            structured_content: Some(structured),
63            ..Default::default()
64        })
65    }
66
67    /// Create an empty result (no content).
68    #[must_use]
69    pub fn empty() -> Self {
70        Self::default()
71    }
72
73    /// Check if this result represents an error.
74    #[must_use]
75    pub fn is_error(&self) -> bool {
76        self.is_error.unwrap_or(false)
77    }
78
79    /// Add structured content to the result.
80    #[must_use]
81    pub fn with_structured<T: Serialize>(mut self, value: &T) -> Self {
82        self.structured_content = serde_json::to_value(value).ok();
83        self
84    }
85
86    /// Add additional content to the result.
87    #[must_use]
88    pub fn with_content(mut self, content: Content) -> Self {
89        self.content.push(content);
90        self
91    }
92
93    /// Add image content to the result.
94    #[must_use]
95    pub fn with_image(self, data: impl Into<String>, mime_type: impl Into<String>) -> Self {
96        self.with_content(Content::image(data, mime_type))
97    }
98
99    /// Set metadata.
100    #[must_use]
101    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
102        self.meta = Some(meta);
103        self
104    }
105
106    /// Get the first text content if present.
107    #[must_use]
108    pub fn first_text(&self) -> Option<&str> {
109        self.content.first().and_then(|c| c.as_text())
110    }
111}
112
113/// Result from reading a resource.
114#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
115pub struct ResourceResult {
116    /// Resource contents
117    pub contents: Vec<ResourceContents>,
118    /// Extension metadata
119    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
120    pub meta: Option<HashMap<String, Value>>,
121}
122
123impl ResourceResult {
124    /// Create a text resource result.
125    #[must_use]
126    pub fn text(uri: impl Into<String>, content: impl Into<String>) -> Self {
127        Self {
128            contents: vec![ResourceContents::Text(TextResourceContents {
129                uri: uri.into(),
130                mime_type: Some("text/plain".into()),
131                text: content.into(),
132                meta: None,
133            })],
134            ..Default::default()
135        }
136    }
137
138    /// Create a JSON resource result.
139    pub fn json<T: Serialize>(
140        uri: impl Into<String>,
141        value: &T,
142    ) -> Result<Self, serde_json::Error> {
143        Ok(Self {
144            contents: vec![ResourceContents::Text(TextResourceContents {
145                uri: uri.into(),
146                mime_type: Some("application/json".into()),
147                text: serde_json::to_string_pretty(value)?,
148                meta: None,
149            })],
150            ..Default::default()
151        })
152    }
153
154    /// Create a binary resource result.
155    #[must_use]
156    pub fn binary(
157        uri: impl Into<String>,
158        data: impl Into<String>,
159        mime_type: impl Into<String>,
160    ) -> Self {
161        Self {
162            contents: vec![ResourceContents::Blob(BlobResourceContents {
163                uri: uri.into(),
164                mime_type: Some(mime_type.into()),
165                blob: data.into(),
166                meta: None,
167            })],
168            ..Default::default()
169        }
170    }
171
172    /// Create an empty resource result.
173    #[must_use]
174    pub fn empty() -> Self {
175        Self::default()
176    }
177
178    /// Add additional content to the result.
179    #[must_use]
180    pub fn with_content(mut self, content: ResourceContents) -> Self {
181        self.contents.push(content);
182        self
183    }
184
185    /// Set metadata.
186    #[must_use]
187    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
188        self.meta = Some(meta);
189        self
190    }
191
192    /// Get the first content's text if present.
193    #[must_use]
194    pub fn first_text(&self) -> Option<&str> {
195        self.contents.first().and_then(|c| match c {
196            ResourceContents::Text(t) => Some(t.text.as_str()),
197            ResourceContents::Blob(_) => None,
198        })
199    }
200}
201
202/// Result from getting a prompt.
203#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
204pub struct PromptResult {
205    /// Description of this prompt result
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub description: Option<String>,
208    /// Messages in the prompt
209    pub messages: Vec<Message>,
210    /// Extension metadata
211    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
212    pub meta: Option<HashMap<String, Value>>,
213}
214
215impl PromptResult {
216    /// Create a prompt result with messages.
217    #[must_use]
218    pub fn new(messages: Vec<Message>) -> Self {
219        Self {
220            messages,
221            ..Default::default()
222        }
223    }
224
225    /// Create a prompt with a single user message.
226    #[must_use]
227    pub fn user(text: impl Into<String>) -> Self {
228        Self::new(vec![Message::user(text)])
229    }
230
231    /// Create a prompt with a single assistant message.
232    #[must_use]
233    pub fn assistant(text: impl Into<String>) -> Self {
234        Self::new(vec![Message::assistant(text)])
235    }
236
237    /// Create an empty prompt.
238    #[must_use]
239    pub fn empty() -> Self {
240        Self::default()
241    }
242
243    /// Add a description to the prompt.
244    #[must_use]
245    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
246        self.description = Some(desc.into());
247        self
248    }
249
250    /// Add a user message to the prompt.
251    #[must_use]
252    pub fn add_user(mut self, text: impl Into<String>) -> Self {
253        self.messages.push(Message::user(text));
254        self
255    }
256
257    /// Add an assistant message to the prompt.
258    #[must_use]
259    pub fn add_assistant(mut self, text: impl Into<String>) -> Self {
260        self.messages.push(Message::assistant(text));
261        self
262    }
263
264    /// Add a message with a specific role.
265    #[must_use]
266    pub fn add_message(mut self, role: Role, text: impl Into<String>) -> Self {
267        self.messages.push(Message::new(role, Content::text(text)));
268        self
269    }
270
271    /// Set metadata.
272    #[must_use]
273    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
274        self.meta = Some(meta);
275        self
276    }
277
278    /// Check if the prompt is empty.
279    #[must_use]
280    pub fn is_empty(&self) -> bool {
281        self.messages.is_empty()
282    }
283
284    /// Get the number of messages.
285    #[must_use]
286    pub fn len(&self) -> usize {
287        self.messages.len()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_tool_result_text() {
297        let result = ToolResult::text("Hello");
298        assert!(!result.is_error());
299        assert_eq!(result.first_text(), Some("Hello"));
300    }
301
302    #[test]
303    fn test_tool_result_error() {
304        let result = ToolResult::error("Failed");
305        assert!(result.is_error());
306        assert_eq!(result.first_text(), Some("Failed"));
307    }
308
309    #[test]
310    fn test_tool_result_json() {
311        let data = serde_json::json!({"key": "value"});
312        let result = ToolResult::json(&data).unwrap();
313        assert!(result.structured_content.is_some());
314        assert!(!result.is_error());
315    }
316
317    #[test]
318    fn test_resource_result_text() {
319        let result = ResourceResult::text("file:///test.txt", "content");
320        assert_eq!(result.first_text(), Some("content"));
321        match &result.contents[0] {
322            ResourceContents::Text(t) => assert_eq!(t.uri, "file:///test.txt"),
323            _ => panic!("Expected text resource contents"),
324        }
325    }
326
327    #[test]
328    fn test_resource_result_binary() {
329        let result = ResourceResult::binary("file:///img.png", "base64data", "image/png");
330        match &result.contents[0] {
331            ResourceContents::Blob(b) => {
332                assert_eq!(b.blob, "base64data");
333                assert_eq!(b.mime_type, Some("image/png".into()));
334            }
335            _ => panic!("Expected blob resource contents"),
336        }
337    }
338
339    #[test]
340    fn test_prompt_result_builder() {
341        let result = PromptResult::user("Hello")
342            .add_assistant("Hi there")
343            .add_user("How are you?")
344            .with_description("Greeting");
345
346        assert_eq!(result.len(), 3);
347        assert_eq!(result.description, Some("Greeting".into()));
348        assert!(result.messages[0].is_user());
349        assert!(result.messages[1].is_assistant());
350        assert!(result.messages[2].is_user());
351    }
352
353    #[test]
354    fn test_prompt_result_serde() {
355        let result = PromptResult::user("Test").with_description("A test prompt");
356        let json = serde_json::to_string(&result).unwrap();
357        let parsed: PromptResult = serde_json::from_str(&json).unwrap();
358        assert_eq!(result, parsed);
359    }
360}