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/// Pretty-print `value` to JSON and enforce `MAX_MESSAGE_SIZE`.
206///
207/// Returns the encoded JSON on success, or a user-facing error string suitable
208/// for placing into a tool-result error variant.
209fn encode_json_for_tool<T: Serialize>(value: &T) -> Result<String, String> {
210    match serde_json::to_string_pretty(value) {
211        Ok(json) if json.len() > crate::MAX_MESSAGE_SIZE => Err(format!(
212            "JSON output too large: {} bytes exceeds {} byte limit",
213            json.len(),
214            crate::MAX_MESSAGE_SIZE
215        )),
216        Ok(json) => Ok(json),
217        Err(e) => Err(format!("JSON serialization failed: {e}")),
218    }
219}
220
221impl<T: Serialize> IntoToolResponse for Json<T> {
222    fn into_tool_response(self) -> CallToolResult {
223        match encode_json_for_tool(&self.0) {
224            Ok(json) => CallToolResult::text(json),
225            Err(msg) => ToolError::new(msg).into_tool_response(),
226        }
227    }
228}
229
230impl<T: Serialize> turbomcp_types::IntoToolResult for Json<T> {
231    fn into_tool_result(self) -> turbomcp_types::ToolResult {
232        match encode_json_for_tool(&self.0) {
233            Ok(json) => turbomcp_types::ToolResult::text(json),
234            Err(msg) => turbomcp_types::ToolResult::error(msg),
235        }
236    }
237}
238
239/// Wrapper for explicitly returning text content.
240///
241/// This is semantically equivalent to returning a `String`, but makes intent clearer.
242///
243/// # Example
244///
245/// ```ignore
246/// async fn handler() -> impl IntoToolResponse {
247///     Text("Operation completed successfully")
248/// }
249/// ```
250#[derive(Debug, Clone)]
251pub struct Text<T>(pub T);
252
253impl<T: Into<String>> IntoToolResponse for Text<T> {
254    #[inline]
255    fn into_tool_response(self) -> CallToolResult {
256        CallToolResult::text(self.0)
257    }
258}
259
260/// Wrapper for returning base64-encoded image data.
261///
262/// # Example
263///
264/// ```ignore
265/// async fn get_image() -> impl IntoToolResponse {
266///     Image {
267///         data: base64_encoded_png,
268///         mime_type: "image/png",
269///     }
270/// }
271/// ```
272#[derive(Debug, Clone)]
273pub struct Image<D, M> {
274    /// Base64-encoded image data
275    pub data: D,
276    /// MIME type of the image (e.g., "image/png", "image/jpeg")
277    pub mime_type: M,
278}
279
280impl<D: Into<String>, M: Into<String>> IntoToolResponse for Image<D, M> {
281    #[inline]
282    fn into_tool_response(self) -> CallToolResult {
283        CallToolResult {
284            content: vec![Content::image(self.data, self.mime_type)],
285            ..Default::default()
286        }
287    }
288}
289
290// ============================================================================
291// Error handling
292// ============================================================================
293
294/// Error type for tool handlers that supports the `?` operator.
295///
296/// This type can be created from any error that implements `Display`,
297/// allowing idiomatic Rust error handling in tool handlers.
298///
299/// # Example
300///
301/// ```ignore
302/// use turbomcp_core::response::ToolError;
303///
304/// async fn handler(path: String) -> Result<String, ToolError> {
305///     // Use ? operator - errors automatically convert to ToolError
306///     let file = std::fs::read_to_string(&path)?;
307///     Ok(format!("Read {} bytes", file.len()))
308/// }
309///
310/// // Create errors manually
311/// async fn validate(value: i32) -> Result<String, ToolError> {
312///     if value < 0 {
313///         return Err(ToolError::new("Value must be non-negative"));
314///     }
315///     Ok("Valid".into())
316/// }
317/// ```
318#[derive(Debug, Clone)]
319pub struct ToolError {
320    message: String,
321    code: Option<i32>,
322}
323
324impl ToolError {
325    /// Create a new tool error with the given message.
326    pub fn new(message: impl Into<String>) -> Self {
327        Self {
328            message: message.into(),
329            code: None,
330        }
331    }
332
333    /// Create a new tool error with a custom error code.
334    pub fn with_code(code: i32, message: impl Into<String>) -> Self {
335        Self {
336            message: message.into(),
337            code: Some(code),
338        }
339    }
340
341    /// Get the error message.
342    pub fn message(&self) -> &str {
343        &self.message
344    }
345
346    /// Get the error code, if any.
347    pub fn code(&self) -> Option<i32> {
348        self.code
349    }
350}
351
352impl IntoToolResponse for ToolError {
353    #[inline]
354    fn into_tool_response(self) -> CallToolResult {
355        CallToolResult::error(self.message)
356    }
357}
358
359impl Display for ToolError {
360    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
361        write!(f, "{}", self.message)
362    }
363}
364
365// Note: std::error::Error requires std, so we only implement it when std is available
366#[cfg(feature = "std")]
367impl std::error::Error for ToolError {}
368
369// ============================================================================
370// From implementations for common error types
371// ============================================================================
372
373impl From<&str> for ToolError {
374    fn from(s: &str) -> Self {
375        Self {
376            message: s.into(),
377            code: None,
378        }
379    }
380}
381
382impl From<String> for ToolError {
383    fn from(s: String) -> Self {
384        Self {
385            message: s,
386            code: None,
387        }
388    }
389}
390
391impl From<serde_json::Error> for ToolError {
392    fn from(e: serde_json::Error) -> Self {
393        Self {
394            message: e.to_string(),
395            code: None,
396        }
397    }
398}
399
400// McpError conversion - enables McpResult<T> to work with IntoToolResponse
401impl From<crate::error::McpError> for ToolError {
402    fn from(e: crate::error::McpError) -> Self {
403        Self {
404            message: e.to_string(),
405            code: Some(e.jsonrpc_code()),
406        }
407    }
408}
409
410// std-only error conversions
411#[cfg(feature = "std")]
412impl From<std::io::Error> for ToolError {
413    fn from(e: std::io::Error) -> Self {
414        Self {
415            message: e.to_string(),
416            code: None,
417        }
418    }
419}
420
421#[cfg(feature = "std")]
422impl From<std::string::FromUtf8Error> for ToolError {
423    fn from(e: std::string::FromUtf8Error) -> Self {
424        Self {
425            message: e.to_string(),
426            code: None,
427        }
428    }
429}
430
431#[cfg(feature = "std")]
432impl From<std::num::ParseIntError> for ToolError {
433    fn from(e: std::num::ParseIntError) -> Self {
434        Self {
435            message: e.to_string(),
436            code: None,
437        }
438    }
439}
440
441#[cfg(feature = "std")]
442impl From<std::num::ParseFloatError> for ToolError {
443    fn from(e: std::num::ParseFloatError) -> Self {
444        Self {
445            message: e.to_string(),
446            code: None,
447        }
448    }
449}
450
451#[cfg(feature = "std")]
452impl From<Box<dyn std::error::Error>> for ToolError {
453    fn from(e: Box<dyn std::error::Error>) -> Self {
454        Self {
455            message: e.to_string(),
456            code: None,
457        }
458    }
459}
460
461#[cfg(feature = "std")]
462impl From<Box<dyn std::error::Error + Send + Sync>> for ToolError {
463    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
464        Self {
465            message: e.to_string(),
466            code: None,
467        }
468    }
469}
470
471/// Convenience trait for converting to ToolError with context.
472///
473/// Provides `.tool_err()` method for easy error conversion with custom messages.
474///
475/// # Example
476///
477/// ```ignore
478/// use turbomcp_core::response::IntoToolError;
479///
480/// fn process() -> Result<(), ToolError> {
481///     some_operation()
482///         .map_err(|e| e.tool_err("Failed to process"))?;
483///     Ok(())
484/// }
485/// ```
486pub trait IntoToolError {
487    /// Convert to a ToolError with additional context
488    fn tool_err(self, context: impl Display) -> ToolError;
489}
490
491impl<E: Display> IntoToolError for E {
492    fn tool_err(self, context: impl Display) -> ToolError {
493        ToolError::new(format!("{}: {}", context, self))
494    }
495}
496
497// ============================================================================
498// Tuple implementations for combining content
499// ============================================================================
500
501impl<A, B> IntoToolResponse for (A, B)
502where
503    A: IntoToolResponse,
504    B: IntoToolResponse,
505{
506    fn into_tool_response(self) -> CallToolResult {
507        let a = self.0.into_tool_response();
508        let b = self.1.into_tool_response();
509
510        let mut content = a.content;
511        content.extend(b.content);
512
513        CallToolResult {
514            content,
515            is_error: a.is_error.or(b.is_error),
516            ..Default::default()
517        }
518    }
519}
520
521// ============================================================================
522// Option implementation
523// ============================================================================
524
525impl<T: IntoToolResponse> IntoToolResponse for Option<T> {
526    fn into_tool_response(self) -> CallToolResult {
527        match self {
528            Some(v) => v.into_tool_response(),
529            None => CallToolResult::text("No result"),
530        }
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_string_into_response() {
540        let response = "hello".into_tool_response();
541        assert_eq!(response.content.len(), 1);
542        assert!(response.is_error.is_none());
543    }
544
545    #[test]
546    fn test_owned_string_into_response() {
547        let response = String::from("hello").into_tool_response();
548        assert_eq!(response.content.len(), 1);
549    }
550
551    #[test]
552    fn test_json_into_response() {
553        let data = serde_json::json!({"key": "value"});
554        let response = Json(data).into_tool_response();
555        assert_eq!(response.content.len(), 1);
556    }
557
558    #[test]
559    fn test_tool_error_into_response() {
560        let error = ToolError::new("something went wrong");
561        let response = error.into_tool_response();
562        assert_eq!(response.is_error, Some(true));
563    }
564
565    #[test]
566    fn test_result_ok_into_response() {
567        let result: Result<String, ToolError> = Ok("success".into());
568        let response = result.into_tool_response();
569        assert!(response.is_error.is_none());
570    }
571
572    #[test]
573    fn test_result_err_into_response() {
574        let result: Result<String, ToolError> = Err(ToolError::new("failed"));
575        let response = result.into_tool_response();
576        assert_eq!(response.is_error, Some(true));
577    }
578
579    #[test]
580    fn test_unit_into_response() {
581        let response = ().into_tool_response();
582        assert!(response.content.is_empty());
583    }
584
585    #[test]
586    fn test_option_some_into_response() {
587        let response = Some("value").into_tool_response();
588        assert_eq!(response.content.len(), 1);
589    }
590
591    #[test]
592    fn test_option_none_into_response() {
593        let response: CallToolResult = None::<String>.into_tool_response();
594        assert_eq!(response.content.len(), 1);
595    }
596
597    #[test]
598    fn test_tuple_into_response() {
599        let response = ("first", "second").into_tool_response();
600        assert_eq!(response.content.len(), 2);
601    }
602
603    #[test]
604    fn test_text_wrapper() {
605        let response = Text("explicit text").into_tool_response();
606        assert_eq!(response.content.len(), 1);
607    }
608
609    #[test]
610    fn test_image_wrapper() {
611        let response = Image {
612            data: "base64data",
613            mime_type: "image/png",
614        }
615        .into_tool_response();
616        assert_eq!(response.content.len(), 1);
617    }
618
619    #[test]
620    fn test_numeric_types() {
621        assert_eq!(42i32.into_tool_response().content.len(), 1);
622        assert_eq!(42i64.into_tool_response().content.len(), 1);
623        assert_eq!(2.5f64.into_tool_response().content.len(), 1);
624    }
625
626    #[test]
627    fn test_bool_into_response() {
628        let true_response = true.into_tool_response();
629        let false_response = false.into_tool_response();
630        assert_eq!(true_response.content.len(), 1);
631        assert_eq!(false_response.content.len(), 1);
632    }
633
634    #[test]
635    fn test_json_size_limit_enforcement() {
636        // Create JSON data larger than MAX_MESSAGE_SIZE (1MB)
637        let large_string = "x".repeat(crate::MAX_MESSAGE_SIZE + 100);
638        let large_data = serde_json::json!({ "data": large_string });
639        let response = Json(large_data).into_tool_response();
640
641        // Should return an error response
642        assert_eq!(response.is_error, Some(true));
643        assert_eq!(response.content.len(), 1);
644
645        // Verify error message mentions size limit
646        if let Content::Text(text) = &response.content[0] {
647            assert!(text.text.contains("too large"));
648            assert!(text.text.contains("byte limit"));
649        } else {
650            panic!("Expected text content in error response");
651        }
652    }
653
654    #[test]
655    fn test_json_within_size_limit() {
656        // Normal JSON should work fine
657        let small_data = serde_json::json!({ "key": "value" });
658        let response = Json(small_data).into_tool_response();
659
660        // Should succeed
661        assert!(response.is_error.is_none() || response.is_error == Some(false));
662        assert_eq!(response.content.len(), 1);
663    }
664}