Skip to main content

turbomcp_core/
response.rs

1//! Response traits for ergonomic tool handler returns.
2//!
3//! This module provides the `IntoToolResponse` trait, inspired by axum's `IntoResponse`,
4//! allowing handlers to return various types that can be converted into `CallToolResult`.
5//!
6//! # Features
7//!
8//! - `no_std` compatible (uses `alloc`)
9//! - Automatic conversion from common types (String, numbers, bool, etc.)
10//! - Result and Option support for error handling with `?` operator
11//! - Wrapper types for explicit control (Json, Text, Image)
12//!
13//! # Example
14//!
15//! ```ignore
16//! use turbomcp_core::response::IntoToolResponse;
17//!
18//! // Return a simple string
19//! async fn greet(name: String) -> impl IntoToolResponse {
20//!     format!("Hello, {}!", name)
21//! }
22//!
23//! // Return JSON with automatic serialization
24//! async fn get_data() -> impl IntoToolResponse {
25//!     Json(MyData { value: 42 })
26//! }
27//!
28//! // Use ? operator with automatic error conversion
29//! async fn fetch_data() -> Result<String, ToolError> {
30//!     let data = some_fallible_operation()?;
31//!     Ok(format!("Got: {}", data))
32//! }
33//! ```
34
35use alloc::format;
36use alloc::string::{String, ToString};
37use alloc::vec;
38use alloc::vec::Vec;
39use core::fmt::Display;
40
41use serde::Serialize;
42
43use crate::types::content::Content;
44use crate::types::tools::CallToolResult;
45
46/// Trait for types that can be converted into a tool response.
47///
48/// This is the primary trait for ergonomic tool handler returns.
49/// Implement this trait to allow your types to be returned directly from handlers.
50///
51/// # Built-in Implementations
52///
53/// - `String`, `&str` - Returns as text content
54/// - `CallToolResult` - Passed through as-is
55/// - `Json<T>` - Serializes to JSON text
56/// - `Result<T, E>` where `T: IntoToolResponse`, `E: Into<ToolError>` - Handles errors automatically
57/// - `()` - Returns empty success response
58/// - Numeric types (`i32`, `i64`, `f64`, etc.) - Returns as text
59/// - `bool` - Returns as "true" or "false"
60///
61/// # Example
62///
63/// ```ignore
64/// // Simple string return
65/// async fn handler() -> impl IntoToolResponse {
66///     "Hello, world!"
67/// }
68///
69/// // Automatic error handling
70/// async fn handler() -> Result<String, ToolError> {
71///     let data = fallible_operation()?;
72///     Ok(format!("Got: {}", data))
73/// }
74/// ```
75pub trait IntoToolResponse {
76    /// Convert this type into a `CallToolResult`
77    fn into_tool_response(self) -> CallToolResult;
78}
79
80// ============================================================================
81// Core implementations
82// ============================================================================
83
84impl IntoToolResponse for CallToolResult {
85    #[inline]
86    fn into_tool_response(self) -> CallToolResult {
87        self
88    }
89}
90
91impl IntoToolResponse for String {
92    #[inline]
93    fn into_tool_response(self) -> CallToolResult {
94        CallToolResult::text(self)
95    }
96}
97
98impl IntoToolResponse for &str {
99    #[inline]
100    fn into_tool_response(self) -> CallToolResult {
101        CallToolResult::text(self)
102    }
103}
104
105impl IntoToolResponse for () {
106    #[inline]
107    fn into_tool_response(self) -> CallToolResult {
108        CallToolResult {
109            content: vec![],
110            is_error: None,
111            _meta: None,
112        }
113    }
114}
115
116// Numeric type implementations
117macro_rules! impl_into_tool_response_for_numeric {
118    ($($t:ty),*) => {
119        $(
120            impl IntoToolResponse for $t {
121                #[inline]
122                fn into_tool_response(self) -> CallToolResult {
123                    CallToolResult::text(self.to_string())
124                }
125            }
126        )*
127    };
128}
129
130impl_into_tool_response_for_numeric!(
131    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
132);
133
134impl IntoToolResponse for bool {
135    #[inline]
136    fn into_tool_response(self) -> CallToolResult {
137        CallToolResult::text(self.to_string())
138    }
139}
140
141impl IntoToolResponse for Content {
142    #[inline]
143    fn into_tool_response(self) -> CallToolResult {
144        CallToolResult {
145            content: vec![self],
146            is_error: None,
147            _meta: None,
148        }
149    }
150}
151
152impl IntoToolResponse for Vec<Content> {
153    #[inline]
154    fn into_tool_response(self) -> CallToolResult {
155        CallToolResult {
156            content: self,
157            is_error: None,
158            _meta: None,
159        }
160    }
161}
162
163// ============================================================================
164// Result implementations - enables ? operator
165// ============================================================================
166
167impl<T, E> IntoToolResponse for Result<T, E>
168where
169    T: IntoToolResponse,
170    E: Into<ToolError>,
171{
172    fn into_tool_response(self) -> CallToolResult {
173        match self {
174            Ok(v) => v.into_tool_response(),
175            Err(e) => {
176                let error: ToolError = e.into();
177                error.into_tool_response()
178            }
179        }
180    }
181}
182
183// ============================================================================
184// Convenience wrapper types
185// ============================================================================
186
187/// Wrapper for returning JSON-serialized data from a tool handler.
188///
189/// Automatically serializes the inner value to pretty-printed JSON.
190///
191/// # Example
192///
193/// ```ignore
194/// use turbomcp_core::response::Json;
195///
196/// #[derive(Serialize)]
197/// struct UserData {
198///     name: String,
199///     age: u32,
200/// }
201///
202/// async fn get_user() -> impl IntoToolResponse {
203///     Json(UserData {
204///         name: "Alice".into(),
205///         age: 30,
206///     })
207/// }
208/// ```
209#[derive(Debug, Clone)]
210pub struct Json<T>(pub T);
211
212impl<T: Serialize> IntoToolResponse for Json<T> {
213    fn into_tool_response(self) -> CallToolResult {
214        match serde_json::to_string_pretty(&self.0) {
215            Ok(json) => {
216                // Enforce size limit to prevent DoS from oversized responses
217                if json.len() > crate::MAX_MESSAGE_SIZE {
218                    return ToolError::new(format!(
219                        "JSON output too large: {} bytes exceeds {} byte limit",
220                        json.len(),
221                        crate::MAX_MESSAGE_SIZE
222                    ))
223                    .into_tool_response();
224                }
225                CallToolResult::text(json)
226            }
227            Err(e) => {
228                ToolError::new(format!("JSON serialization failed: {e}")).into_tool_response()
229            }
230        }
231    }
232}
233
234impl<T: Serialize> turbomcp_types::IntoToolResult for Json<T> {
235    fn into_tool_result(self) -> turbomcp_types::ToolResult {
236        match serde_json::to_string_pretty(&self.0) {
237            Ok(json) => {
238                // Enforce size limit to prevent DoS from oversized responses
239                if json.len() > crate::MAX_MESSAGE_SIZE {
240                    return turbomcp_types::ToolResult::error(format!(
241                        "JSON output too large: {} bytes exceeds {} byte limit",
242                        json.len(),
243                        crate::MAX_MESSAGE_SIZE
244                    ));
245                }
246                turbomcp_types::ToolResult::text(json)
247            }
248            Err(e) => turbomcp_types::ToolResult::error(format!("JSON serialization failed: {e}")),
249        }
250    }
251}
252
253/// Wrapper for explicitly returning text content.
254///
255/// This is semantically equivalent to returning a `String`, but makes intent clearer.
256///
257/// # Example
258///
259/// ```ignore
260/// async fn handler() -> impl IntoToolResponse {
261///     Text("Operation completed successfully")
262/// }
263/// ```
264#[derive(Debug, Clone)]
265pub struct Text<T>(pub T);
266
267impl<T: Into<String>> IntoToolResponse for Text<T> {
268    #[inline]
269    fn into_tool_response(self) -> CallToolResult {
270        CallToolResult::text(self.0)
271    }
272}
273
274/// Wrapper for returning base64-encoded image data.
275///
276/// # Example
277///
278/// ```ignore
279/// async fn get_image() -> impl IntoToolResponse {
280///     Image {
281///         data: base64_encoded_png,
282///         mime_type: "image/png",
283///     }
284/// }
285/// ```
286#[derive(Debug, Clone)]
287pub struct Image<D, M> {
288    /// Base64-encoded image data
289    pub data: D,
290    /// MIME type of the image (e.g., "image/png", "image/jpeg")
291    pub mime_type: M,
292}
293
294impl<D: Into<String>, M: Into<String>> IntoToolResponse for Image<D, M> {
295    #[inline]
296    fn into_tool_response(self) -> CallToolResult {
297        CallToolResult {
298            content: vec![Content::image(self.data, self.mime_type)],
299            is_error: None,
300            _meta: None,
301        }
302    }
303}
304
305// ============================================================================
306// Error handling
307// ============================================================================
308
309/// Error type for tool handlers that supports the `?` operator.
310///
311/// This type can be created from any error that implements `Display`,
312/// allowing idiomatic Rust error handling in tool handlers.
313///
314/// # Example
315///
316/// ```ignore
317/// use turbomcp_core::response::ToolError;
318///
319/// async fn handler(path: String) -> Result<String, ToolError> {
320///     // Use ? operator - errors automatically convert to ToolError
321///     let file = std::fs::read_to_string(&path)?;
322///     Ok(format!("Read {} bytes", file.len()))
323/// }
324///
325/// // Create errors manually
326/// async fn validate(value: i32) -> Result<String, ToolError> {
327///     if value < 0 {
328///         return Err(ToolError::new("Value must be non-negative"));
329///     }
330///     Ok("Valid".into())
331/// }
332/// ```
333#[derive(Debug, Clone)]
334pub struct ToolError {
335    message: String,
336    code: Option<i32>,
337}
338
339impl ToolError {
340    /// Create a new tool error with the given message.
341    pub fn new(message: impl Into<String>) -> Self {
342        Self {
343            message: message.into(),
344            code: None,
345        }
346    }
347
348    /// Create a new tool error with a custom error code.
349    pub fn with_code(code: i32, message: impl Into<String>) -> Self {
350        Self {
351            message: message.into(),
352            code: Some(code),
353        }
354    }
355
356    /// Get the error message.
357    pub fn message(&self) -> &str {
358        &self.message
359    }
360
361    /// Get the error code, if any.
362    pub fn code(&self) -> Option<i32> {
363        self.code
364    }
365}
366
367impl IntoToolResponse for ToolError {
368    #[inline]
369    fn into_tool_response(self) -> CallToolResult {
370        CallToolResult::error(self.message)
371    }
372}
373
374impl Display for ToolError {
375    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
376        write!(f, "{}", self.message)
377    }
378}
379
380// Note: std::error::Error requires std, so we only implement it when std is available
381#[cfg(feature = "std")]
382impl std::error::Error for ToolError {}
383
384// ============================================================================
385// From implementations for common error types
386// ============================================================================
387
388impl From<&str> for ToolError {
389    fn from(s: &str) -> Self {
390        Self {
391            message: s.into(),
392            code: None,
393        }
394    }
395}
396
397impl From<String> for ToolError {
398    fn from(s: String) -> Self {
399        Self {
400            message: s,
401            code: None,
402        }
403    }
404}
405
406impl From<serde_json::Error> for ToolError {
407    fn from(e: serde_json::Error) -> Self {
408        Self {
409            message: e.to_string(),
410            code: None,
411        }
412    }
413}
414
415// McpError conversion - enables McpResult<T> to work with IntoToolResponse
416impl From<crate::error::McpError> for ToolError {
417    fn from(e: crate::error::McpError) -> Self {
418        Self {
419            message: e.to_string(),
420            code: Some(e.jsonrpc_code()),
421        }
422    }
423}
424
425// std-only error conversions
426#[cfg(feature = "std")]
427impl From<std::io::Error> for ToolError {
428    fn from(e: std::io::Error) -> Self {
429        Self {
430            message: e.to_string(),
431            code: None,
432        }
433    }
434}
435
436#[cfg(feature = "std")]
437impl From<std::string::FromUtf8Error> for ToolError {
438    fn from(e: std::string::FromUtf8Error) -> Self {
439        Self {
440            message: e.to_string(),
441            code: None,
442        }
443    }
444}
445
446#[cfg(feature = "std")]
447impl From<std::num::ParseIntError> for ToolError {
448    fn from(e: std::num::ParseIntError) -> Self {
449        Self {
450            message: e.to_string(),
451            code: None,
452        }
453    }
454}
455
456#[cfg(feature = "std")]
457impl From<std::num::ParseFloatError> for ToolError {
458    fn from(e: std::num::ParseFloatError) -> Self {
459        Self {
460            message: e.to_string(),
461            code: None,
462        }
463    }
464}
465
466#[cfg(feature = "std")]
467impl From<Box<dyn std::error::Error>> for ToolError {
468    fn from(e: Box<dyn std::error::Error>) -> Self {
469        Self {
470            message: e.to_string(),
471            code: None,
472        }
473    }
474}
475
476#[cfg(feature = "std")]
477impl From<Box<dyn std::error::Error + Send + Sync>> for ToolError {
478    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
479        Self {
480            message: e.to_string(),
481            code: None,
482        }
483    }
484}
485
486/// Convenience trait for converting to ToolError with context.
487///
488/// Provides `.tool_err()` method for easy error conversion with custom messages.
489///
490/// # Example
491///
492/// ```ignore
493/// use turbomcp_core::response::IntoToolError;
494///
495/// fn process() -> Result<(), ToolError> {
496///     some_operation()
497///         .map_err(|e| e.tool_err("Failed to process"))?;
498///     Ok(())
499/// }
500/// ```
501pub trait IntoToolError {
502    /// Convert to a ToolError with additional context
503    fn tool_err(self, context: impl Display) -> ToolError;
504}
505
506impl<E: Display> IntoToolError for E {
507    fn tool_err(self, context: impl Display) -> ToolError {
508        ToolError::new(format!("{}: {}", context, self))
509    }
510}
511
512// ============================================================================
513// Tuple implementations for combining content
514// ============================================================================
515
516impl<A, B> IntoToolResponse for (A, B)
517where
518    A: IntoToolResponse,
519    B: IntoToolResponse,
520{
521    fn into_tool_response(self) -> CallToolResult {
522        let a = self.0.into_tool_response();
523        let b = self.1.into_tool_response();
524
525        let mut content = a.content;
526        content.extend(b.content);
527
528        CallToolResult {
529            content,
530            is_error: a.is_error.or(b.is_error),
531            _meta: None,
532        }
533    }
534}
535
536// ============================================================================
537// Option implementation
538// ============================================================================
539
540impl<T: IntoToolResponse> IntoToolResponse for Option<T> {
541    fn into_tool_response(self) -> CallToolResult {
542        match self {
543            Some(v) => v.into_tool_response(),
544            None => CallToolResult::text("No result"),
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn test_string_into_response() {
555        let response = "hello".into_tool_response();
556        assert_eq!(response.content.len(), 1);
557        assert!(response.is_error.is_none());
558    }
559
560    #[test]
561    fn test_owned_string_into_response() {
562        let response = String::from("hello").into_tool_response();
563        assert_eq!(response.content.len(), 1);
564    }
565
566    #[test]
567    fn test_json_into_response() {
568        let data = serde_json::json!({"key": "value"});
569        let response = Json(data).into_tool_response();
570        assert_eq!(response.content.len(), 1);
571    }
572
573    #[test]
574    fn test_tool_error_into_response() {
575        let error = ToolError::new("something went wrong");
576        let response = error.into_tool_response();
577        assert_eq!(response.is_error, Some(true));
578    }
579
580    #[test]
581    fn test_result_ok_into_response() {
582        let result: Result<String, ToolError> = Ok("success".into());
583        let response = result.into_tool_response();
584        assert!(response.is_error.is_none());
585    }
586
587    #[test]
588    fn test_result_err_into_response() {
589        let result: Result<String, ToolError> = Err(ToolError::new("failed"));
590        let response = result.into_tool_response();
591        assert_eq!(response.is_error, Some(true));
592    }
593
594    #[test]
595    fn test_unit_into_response() {
596        let response = ().into_tool_response();
597        assert!(response.content.is_empty());
598    }
599
600    #[test]
601    fn test_option_some_into_response() {
602        let response = Some("value").into_tool_response();
603        assert_eq!(response.content.len(), 1);
604    }
605
606    #[test]
607    fn test_option_none_into_response() {
608        let response: CallToolResult = None::<String>.into_tool_response();
609        assert_eq!(response.content.len(), 1);
610    }
611
612    #[test]
613    fn test_tuple_into_response() {
614        let response = ("first", "second").into_tool_response();
615        assert_eq!(response.content.len(), 2);
616    }
617
618    #[test]
619    fn test_text_wrapper() {
620        let response = Text("explicit text").into_tool_response();
621        assert_eq!(response.content.len(), 1);
622    }
623
624    #[test]
625    fn test_image_wrapper() {
626        let response = Image {
627            data: "base64data",
628            mime_type: "image/png",
629        }
630        .into_tool_response();
631        assert_eq!(response.content.len(), 1);
632    }
633
634    #[test]
635    fn test_numeric_types() {
636        assert_eq!(42i32.into_tool_response().content.len(), 1);
637        assert_eq!(42i64.into_tool_response().content.len(), 1);
638        assert_eq!(2.5f64.into_tool_response().content.len(), 1);
639    }
640
641    #[test]
642    fn test_bool_into_response() {
643        let true_response = true.into_tool_response();
644        let false_response = false.into_tool_response();
645        assert_eq!(true_response.content.len(), 1);
646        assert_eq!(false_response.content.len(), 1);
647    }
648
649    #[test]
650    fn test_json_size_limit_enforcement() {
651        // Create JSON data larger than MAX_MESSAGE_SIZE (1MB)
652        let large_string = "x".repeat(crate::MAX_MESSAGE_SIZE + 100);
653        let large_data = serde_json::json!({ "data": large_string });
654        let response = Json(large_data).into_tool_response();
655
656        // Should return an error response
657        assert_eq!(response.is_error, Some(true));
658        assert_eq!(response.content.len(), 1);
659
660        // Verify error message mentions size limit
661        if let Content::Text { text, .. } = &response.content[0] {
662            assert!(text.contains("too large"));
663            assert!(text.contains("byte limit"));
664        } else {
665            panic!("Expected text content in error response");
666        }
667    }
668
669    #[test]
670    fn test_json_within_size_limit() {
671        // Normal JSON should work fine
672        let small_data = serde_json::json!({ "key": "value" });
673        let response = Json(small_data).into_tool_response();
674
675        // Should succeed
676        assert!(response.is_error.is_none() || response.is_error == Some(false));
677        assert_eq!(response.content.len(), 1);
678    }
679}