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