Skip to main content

turbomcp_types/
traits.rs

1//! Conversion traits for ergonomic result handling.
2//!
3//! These traits allow tool, resource, and prompt handlers to return
4//! various types that are automatically converted to the appropriate result type.
5//!
6//! # Example
7//!
8//! ```
9//! use turbomcp_types::{IntoToolResult, ToolResult};
10//!
11//! // All of these work as tool return types:
12//! fn returns_string() -> impl IntoToolResult { "Hello".to_string() }
13//! fn returns_i64() -> impl IntoToolResult { 42i64 }
14//! fn returns_result() -> impl IntoToolResult { Ok::<_, String>("Success") }
15//! fn returns_tool_result() -> impl IntoToolResult { ToolResult::text("Direct") }
16//! ```
17
18use core::fmt::Display;
19
20use serde::Serialize;
21
22#[cfg(not(feature = "std"))]
23use alloc::{
24    format,
25    string::{String, ToString},
26    vec,
27    vec::Vec,
28};
29
30use crate::content::Message;
31use crate::results::{PromptResult, ResourceResult, ToolResult};
32
33/// Convert any type to a `ToolResult`.
34///
35/// This trait is implemented for common types, allowing handlers to return
36/// simple types that are automatically wrapped in `ToolResult`.
37///
38/// # Implementations
39///
40/// - `String`, `&str` → text result
41/// - Numeric types → text result with string representation
42/// - `bool` → text result ("true" or "false")
43/// - `()` → empty result
44/// - `ToolResult` → pass through
45/// - `Result<T, E>` → success result or error result
46/// - `Option<T>` → result or empty
47/// - `Vec<T>` → JSON result
48///
49/// # Example
50///
51/// ```
52/// use turbomcp_types::{IntoToolResult, ToolResult};
53///
54/// // String becomes text result
55/// let result: ToolResult = "Hello".to_string().into_tool_result();
56/// assert_eq!(result.first_text(), Some("Hello"));
57///
58/// // Numbers become text
59/// let result: ToolResult = 42i64.into_tool_result();
60/// assert_eq!(result.first_text(), Some("42"));
61///
62/// // Results are handled properly
63/// let ok: Result<&str, &str> = Ok("success");
64/// let result = ok.into_tool_result();
65/// assert!(!result.is_error());
66///
67/// let err: Result<&str, &str> = Err("failed");
68/// let result = err.into_tool_result();
69/// assert!(result.is_error());
70/// ```
71pub trait IntoToolResult {
72    /// Convert this value into a `ToolResult`.
73    fn into_tool_result(self) -> ToolResult;
74}
75
76// String types
77impl IntoToolResult for String {
78    fn into_tool_result(self) -> ToolResult {
79        ToolResult::text(self)
80    }
81}
82
83impl IntoToolResult for &str {
84    fn into_tool_result(self) -> ToolResult {
85        ToolResult::text(self)
86    }
87}
88
89impl IntoToolResult for &String {
90    fn into_tool_result(self) -> ToolResult {
91        ToolResult::text(self.clone())
92    }
93}
94
95// Numeric types
96impl IntoToolResult for i8 {
97    fn into_tool_result(self) -> ToolResult {
98        ToolResult::text(self.to_string())
99    }
100}
101
102impl IntoToolResult for i16 {
103    fn into_tool_result(self) -> ToolResult {
104        ToolResult::text(self.to_string())
105    }
106}
107
108impl IntoToolResult for i32 {
109    fn into_tool_result(self) -> ToolResult {
110        ToolResult::text(self.to_string())
111    }
112}
113
114impl IntoToolResult for i64 {
115    fn into_tool_result(self) -> ToolResult {
116        ToolResult::text(self.to_string())
117    }
118}
119
120impl IntoToolResult for i128 {
121    fn into_tool_result(self) -> ToolResult {
122        ToolResult::text(self.to_string())
123    }
124}
125
126impl IntoToolResult for isize {
127    fn into_tool_result(self) -> ToolResult {
128        ToolResult::text(self.to_string())
129    }
130}
131
132impl IntoToolResult for u8 {
133    fn into_tool_result(self) -> ToolResult {
134        ToolResult::text(self.to_string())
135    }
136}
137
138impl IntoToolResult for u16 {
139    fn into_tool_result(self) -> ToolResult {
140        ToolResult::text(self.to_string())
141    }
142}
143
144impl IntoToolResult for u32 {
145    fn into_tool_result(self) -> ToolResult {
146        ToolResult::text(self.to_string())
147    }
148}
149
150impl IntoToolResult for u64 {
151    fn into_tool_result(self) -> ToolResult {
152        ToolResult::text(self.to_string())
153    }
154}
155
156impl IntoToolResult for u128 {
157    fn into_tool_result(self) -> ToolResult {
158        ToolResult::text(self.to_string())
159    }
160}
161
162impl IntoToolResult for usize {
163    fn into_tool_result(self) -> ToolResult {
164        ToolResult::text(self.to_string())
165    }
166}
167
168impl IntoToolResult for f32 {
169    fn into_tool_result(self) -> ToolResult {
170        ToolResult::text(self.to_string())
171    }
172}
173
174impl IntoToolResult for f64 {
175    fn into_tool_result(self) -> ToolResult {
176        ToolResult::text(self.to_string())
177    }
178}
179
180// Boolean
181impl IntoToolResult for bool {
182    fn into_tool_result(self) -> ToolResult {
183        ToolResult::text(self.to_string())
184    }
185}
186
187// Unit type (empty result)
188impl IntoToolResult for () {
189    fn into_tool_result(self) -> ToolResult {
190        ToolResult::empty()
191    }
192}
193
194// Pass through
195impl IntoToolResult for ToolResult {
196    fn into_tool_result(self) -> ToolResult {
197        self
198    }
199}
200
201// Result handling
202impl<T: IntoToolResult, E: Display> IntoToolResult for Result<T, E> {
203    fn into_tool_result(self) -> ToolResult {
204        match self {
205            Ok(v) => v.into_tool_result(),
206            Err(e) => ToolResult::error(e.to_string()),
207        }
208    }
209}
210
211// Option handling
212impl<T: IntoToolResult> IntoToolResult for Option<T> {
213    fn into_tool_result(self) -> ToolResult {
214        match self {
215            Some(v) => v.into_tool_result(),
216            None => ToolResult::empty(),
217        }
218    }
219}
220
221// Vec as JSON (for serializable types)
222impl<T: Serialize> IntoToolResult for Vec<T> {
223    fn into_tool_result(self) -> ToolResult {
224        ToolResult::json(&self).unwrap_or_else(|e| ToolResult::error(e.to_string()))
225    }
226}
227
228// JSON Value
229impl IntoToolResult for serde_json::Value {
230    fn into_tool_result(self) -> ToolResult {
231        ToolResult::json(&self).unwrap_or_else(|e| ToolResult::error(e.to_string()))
232    }
233}
234
235/// Convert any type to a `ResourceResult`.
236///
237/// This trait allows resource handlers to return simple types that are
238/// automatically wrapped in `ResourceResult`.
239///
240/// # Example
241///
242/// ```
243/// use turbomcp_types::{IntoResourceResult, ResourceResult};
244///
245/// // String becomes text resource
246/// let result: ResourceResult = "Content".to_string().into_resource_result("file:///test");
247/// assert_eq!(result.first_text(), Some("Content"));
248/// ```
249pub trait IntoResourceResult {
250    /// Convert this value into a `ResourceResult`.
251    ///
252    /// The `uri` parameter is used to set the resource URI.
253    fn into_resource_result(self, uri: &str) -> ResourceResult;
254}
255
256impl IntoResourceResult for String {
257    fn into_resource_result(self, uri: &str) -> ResourceResult {
258        ResourceResult::text(uri, self)
259    }
260}
261
262impl IntoResourceResult for &str {
263    fn into_resource_result(self, uri: &str) -> ResourceResult {
264        ResourceResult::text(uri, self)
265    }
266}
267
268impl IntoResourceResult for ResourceResult {
269    fn into_resource_result(self, _uri: &str) -> ResourceResult {
270        self
271    }
272}
273
274impl<T: IntoResourceResult, E: Display> IntoResourceResult for Result<T, E> {
275    fn into_resource_result(self, uri: &str) -> ResourceResult {
276        match self {
277            Ok(v) => v.into_resource_result(uri),
278            Err(e) => ResourceResult::text(uri, format!("Error: {e}")),
279        }
280    }
281}
282
283impl<T: IntoResourceResult> IntoResourceResult for Option<T> {
284    fn into_resource_result(self, uri: &str) -> ResourceResult {
285        match self {
286            Some(v) => v.into_resource_result(uri),
287            None => ResourceResult::empty(),
288        }
289    }
290}
291
292/// Convert any type to a `PromptResult`.
293///
294/// This trait allows prompt handlers to return various message types
295/// that are automatically wrapped in `PromptResult`.
296///
297/// # Example
298///
299/// ```
300/// use turbomcp_types::{IntoPromptResult, PromptResult, Message};
301///
302/// // Vec of messages becomes prompt
303/// let messages = vec![Message::user("Hello"), Message::assistant("Hi!")];
304/// let result: PromptResult = messages.into_prompt_result();
305/// assert_eq!(result.len(), 2);
306/// ```
307pub trait IntoPromptResult {
308    /// Convert this value into a `PromptResult`.
309    fn into_prompt_result(self) -> PromptResult;
310}
311
312impl IntoPromptResult for Vec<Message> {
313    fn into_prompt_result(self) -> PromptResult {
314        PromptResult::new(self)
315    }
316}
317
318impl IntoPromptResult for PromptResult {
319    fn into_prompt_result(self) -> PromptResult {
320        self
321    }
322}
323
324impl IntoPromptResult for Message {
325    fn into_prompt_result(self) -> PromptResult {
326        PromptResult::new(vec![self])
327    }
328}
329
330impl IntoPromptResult for String {
331    fn into_prompt_result(self) -> PromptResult {
332        PromptResult::user(self)
333    }
334}
335
336impl IntoPromptResult for &str {
337    fn into_prompt_result(self) -> PromptResult {
338        PromptResult::user(self)
339    }
340}
341
342/// Last-resort rendering for prompt handlers whose error type is **not**
343/// `McpError`.
344///
345/// The error becomes a user message reading `Error: …`, which a model will read
346/// as prompt content — a failed render then looks like a successful one. There
347/// is nothing better to do here, because a bare `Display` value carries no
348/// classification to propagate.
349///
350/// Return [`McpResult`](https://docs.rs/turbomcp-core) from a `#[prompt]`
351/// instead: the `#[server]` macro detects that signature and propagates the
352/// error as a JSON-RPC error rather than routing it through this impl.
353impl<T: IntoPromptResult, E: Display> IntoPromptResult for Result<T, E> {
354    fn into_prompt_result(self) -> PromptResult {
355        match self {
356            Ok(v) => v.into_prompt_result(),
357            Err(e) => PromptResult::user(format!("Error: {e}")),
358        }
359    }
360}
361
362impl<T: IntoPromptResult> IntoPromptResult for Option<T> {
363    fn into_prompt_result(self) -> PromptResult {
364        match self {
365            Some(v) => v.into_prompt_result(),
366            None => PromptResult::empty(),
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_string_into_tool_result() {
377        let result = "Hello".to_string().into_tool_result();
378        assert_eq!(result.first_text(), Some("Hello"));
379        assert!(!result.is_error());
380    }
381
382    #[test]
383    fn test_str_into_tool_result() {
384        let result = "Hello".into_tool_result();
385        assert_eq!(result.first_text(), Some("Hello"));
386    }
387
388    #[test]
389    fn test_i64_into_tool_result() {
390        let result = 42i64.into_tool_result();
391        assert_eq!(result.first_text(), Some("42"));
392    }
393
394    #[test]
395    fn test_bool_into_tool_result() {
396        let result = true.into_tool_result();
397        assert_eq!(result.first_text(), Some("true"));
398    }
399
400    #[test]
401    fn test_unit_into_tool_result() {
402        let result = ().into_tool_result();
403        assert!(result.content.is_empty());
404    }
405
406    #[test]
407    fn test_result_ok_into_tool_result() {
408        let r: Result<&str, &str> = Ok("success");
409        let result = r.into_tool_result();
410        assert_eq!(result.first_text(), Some("success"));
411        assert!(!result.is_error());
412    }
413
414    #[test]
415    fn test_result_err_into_tool_result() {
416        let r: Result<&str, &str> = Err("failed");
417        let result = r.into_tool_result();
418        assert_eq!(result.first_text(), Some("failed"));
419        assert!(result.is_error());
420    }
421
422    #[test]
423    fn test_option_some_into_tool_result() {
424        let r: Option<&str> = Some("value");
425        let result = r.into_tool_result();
426        assert_eq!(result.first_text(), Some("value"));
427    }
428
429    #[test]
430    fn test_option_none_into_tool_result() {
431        let r: Option<&str> = None;
432        let result = r.into_tool_result();
433        assert!(result.content.is_empty());
434    }
435
436    #[test]
437    fn test_vec_into_tool_result() {
438        // A Vec serializes to a JSON array, and `structuredContent` is typed
439        // `{ [key: string]: unknown }` in every schema version this SDK
440        // speaks — so the value travels as text only. Emitting the array there
441        // made the whole result invalid for validating clients.
442        let v = vec!["a", "b", "c"];
443        let result = v.into_tool_result();
444        assert_eq!(result.structured_content, None);
445        assert_eq!(
446            result.first_text(),
447            Some("[\n  \"a\",\n  \"b\",\n  \"c\"\n]")
448        );
449    }
450
451    #[test]
452    fn test_object_into_tool_result_keeps_structured_content() {
453        let result = serde_json::json!({ "a": 1 }).into_tool_result();
454        assert_eq!(result.structured_content, Some(serde_json::json!({"a": 1})));
455    }
456
457    #[test]
458    fn test_string_into_resource_result() {
459        let result = "content".to_string().into_resource_result("file:///test");
460        assert_eq!(result.first_text(), Some("content"));
461        match &result.contents[0] {
462            crate::content::ResourceContents::Text(t) => assert_eq!(t.uri, "file:///test"),
463            _ => panic!("Expected text resource contents"),
464        }
465    }
466
467    #[test]
468    fn test_messages_into_prompt_result() {
469        let messages = vec![Message::user("Hello")];
470        let result = messages.into_prompt_result();
471        assert_eq!(result.len(), 1);
472    }
473}