Skip to main content

turbomcp_protocol/types/
tools.rs

1//! Types for the MCP tool-calling system.
2//!
3//! This module defines the data structures for defining tools, their input/output schemas,
4//! and the requests and responses used to list and execute them, as specified by the MCP standard.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::{content::ContentBlock, core::Cursor};
10
11/// Optional metadata hints about a tool's behavior.
12///
13/// **Critical Warning** (from MCP spec):
14/// > "All properties in ToolAnnotations are **hints**. They are not guaranteed to
15/// > provide a faithful description of tool behavior. **Clients should never make
16/// > tool use decisions based on ToolAnnotations received from untrusted servers.**"
17///
18/// These fields are useful for UI display and general guidance, but should never
19/// be trusted for security decisions or behavioral assumptions.
20#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct ToolAnnotations {
22    /// A user-friendly title for display in UIs (hint only).
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub title: Option<String>,
25    /// Role-based audience hint. Per spec, should be `"user"` or `"assistant"` (hint only).
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub audience: Option<Vec<String>>,
28    /// Subjective priority for UI sorting (hint only, often ignored).
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub priority: Option<f64>,
31    /// **Hint** that the tool may perform destructive actions (e.g., deleting data).
32    ///
33    /// Do not trust this for security decisions. Default: `true` if not specified.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    #[serde(rename = "destructiveHint")]
36    pub destructive_hint: Option<bool>,
37    /// **Hint** that repeated calls with same args have no additional effects.
38    ///
39    /// Useful for retry logic, but verify actual behavior. Default: `false` if not specified.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[serde(rename = "idempotentHint")]
42    pub idempotent_hint: Option<bool>,
43    /// **Hint** that the tool may interact with external systems or the real world.
44    ///
45    /// Do not trust this for sandboxing decisions. Default: `true` if not specified.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    #[serde(rename = "openWorldHint")]
48    pub open_world_hint: Option<bool>,
49    /// **Hint** that the tool does not modify state (read-only).
50    ///
51    /// Do not trust this for security decisions. Default: `false` if not specified.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    #[serde(rename = "readOnlyHint")]
54    pub read_only_hint: Option<bool>,
55
56    /// **Hint** for task augmentation support (MCP 2025-11-25 draft, SEP-1686)
57    ///
58    /// Indicates whether this tool supports task-augmented invocation:
59    /// - `never` (default): Tool MUST NOT be invoked as a task
60    /// - `optional`: Tool MAY be invoked as a task or normal request
61    /// - `always`: Tool SHOULD be invoked as a task (server may reject non-task calls)
62    ///
63    /// This is a **hint** and does not guarantee behavioral conformance.
64    ///
65    /// ## Capability Requirements
66    ///
67    /// If `tasks.requests.tools.call` capability is false, clients MUST ignore this hint.
68    /// If capability is true:
69    /// - `taskHint` absent or `"never"`: MUST NOT invoke as task
70    /// - `taskHint: "optional"`: MAY invoke as task
71    /// - `taskHint: "always"`: SHOULD invoke as task
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[serde(rename = "taskHint")]
74    pub task_hint: Option<TaskHint>,
75
76    /// Custom application-specific hints.
77    #[serde(flatten)]
78    pub custom: HashMap<String, serde_json::Value>,
79}
80
81/// Task hint for tool invocation (MCP 2025-11-25 draft, SEP-1686)
82///
83/// Indicates how a tool should be invoked with respect to task augmentation.
84/// Note: This is kept for backward compatibility. The newer API uses
85/// `ToolExecution.task_support` with `TaskSupportMode`.
86#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
87#[serde(rename_all = "lowercase")]
88pub enum TaskHint {
89    /// Tool MUST NOT be invoked as a task (default behavior)
90    Never,
91    /// Tool MAY be invoked as either a task or normal request
92    Optional,
93    /// Tool SHOULD be invoked as a task (server may reject non-task calls)
94    Always,
95}
96
97/// Task support mode for tool execution (MCP 2025-11-25)
98///
99/// Indicates whether this tool supports task-augmented execution.
100/// This allows clients to handle long-running operations through polling
101/// the task system.
102#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
103#[serde(rename_all = "lowercase")]
104pub enum TaskSupportMode {
105    /// Tool does not support task-augmented execution (default when absent)
106    #[default]
107    Forbidden,
108    /// Tool may support task-augmented execution
109    Optional,
110    /// Tool requires task-augmented execution
111    Required,
112}
113
114/// Execution-related properties for a tool (MCP 2025-11-25)
115///
116/// Contains execution configuration hints for tools, particularly around
117/// task-augmented execution support.
118#[derive(Debug, Clone, Serialize, Deserialize, Default)]
119pub struct ToolExecution {
120    /// Indicates whether this tool supports task-augmented execution.
121    ///
122    /// - `forbidden` (default): Tool does not support task-augmented execution
123    /// - `optional`: Tool may support task-augmented execution
124    /// - `required`: Tool requires task-augmented execution
125    #[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
126    pub task_support: Option<TaskSupportMode>,
127}
128
129/// Represents a tool that can be executed by an MCP server
130///
131/// A `Tool` definition includes its programmatic name, a human-readable description,
132/// and JSON schemas for its inputs and outputs.
133///
134/// ## Version Support
135/// - MCP 2025-06-18: name, title, description, inputSchema, outputSchema, annotations, _meta
136/// - MCP 2025-11-25 draft (SEP-973): + icons
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Tool {
139    /// The programmatic name of the tool, used to identify it in `CallToolRequest`.
140    pub name: String,
141
142    /// An optional, user-friendly title for the tool. Display name precedence is: `title`, `annotations.title`, then `name`.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub title: Option<String>,
145
146    /// A human-readable description of what the tool does, which can be used by clients or LLMs.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub description: Option<String>,
149
150    /// The JSON Schema object defining the parameters the tool accepts.
151    #[serde(rename = "inputSchema")]
152    pub input_schema: ToolInputSchema,
153
154    /// An optional JSON Schema object defining the structure of the tool's successful output.
155    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
156    pub output_schema: Option<ToolOutputSchema>,
157
158    /// Execution-related properties for this tool (MCP 2025-11-25)
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub execution: Option<ToolExecution>,
161
162    /// Optional, additional metadata providing hints about the tool's behavior.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub annotations: Option<ToolAnnotations>,
165
166    /// Optional set of icons for UI display (MCP 2025-11-25 draft, SEP-973)
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub icons: Option<Vec<super::core::Icon>>,
169
170    /// A general-purpose metadata field for custom data.
171    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
172    pub meta: Option<HashMap<String, serde_json::Value>>,
173}
174
175impl Default for Tool {
176    fn default() -> Self {
177        Self {
178            name: "unnamed_tool".to_string(), // Must have a valid name for MCP compliance
179            title: None,
180            description: None,
181            input_schema: ToolInputSchema::default(),
182            output_schema: None,
183            execution: None,
184            annotations: None,
185            icons: None,
186            meta: None,
187        }
188    }
189}
190
191impl Tool {
192    /// Creates a new `Tool` with a given name.
193    ///
194    /// # Panics
195    /// Panics if the name is empty or contains only whitespace.
196    pub fn new(name: impl Into<String>) -> Self {
197        let name = name.into();
198        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
199        Self {
200            name,
201            title: None,
202            description: None,
203            input_schema: ToolInputSchema::default(),
204            output_schema: None,
205            execution: None,
206            annotations: None,
207            icons: None,
208            meta: None,
209        }
210    }
211
212    /// Creates a new `Tool` with a name and a description.
213    ///
214    /// # Panics
215    /// Panics if the name is empty or contains only whitespace.
216    pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
217        let name = name.into();
218        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
219        Self {
220            name,
221            title: None,
222            description: Some(description.into()),
223            input_schema: ToolInputSchema::default(),
224            output_schema: None,
225            execution: None,
226            annotations: None,
227            icons: None,
228            meta: None,
229        }
230    }
231
232    /// Sets the execution properties for this tool.
233    pub fn with_execution(mut self, execution: ToolExecution) -> Self {
234        self.execution = Some(execution);
235        self
236    }
237
238    /// Sets the input schema for this tool.
239    ///
240    /// # Example
241    /// ```
242    /// # use turbomcp_protocol::types::{Tool, ToolInputSchema};
243    /// let schema = ToolInputSchema::empty();
244    /// let tool = Tool::new("my_tool").with_input_schema(schema);
245    /// ```
246    pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
247        self.input_schema = schema;
248        self
249    }
250
251    /// Sets the output schema for this tool.
252    pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
253        self.output_schema = Some(schema);
254        self
255    }
256
257    /// Sets the user-friendly title for this tool.
258    pub fn with_title(mut self, title: impl Into<String>) -> Self {
259        self.title = Some(title.into());
260        self
261    }
262
263    /// Sets the annotations for this tool.
264    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
265        self.annotations = Some(annotations);
266        self
267    }
268}
269
270/// Defines the structure of the arguments a tool accepts, as a JSON Schema object.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ToolInputSchema {
273    /// The type of the schema, which must be "object" for tool inputs.
274    #[serde(rename = "type")]
275    pub schema_type: String,
276    /// A map defining the properties (parameters) the tool accepts.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub properties: Option<HashMap<String, serde_json::Value>>,
279    /// A list of property names that are required.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub required: Option<Vec<String>>,
282    /// Whether additional, unspecified properties are allowed.
283    #[serde(
284        rename = "additionalProperties",
285        skip_serializing_if = "Option::is_none"
286    )]
287    pub additional_properties: Option<bool>,
288}
289
290impl Default for ToolInputSchema {
291    /// Creates a default `ToolInputSchema` that accepts an empty object.
292    fn default() -> Self {
293        Self {
294            schema_type: "object".to_string(),
295            properties: None,
296            required: None,
297            additional_properties: None,
298        }
299    }
300}
301
302impl ToolInputSchema {
303    /// Creates a new, empty input schema that accepts no parameters.
304    pub fn empty() -> Self {
305        Self::default()
306    }
307
308    /// Creates a new schema with a given set of properties.
309    pub fn with_properties(properties: HashMap<String, serde_json::Value>) -> Self {
310        Self {
311            schema_type: "object".to_string(),
312            properties: Some(properties),
313            required: None,
314            additional_properties: None,
315        }
316    }
317
318    /// Creates a new schema with a given set of properties and a list of required properties.
319    pub fn with_required_properties(
320        properties: HashMap<String, serde_json::Value>,
321        required: Vec<String>,
322    ) -> Self {
323        Self {
324            schema_type: "object".to_string(),
325            properties: Some(properties),
326            required: Some(required),
327            additional_properties: Some(false),
328        }
329    }
330
331    /// Adds a property to the schema using a builder pattern.
332    ///
333    /// # Example
334    /// ```
335    /// # use turbomcp_protocol::types::ToolInputSchema;
336    /// # use serde_json::json;
337    /// let schema = ToolInputSchema::empty()
338    ///     .add_property("name".to_string(), json!({ "type": "string" }));
339    /// ```
340    pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
341        self.properties
342            .get_or_insert_with(HashMap::new)
343            .insert(name, property);
344        self
345    }
346
347    /// Marks a property as required using a builder pattern.
348    ///
349    /// # Example
350    /// ```
351    /// # use turbomcp_protocol::types::ToolInputSchema;
352    /// # use serde_json::json;
353    /// let schema = ToolInputSchema::empty()
354    ///     .add_property("name".to_string(), json!({ "type": "string" }))
355    ///     .require_property("name".to_string());
356    /// ```
357    pub fn require_property(mut self, name: String) -> Self {
358        let required = self.required.get_or_insert_with(Vec::new);
359        if !required.contains(&name) {
360            required.push(name);
361        }
362        self
363    }
364}
365
366/// Defines the structure of a tool's successful output, as a JSON Schema object.
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ToolOutputSchema {
369    /// The type of the schema, which must be "object" for tool outputs.
370    #[serde(rename = "type")]
371    pub schema_type: String,
372    /// A map defining the properties of the output object.
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub properties: Option<HashMap<String, serde_json::Value>>,
375    /// A list of property names in the output that are required.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub required: Option<Vec<String>>,
378    /// Whether additional, unspecified properties are allowed in the output.
379    #[serde(
380        rename = "additionalProperties",
381        skip_serializing_if = "Option::is_none"
382    )]
383    pub additional_properties: Option<bool>,
384}
385
386/// A request to list the available tools on a server.
387#[derive(Debug, Clone, Serialize, Deserialize, Default)]
388pub struct ListToolsRequest {
389    /// An optional cursor for pagination. If provided, the server should return
390    /// the next page of results starting after this cursor.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub cursor: Option<Cursor>,
393    /// Optional metadata for the request.
394    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
395    pub _meta: Option<serde_json::Value>,
396}
397
398/// The result of a `ListToolsRequest`.
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ListToolsResult {
401    /// The list of available tools for the current page.
402    pub tools: Vec<Tool>,
403    /// An optional continuation token for retrieving the next page of results.
404    /// If `None`, there are no more results.
405    #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
406    pub next_cursor: Option<Cursor>,
407    /// Optional metadata for the result.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub _meta: Option<serde_json::Value>,
410}
411
412/// A request to execute a specific tool.
413///
414/// ## Version Support
415/// - MCP 2025-06-18: name, arguments, _meta
416/// - MCP 2025-11-25 draft (SEP-1686): + task (optional task augmentation)
417///
418/// ## Task Augmentation
419///
420/// When the `task` field is present, the receiver responds immediately with
421/// a `CreateTaskResult` containing a task ID. The actual tool result is available
422/// later via `tasks/result`.
423///
424/// ```rust,ignore
425/// use turbomcp_protocol::types::{CallToolRequest, tasks::TaskMetadata};
426///
427/// let request = CallToolRequest {
428///     name: "long_running_tool".to_string(),
429///     arguments: Some(json!({"data": "value"})),
430///     task: Some(TaskMetadata { ttl: Some(300_000) }), // 5 minute lifetime
431///     _meta: None,
432/// };
433/// ```
434#[derive(Debug, Clone, Serialize, Deserialize, Default)]
435pub struct CallToolRequest {
436    /// The programmatic name of the tool to call.
437    pub name: String,
438
439    /// The arguments to pass to the tool, conforming to its `input_schema`.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub arguments: Option<HashMap<String, serde_json::Value>>,
442
443    /// Optional task metadata for task-augmented requests (MCP 2025-11-25 draft)
444    ///
445    /// When present, this request will be executed asynchronously and the receiver
446    /// will respond immediately with a `CreateTaskResult`. The actual tool result
447    /// is available later via `tasks/result`.
448    ///
449    /// Requires:
450    /// - Server capability: `tasks.requests.tools.call`
451    /// - Tool annotation: `taskHint` must be "optional" or "always" (or absent/"never" for default)
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub task: Option<crate::types::tasks::TaskMetadata>,
454
455    /// Optional metadata for the request.
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub _meta: Option<serde_json::Value>,
458}
459
460/// The result of a `CallToolRequest`.
461#[derive(Debug, Clone, Serialize, Deserialize, Default)]
462pub struct CallToolResult {
463    /// The output of the tool, typically as a series of text or other content blocks. This is required.
464    pub content: Vec<ContentBlock>,
465    /// An optional boolean indicating whether the tool execution resulted in an error.
466    ///
467    /// When `is_error` is `true`, all content blocks should be treated as error information.
468    /// The error message may span multiple text blocks for structured error reporting.
469    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
470    pub is_error: Option<bool>,
471    /// Optional structured output from the tool, conforming to its `output_schema`.
472    ///
473    /// When present, this contains schema-validated JSON output that clients can parse
474    /// and use programmatically. Tools that return structured content SHOULD also include
475    /// the serialized JSON in a TextContent block for backward compatibility with clients
476    /// that don't support structured output.
477    ///
478    /// See [`Tool::output_schema`] for defining the expected structure.
479    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
480    pub structured_content: Option<serde_json::Value>,
481    /// Optional metadata for the result.
482    ///
483    /// This field is for client applications and tools to pass additional context that
484    /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
485    /// cache status, or internal state information.
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub _meta: Option<serde_json::Value>,
488    /// Optional task ID when tool execution is augmented with task tracking (MCP 2025-11-25 draft - SEP-1686).
489    ///
490    /// When a tool call includes task metadata, the server creates a task to track the operation
491    /// and returns the task_id here. Clients can use this to monitor progress via tasks/get
492    /// or retrieve final results via tasks/result.
493    #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
494    pub task_id: Option<String>,
495}
496
497impl CallToolResult {
498    /// Extracts and concatenates all text content from the result.
499    ///
500    /// This is useful for simple text-only tools or when you want to present
501    /// all textual output as a single string.
502    ///
503    /// # Returns
504    ///
505    /// A single string containing all text blocks concatenated with newlines.
506    /// Returns an empty string if there are no text blocks.
507    ///
508    /// # Example
509    ///
510    /// ```rust
511    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
512    ///
513    /// let result = CallToolResult {
514    ///     content: vec![
515    ///         ContentBlock::Text(TextContent {
516    ///             text: "Line 1".to_string(),
517    ///             annotations: None,
518    ///             meta: None,
519    ///         }),
520    ///         ContentBlock::Text(TextContent {
521    ///             text: "Line 2".to_string(),
522    ///             annotations: None,
523    ///             meta: None,
524    ///         }),
525    ///     ],
526    ///     is_error: None,
527    ///     structured_content: None,
528    ///     _meta: None,
529    ///     task_id: None,
530    /// };
531    ///
532    /// assert_eq!(result.all_text(), "Line 1\nLine 2");
533    /// ```
534    pub fn all_text(&self) -> String {
535        self.content
536            .iter()
537            .filter_map(|block| match block {
538                ContentBlock::Text(text) => Some(text.text.as_str()),
539                _ => None,
540            })
541            .collect::<Vec<_>>()
542            .join("\n")
543    }
544
545    /// Returns the text content of the first text block, if any.
546    ///
547    /// This is a common pattern for simple tools that return a single text response.
548    ///
549    /// # Returns
550    ///
551    /// `Some(&str)` if the first content block is text, `None` otherwise.
552    ///
553    /// # Example
554    ///
555    /// ```rust
556    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
557    ///
558    /// let result = CallToolResult {
559    ///     content: vec![
560    ///         ContentBlock::Text(TextContent {
561    ///             text: "Hello, world!".to_string(),
562    ///             annotations: None,
563    ///             meta: None,
564    ///         }),
565    ///     ],
566    ///     is_error: None,
567    ///     structured_content: None,
568    ///     _meta: None,
569    ///     task_id: None,
570    /// };
571    ///
572    /// assert_eq!(result.first_text(), Some("Hello, world!"));
573    /// ```
574    pub fn first_text(&self) -> Option<&str> {
575        self.content.first().and_then(|block| match block {
576            ContentBlock::Text(text) => Some(text.text.as_str()),
577            _ => None,
578        })
579    }
580
581    /// Checks if the tool execution resulted in an error.
582    ///
583    /// # Returns
584    ///
585    /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
586    /// (including when `is_error` is `None`).
587    ///
588    /// # Example
589    ///
590    /// ```rust
591    /// use turbomcp_protocol::types::CallToolResult;
592    ///
593    /// let success_result = CallToolResult {
594    ///     content: vec![],
595    ///     is_error: Some(false),
596    ///     structured_content: None,
597    ///     _meta: None,
598    ///     task_id: None,
599    /// };
600    /// assert!(!success_result.has_error());
601    ///
602    /// let error_result = CallToolResult {
603    ///     content: vec![],
604    ///     is_error: Some(true),
605    ///     structured_content: None,
606    ///     _meta: None,
607    ///     task_id: None,
608    /// };
609    /// assert!(error_result.has_error());
610    ///
611    /// let unspecified_result = CallToolResult {
612    ///     content: vec![],
613    ///     is_error: None,
614    ///     structured_content: None,
615    ///     _meta: None,
616    ///     task_id: None,
617    /// };
618    /// assert!(!unspecified_result.has_error());
619    /// ```
620    pub fn has_error(&self) -> bool {
621        self.is_error.unwrap_or(false)
622    }
623
624    /// Creates a user-friendly display string for the tool result.
625    ///
626    /// This method provides a formatted representation suitable for logging,
627    /// debugging, or displaying to end users. It handles multiple content types
628    /// and includes structured content and error information when present.
629    ///
630    /// # Returns
631    ///
632    /// A formatted string representing the tool result.
633    ///
634    /// # Example
635    ///
636    /// ```rust
637    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
638    ///
639    /// let result = CallToolResult {
640    ///     content: vec![
641    ///         ContentBlock::Text(TextContent {
642    ///             text: "Operation completed".to_string(),
643    ///             annotations: None,
644    ///             meta: None,
645    ///         }),
646    ///     ],
647    ///     is_error: Some(false),
648    ///     structured_content: None,
649    ///     _meta: None,
650    ///     task_id: None,
651    /// };
652    ///
653    /// let display = result.to_display_string();
654    /// assert!(display.contains("Operation completed"));
655    /// ```
656    pub fn to_display_string(&self) -> String {
657        let mut parts = Vec::new();
658
659        // Add error indicator if present
660        if self.has_error() {
661            parts.push("ERROR:".to_string());
662        }
663
664        // Process content blocks
665        for (i, block) in self.content.iter().enumerate() {
666            match block {
667                ContentBlock::Text(text) => {
668                    parts.push(text.text.clone());
669                }
670                ContentBlock::Image(img) => {
671                    parts.push(format!(
672                        "[Image: {} bytes, type: {}]",
673                        img.data.len(),
674                        img.mime_type
675                    ));
676                }
677                ContentBlock::Audio(audio) => {
678                    parts.push(format!(
679                        "[Audio: {} bytes, type: {}]",
680                        audio.data.len(),
681                        audio.mime_type
682                    ));
683                }
684                ContentBlock::ResourceLink(link) => {
685                    let desc = link.description.as_deref().unwrap_or("");
686                    let mime = link
687                        .mime_type
688                        .as_deref()
689                        .map(|m| format!(" [{}]", m))
690                        .unwrap_or_default();
691                    parts.push(format!(
692                        "[Resource: {}{}{}{}]",
693                        link.name,
694                        mime,
695                        if !desc.is_empty() { ": " } else { "" },
696                        desc
697                    ));
698                }
699                ContentBlock::Resource(_resource) => {
700                    parts.push(format!("[Embedded Resource #{}]", i + 1));
701                }
702                ContentBlock::ToolUse(tool_use) => {
703                    parts.push(format!(
704                        "[Tool Use: {} (id: {})]",
705                        tool_use.name, tool_use.id
706                    ));
707                }
708                ContentBlock::ToolResult(tool_result) => {
709                    parts.push(format!(
710                        "[Tool Result for: {}{}]",
711                        tool_result.tool_use_id,
712                        if tool_result.is_error.unwrap_or(false) {
713                            " (ERROR)"
714                        } else {
715                            ""
716                        }
717                    ));
718                }
719            }
720        }
721
722        // Add structured content indicator if present
723        if self.structured_content.is_some() {
724            parts.push("[Includes structured output]".to_string());
725        }
726
727        parts.join("\n")
728    }
729}
730
731// =============================================================================
732// Conversions from turbomcp-core types (for unified handler support)
733// =============================================================================
734//
735// These conversions enable the unified IntoToolResponse pattern, allowing
736// handlers to return core types that are automatically converted to protocol types.
737//
738// IMPORTANT NOTES:
739// - HashMap conversion: O(n) overhead due to hashbrown→std HashMap conversion
740// - Lossy conversion: Protocol's `structured_content` and `task_id` fields are
741//   NOT present in core types, so round-trip (protocol→core→protocol) loses them
742// - Resource fallback: Empty text is used if ResourceContent has neither text nor blob
743
744/// Convert core Annotations to protocol Annotations.
745///
746/// Note: Incurs O(n) conversion overhead from `hashbrown::HashMap` to `std::collections::HashMap`.
747impl From<turbomcp_core::types::core::Annotations> for super::Annotations {
748    fn from(core_ann: turbomcp_core::types::core::Annotations) -> Self {
749        // Convert hashbrown::HashMap to std::collections::HashMap
750        // Both implementations guarantee unique keys, so no data loss occurs
751        let custom: std::collections::HashMap<String, serde_json::Value> =
752            core_ann.custom.into_iter().collect();
753        Self {
754            audience: core_ann.audience,
755            priority: core_ann.priority,
756            last_modified: core_ann.last_modified,
757            custom,
758        }
759    }
760}
761
762/// Convert core Content to protocol ContentBlock
763impl From<turbomcp_core::types::content::Content> for super::ContentBlock {
764    fn from(content: turbomcp_core::types::content::Content) -> Self {
765        use turbomcp_core::types::content::Content as CoreContent;
766        match content {
767            CoreContent::Text { text, annotations } => {
768                super::ContentBlock::Text(super::TextContent {
769                    text,
770                    annotations: annotations.map(Into::into),
771                    meta: None,
772                })
773            }
774            CoreContent::Image {
775                data,
776                mime_type,
777                annotations,
778            } => super::ContentBlock::Image(super::ImageContent {
779                data,
780                mime_type,
781                annotations: annotations.map(Into::into),
782                meta: None,
783            }),
784            CoreContent::Audio {
785                data,
786                mime_type,
787                annotations,
788            } => super::ContentBlock::Audio(super::AudioContent {
789                data,
790                mime_type,
791                annotations: annotations.map(Into::into),
792                meta: None,
793            }),
794            CoreContent::Resource {
795                resource,
796                annotations,
797            } => {
798                // Convert core ResourceContent to protocol EmbeddedResource
799                // Core uses a flat struct with optional text/blob, protocol uses an enum
800                let protocol_resource = if let Some(text) = resource.text {
801                    super::ResourceContent::Text(super::TextResourceContents {
802                        uri: resource.uri,
803                        mime_type: resource.mime_type,
804                        text,
805                        meta: None,
806                    })
807                } else if let Some(blob) = resource.blob {
808                    super::ResourceContent::Blob(super::BlobResourceContents {
809                        uri: resource.uri,
810                        mime_type: resource.mime_type,
811                        blob,
812                        meta: None,
813                    })
814                } else {
815                    // Default to empty text if neither is set.
816                    // NOTE: This is a fallback for malformed core resources - callers should
817                    // ensure ResourceContent has either text or blob set.
818                    #[cfg(feature = "std")]
819                    eprintln!(
820                        "[turbomcp-protocol] WARNING: Resource '{}' has neither text nor blob content",
821                        resource.uri
822                    );
823                    super::ResourceContent::Text(super::TextResourceContents {
824                        uri: resource.uri,
825                        mime_type: resource.mime_type,
826                        text: String::new(),
827                        meta: None,
828                    })
829                };
830                super::ContentBlock::Resource(super::EmbeddedResource {
831                    resource: protocol_resource,
832                    annotations: annotations.map(Into::into),
833                    meta: None,
834                })
835            }
836        }
837    }
838}
839
840/// Convert core CallToolResult to protocol CallToolResult
841///
842/// This enables the unified IntoToolResponse pattern for native handlers.
843///
844/// **Note**: `structured_content` and `task_id` fields are set to `None` since
845/// core types don't have these fields. Round-trip conversion (protocol→core→protocol)
846/// will lose these values.
847impl From<turbomcp_core::types::tools::CallToolResult> for CallToolResult {
848    fn from(core_result: turbomcp_core::types::tools::CallToolResult) -> Self {
849        Self {
850            content: core_result.content.into_iter().map(Into::into).collect(),
851            is_error: core_result.is_error,
852            structured_content: None,
853            _meta: core_result._meta,
854            task_id: None,
855        }
856    }
857}
858
859#[cfg(test)]
860mod conversion_tests {
861    use super::*;
862    use turbomcp_core::types::content::Content as CoreContent;
863    use turbomcp_core::types::tools::CallToolResult as CoreCallToolResult;
864
865    #[test]
866    fn test_core_content_to_protocol_text() {
867        let core = CoreContent::text("hello world");
868        let protocol: ContentBlock = core.into();
869
870        match protocol {
871            ContentBlock::Text(text) => {
872                assert_eq!(text.text, "hello world");
873                assert!(text.annotations.is_none());
874            }
875            _ => panic!("Expected Text variant"),
876        }
877    }
878
879    #[test]
880    fn test_core_content_to_protocol_image() {
881        let core = CoreContent::image("base64data", "image/png");
882        let protocol: ContentBlock = core.into();
883
884        match protocol {
885            ContentBlock::Image(img) => {
886                assert_eq!(img.data, "base64data");
887                assert_eq!(img.mime_type, "image/png");
888            }
889            _ => panic!("Expected Image variant"),
890        }
891    }
892
893    #[test]
894    fn test_core_call_tool_result_to_protocol() {
895        let core = CoreCallToolResult::text("success");
896        let protocol: CallToolResult = core.into();
897
898        assert_eq!(protocol.content.len(), 1);
899        assert!(protocol.is_error.is_none());
900        assert!(protocol.structured_content.is_none());
901        assert!(protocol.task_id.is_none());
902
903        match &protocol.content[0] {
904            ContentBlock::Text(text) => assert_eq!(text.text, "success"),
905            _ => panic!("Expected Text content"),
906        }
907    }
908
909    #[test]
910    fn test_core_call_tool_result_error_preserved() {
911        let core = CoreCallToolResult::error("something failed");
912        let protocol: CallToolResult = core.into();
913
914        assert_eq!(protocol.is_error, Some(true));
915        match &protocol.content[0] {
916            ContentBlock::Text(text) => assert_eq!(text.text, "something failed"),
917            _ => panic!("Expected Text content"),
918        }
919    }
920
921    #[test]
922    fn test_annotations_conversion() {
923        use crate::types::Annotations;
924        use turbomcp_core::types::core::Annotations as CoreAnnotations;
925
926        // Create core annotations (custom field uses Default for simplicity)
927        let core = CoreAnnotations {
928            audience: Some(vec!["user".to_string(), "assistant".to_string()]),
929            priority: Some(0.75),
930            last_modified: Some("2025-01-13T12:00:00Z".to_string()),
931            custom: Default::default(),
932        };
933
934        let protocol: Annotations = core.into();
935
936        // Verify all fields are correctly converted
937        assert_eq!(
938            protocol.audience,
939            Some(vec!["user".to_string(), "assistant".to_string()])
940        );
941        assert_eq!(protocol.priority, Some(0.75));
942        assert_eq!(
943            protocol.last_modified,
944            Some("2025-01-13T12:00:00Z".to_string())
945        );
946        assert!(protocol.custom.is_empty());
947    }
948}