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-11-25: 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 schema type declaration. This may be a string or an array of strings.
274    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
275    pub schema_type: Option<serde_json::Value>,
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, or a schema constraining them.
283    #[serde(
284        rename = "additionalProperties",
285        skip_serializing_if = "Option::is_none"
286    )]
287    pub additional_properties: Option<serde_json::Value>,
288    /// Additional JSON Schema keywords preserved losslessly.
289    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
290    pub extra_keywords: HashMap<String, serde_json::Value>,
291}
292
293impl Default for ToolInputSchema {
294    /// Creates a default `ToolInputSchema` that accepts an empty object.
295    fn default() -> Self {
296        Self {
297            schema_type: Some(serde_json::Value::String("object".to_string())),
298            properties: None,
299            required: None,
300            additional_properties: None,
301            extra_keywords: HashMap::new(),
302        }
303    }
304}
305
306impl ToolInputSchema {
307    /// Creates a new, empty input schema that accepts no parameters.
308    pub fn empty() -> Self {
309        Self::default()
310    }
311
312    /// Creates a new schema with a given set of properties.
313    pub fn with_properties(properties: HashMap<String, serde_json::Value>) -> Self {
314        Self {
315            schema_type: Some(serde_json::Value::String("object".to_string())),
316            properties: Some(properties),
317            required: None,
318            additional_properties: None,
319            extra_keywords: HashMap::new(),
320        }
321    }
322
323    /// Creates a new schema with a given set of properties and a list of required properties.
324    pub fn with_required_properties(
325        properties: HashMap<String, serde_json::Value>,
326        required: Vec<String>,
327    ) -> Self {
328        Self {
329            schema_type: Some(serde_json::Value::String("object".to_string())),
330            properties: Some(properties),
331            required: Some(required),
332            additional_properties: Some(serde_json::Value::Bool(false)),
333            extra_keywords: HashMap::new(),
334        }
335    }
336
337    /// Adds a property to the schema using a builder pattern.
338    ///
339    /// # Example
340    /// ```
341    /// # use turbomcp_protocol::types::ToolInputSchema;
342    /// # use serde_json::json;
343    /// let schema = ToolInputSchema::empty()
344    ///     .add_property("name".to_string(), json!({ "type": "string" }));
345    /// ```
346    pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
347        self.properties
348            .get_or_insert_with(HashMap::new)
349            .insert(name, property);
350        self
351    }
352
353    /// Marks a property as required using a builder pattern.
354    ///
355    /// # Example
356    /// ```
357    /// # use turbomcp_protocol::types::ToolInputSchema;
358    /// # use serde_json::json;
359    /// let schema = ToolInputSchema::empty()
360    ///     .add_property("name".to_string(), json!({ "type": "string" }))
361    ///     .require_property("name".to_string());
362    /// ```
363    pub fn require_property(mut self, name: String) -> Self {
364        let required = self.required.get_or_insert_with(Vec::new);
365        if !required.contains(&name) {
366            required.push(name);
367        }
368        self
369    }
370}
371
372/// Defines the structure of a tool's successful output, as a JSON Schema object.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct ToolOutputSchema {
375    /// The schema type declaration. This may be a string or an array of strings.
376    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
377    pub schema_type: Option<serde_json::Value>,
378    /// A map defining the properties of the output object.
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub properties: Option<HashMap<String, serde_json::Value>>,
381    /// A list of property names in the output that are required.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub required: Option<Vec<String>>,
384    /// Whether additional, unspecified properties are allowed in the output, or a schema constraining them.
385    #[serde(
386        rename = "additionalProperties",
387        skip_serializing_if = "Option::is_none"
388    )]
389    pub additional_properties: Option<serde_json::Value>,
390    /// Additional JSON Schema keywords preserved losslessly.
391    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
392    pub extra_keywords: HashMap<String, serde_json::Value>,
393}
394
395/// A request to list the available tools on a server.
396#[derive(Debug, Clone, Serialize, Deserialize, Default)]
397pub struct ListToolsRequest {
398    /// An optional cursor for pagination. If provided, the server should return
399    /// the next page of results starting after this cursor.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub cursor: Option<Cursor>,
402    /// Optional metadata for the request.
403    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
404    pub _meta: Option<serde_json::Value>,
405}
406
407/// The result of a `ListToolsRequest`.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ListToolsResult {
410    /// The list of available tools for the current page.
411    pub tools: Vec<Tool>,
412    /// An optional continuation token for retrieving the next page of results.
413    /// If `None`, there are no more results.
414    #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
415    pub next_cursor: Option<Cursor>,
416    /// Optional metadata for the result.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub _meta: Option<serde_json::Value>,
419}
420
421/// A request to execute a specific tool.
422///
423/// ## Version Support
424/// - MCP 2025-11-25: name, arguments, _meta
425/// - MCP 2025-11-25 draft (SEP-1686): + task (optional task augmentation)
426///
427/// ## Task Augmentation
428///
429/// When the `task` field is present, the receiver responds immediately with
430/// a `CreateTaskResult` containing a task ID. The actual tool result is available
431/// later via `tasks/result`.
432///
433/// ```rust,ignore
434/// use turbomcp_protocol::types::{CallToolRequest, tasks::TaskMetadata};
435///
436/// let request = CallToolRequest {
437///     name: "long_running_tool".to_string(),
438///     arguments: Some(json!({"data": "value"})),
439///     task: Some(TaskMetadata { ttl: Some(300_000) }), // 5 minute lifetime
440///     _meta: None,
441/// };
442/// ```
443#[derive(Debug, Clone, Serialize, Deserialize, Default)]
444pub struct CallToolRequest {
445    /// The programmatic name of the tool to call.
446    pub name: String,
447
448    /// The arguments to pass to the tool, conforming to its `input_schema`.
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub arguments: Option<HashMap<String, serde_json::Value>>,
451
452    /// Optional task metadata for task-augmented requests (MCP 2025-11-25 draft)
453    ///
454    /// When present, this request will be executed asynchronously and the receiver
455    /// will respond immediately with a `CreateTaskResult`. The actual tool result
456    /// is available later via `tasks/result`.
457    ///
458    /// Requires:
459    /// - Server capability: `tasks.requests.tools.call`
460    /// - Tool annotation: `taskHint` must be "optional" or "always" (or absent/"never" for default)
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub task: Option<crate::types::tasks::TaskMetadata>,
463
464    /// Optional metadata for the request.
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub _meta: Option<serde_json::Value>,
467}
468
469/// The result of a `CallToolRequest`.
470#[derive(Debug, Clone, Serialize, Deserialize, Default)]
471pub struct CallToolResult {
472    /// The output of the tool, typically as a series of text or other content blocks. This is required.
473    pub content: Vec<ContentBlock>,
474    /// An optional boolean indicating whether the tool execution resulted in an error.
475    ///
476    /// When `is_error` is `true`, all content blocks should be treated as error information.
477    /// The error message may span multiple text blocks for structured error reporting.
478    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
479    pub is_error: Option<bool>,
480    /// Optional structured output from the tool, conforming to its `output_schema`.
481    ///
482    /// When present, this contains schema-validated JSON output that clients can parse
483    /// and use programmatically. Tools that return structured content SHOULD also include
484    /// the serialized JSON in a TextContent block for backward compatibility with clients
485    /// that don't support structured output.
486    ///
487    /// See [`Tool::output_schema`] for defining the expected structure.
488    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
489    pub structured_content: Option<serde_json::Value>,
490    /// Optional metadata for the result.
491    ///
492    /// This field is for client applications and tools to pass additional context that
493    /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
494    /// cache status, or internal state information.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub _meta: Option<serde_json::Value>,
497    /// Optional task ID when tool execution is augmented with task tracking (MCP 2025-11-25 draft - SEP-1686).
498    ///
499    /// When a tool call includes task metadata, the server creates a task to track the operation
500    /// and returns the task_id here. Clients can use this to monitor progress via tasks/get
501    /// or retrieve final results via tasks/result.
502    #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
503    pub task_id: Option<String>,
504}
505
506impl CallToolResult {
507    /// Extracts and concatenates all text content from the result.
508    ///
509    /// This is useful for simple text-only tools or when you want to present
510    /// all textual output as a single string.
511    ///
512    /// # Returns
513    ///
514    /// A single string containing all text blocks concatenated with newlines.
515    /// Returns an empty string if there are no text blocks.
516    ///
517    /// # Example
518    ///
519    /// ```rust
520    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
521    ///
522    /// let result = CallToolResult {
523    ///     content: vec![
524    ///         ContentBlock::Text(TextContent {
525    ///             text: "Line 1".to_string(),
526    ///             annotations: None,
527    ///             meta: None,
528    ///         }),
529    ///         ContentBlock::Text(TextContent {
530    ///             text: "Line 2".to_string(),
531    ///             annotations: None,
532    ///             meta: None,
533    ///         }),
534    ///     ],
535    ///     is_error: None,
536    ///     structured_content: None,
537    ///     _meta: None,
538    ///     task_id: None,
539    /// };
540    ///
541    /// assert_eq!(result.all_text(), "Line 1\nLine 2");
542    /// ```
543    pub fn all_text(&self) -> String {
544        self.content
545            .iter()
546            .filter_map(|block| match block {
547                ContentBlock::Text(text) => Some(text.text.as_str()),
548                _ => None,
549            })
550            .collect::<Vec<_>>()
551            .join("\n")
552    }
553
554    /// Returns the text content of the first text block, if any.
555    ///
556    /// This is a common pattern for simple tools that return a single text response.
557    ///
558    /// # Returns
559    ///
560    /// `Some(&str)` if the first content block is text, `None` otherwise.
561    ///
562    /// # Example
563    ///
564    /// ```rust
565    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
566    ///
567    /// let result = CallToolResult {
568    ///     content: vec![
569    ///         ContentBlock::Text(TextContent {
570    ///             text: "Hello, world!".to_string(),
571    ///             annotations: None,
572    ///             meta: None,
573    ///         }),
574    ///     ],
575    ///     is_error: None,
576    ///     structured_content: None,
577    ///     _meta: None,
578    ///     task_id: None,
579    /// };
580    ///
581    /// assert_eq!(result.first_text(), Some("Hello, world!"));
582    /// ```
583    pub fn first_text(&self) -> Option<&str> {
584        self.content.first().and_then(|block| match block {
585            ContentBlock::Text(text) => Some(text.text.as_str()),
586            _ => None,
587        })
588    }
589
590    /// Checks if the tool execution resulted in an error.
591    ///
592    /// # Returns
593    ///
594    /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
595    /// (including when `is_error` is `None`).
596    ///
597    /// # Example
598    ///
599    /// ```rust
600    /// use turbomcp_protocol::types::CallToolResult;
601    ///
602    /// let success_result = CallToolResult {
603    ///     content: vec![],
604    ///     is_error: Some(false),
605    ///     structured_content: None,
606    ///     _meta: None,
607    ///     task_id: None,
608    /// };
609    /// assert!(!success_result.has_error());
610    ///
611    /// let error_result = CallToolResult {
612    ///     content: vec![],
613    ///     is_error: Some(true),
614    ///     structured_content: None,
615    ///     _meta: None,
616    ///     task_id: None,
617    /// };
618    /// assert!(error_result.has_error());
619    ///
620    /// let unspecified_result = CallToolResult {
621    ///     content: vec![],
622    ///     is_error: None,
623    ///     structured_content: None,
624    ///     _meta: None,
625    ///     task_id: None,
626    /// };
627    /// assert!(!unspecified_result.has_error());
628    /// ```
629    pub fn has_error(&self) -> bool {
630        self.is_error.unwrap_or(false)
631    }
632
633    /// Creates a user-friendly display string for the tool result.
634    ///
635    /// This method provides a formatted representation suitable for logging,
636    /// debugging, or displaying to end users. It handles multiple content types
637    /// and includes structured content and error information when present.
638    ///
639    /// # Returns
640    ///
641    /// A formatted string representing the tool result.
642    ///
643    /// # Example
644    ///
645    /// ```rust
646    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
647    ///
648    /// let result = CallToolResult {
649    ///     content: vec![
650    ///         ContentBlock::Text(TextContent {
651    ///             text: "Operation completed".to_string(),
652    ///             annotations: None,
653    ///             meta: None,
654    ///         }),
655    ///     ],
656    ///     is_error: Some(false),
657    ///     structured_content: None,
658    ///     _meta: None,
659    ///     task_id: None,
660    /// };
661    ///
662    /// let display = result.to_display_string();
663    /// assert!(display.contains("Operation completed"));
664    /// ```
665    pub fn to_display_string(&self) -> String {
666        let mut parts = Vec::new();
667
668        // Add error indicator if present
669        if self.has_error() {
670            parts.push("ERROR:".to_string());
671        }
672
673        // Process content blocks
674        for (i, block) in self.content.iter().enumerate() {
675            match block {
676                ContentBlock::Text(text) => {
677                    parts.push(text.text.clone());
678                }
679                ContentBlock::Image(img) => {
680                    parts.push(format!(
681                        "[Image: {} bytes, type: {}]",
682                        img.data.len(),
683                        img.mime_type
684                    ));
685                }
686                ContentBlock::Audio(audio) => {
687                    parts.push(format!(
688                        "[Audio: {} bytes, type: {}]",
689                        audio.data.len(),
690                        audio.mime_type
691                    ));
692                }
693                ContentBlock::ResourceLink(link) => {
694                    let desc = link.description.as_deref().unwrap_or("");
695                    let mime = link
696                        .mime_type
697                        .as_deref()
698                        .map(|m| format!(" [{}]", m))
699                        .unwrap_or_default();
700                    parts.push(format!(
701                        "[Resource: {}{}{}{}]",
702                        link.name,
703                        mime,
704                        if !desc.is_empty() { ": " } else { "" },
705                        desc
706                    ));
707                }
708                ContentBlock::Resource(_resource) => {
709                    parts.push(format!("[Embedded Resource #{}]", i + 1));
710                }
711                ContentBlock::ToolUse(tool_use) => {
712                    parts.push(format!(
713                        "[Tool Use: {} (id: {})]",
714                        tool_use.name, tool_use.id
715                    ));
716                }
717                ContentBlock::ToolResult(tool_result) => {
718                    parts.push(format!(
719                        "[Tool Result for: {}{}]",
720                        tool_result.tool_use_id,
721                        if tool_result.is_error.unwrap_or(false) {
722                            " (ERROR)"
723                        } else {
724                            ""
725                        }
726                    ));
727                }
728            }
729        }
730
731        // Add structured content indicator if present
732        if self.structured_content.is_some() {
733            parts.push("[Includes structured output]".to_string());
734        }
735
736        parts.join("\n")
737    }
738}
739
740// =============================================================================
741// Conversions from turbomcp-core types (for unified handler support)
742// =============================================================================
743//
744// These conversions enable the unified IntoToolResponse pattern, allowing
745// handlers to return core types that are automatically converted to protocol types.
746//
747// IMPORTANT NOTES:
748// - HashMap conversion: O(n) overhead due to hashbrown→std HashMap conversion
749// - Lossy conversion: Protocol's `structured_content` and `task_id` fields are
750//   NOT present in core types, so round-trip (protocol→core→protocol) loses them
751// - Resource fallback: Empty text is used if ResourceContent has neither text nor blob
752
753/// Convert core Annotations to protocol Annotations.
754///
755/// Note: Incurs O(n) conversion overhead from `hashbrown::HashMap` to `std::collections::HashMap`.
756impl From<turbomcp_core::types::core::Annotations> for super::Annotations {
757    fn from(core_ann: turbomcp_core::types::core::Annotations) -> Self {
758        // Convert hashbrown::HashMap to std::collections::HashMap
759        // Both implementations guarantee unique keys, so no data loss occurs
760        let custom: std::collections::HashMap<String, serde_json::Value> =
761            core_ann.custom.into_iter().collect();
762        Self {
763            audience: core_ann.audience,
764            priority: core_ann.priority,
765            last_modified: core_ann.last_modified,
766            custom,
767        }
768    }
769}
770
771/// Convert core Content to protocol ContentBlock
772impl From<turbomcp_core::types::content::Content> for super::ContentBlock {
773    fn from(content: turbomcp_core::types::content::Content) -> Self {
774        use turbomcp_core::types::content::Content as CoreContent;
775        match content {
776            CoreContent::Text { text, annotations } => {
777                super::ContentBlock::Text(super::TextContent {
778                    text,
779                    annotations: annotations.map(Into::into),
780                    meta: None,
781                })
782            }
783            CoreContent::Image {
784                data,
785                mime_type,
786                annotations,
787            } => super::ContentBlock::Image(super::ImageContent {
788                data: data.into(),
789                mime_type: mime_type.to_string().into(),
790                annotations: annotations.map(Into::into),
791                meta: None,
792            }),
793            CoreContent::Audio {
794                data,
795                mime_type,
796                annotations,
797            } => super::ContentBlock::Audio(super::AudioContent {
798                data: data.into(),
799                mime_type: mime_type.to_string().into(),
800                annotations: annotations.map(Into::into),
801                meta: None,
802            }),
803            CoreContent::Resource {
804                resource,
805                annotations,
806            } => {
807                // Convert core ResourceContent to protocol EmbeddedResource
808                // Core uses a flat struct with optional text/blob, protocol uses an enum
809                let protocol_resource = if let Some(text) = resource.text {
810                    super::ResourceContent::Text(super::TextResourceContents {
811                        uri: resource.uri.to_string().into(),
812                        mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
813                        text,
814                        meta: None,
815                    })
816                } else if let Some(blob) = resource.blob {
817                    super::ResourceContent::Blob(super::BlobResourceContents {
818                        uri: resource.uri.to_string().into(),
819                        mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
820                        blob: blob.into(),
821                        meta: None,
822                    })
823                } else {
824                    // Default to empty text if neither is set.
825                    // NOTE: This is a fallback for malformed core resources - callers should
826                    // ensure ResourceContent has either text or blob set.
827                    #[cfg(feature = "std")]
828                    eprintln!(
829                        "[turbomcp-protocol] WARNING: Resource '{}' has neither text nor blob content",
830                        resource.uri
831                    );
832                    super::ResourceContent::Text(super::TextResourceContents {
833                        uri: resource.uri.to_string().into(),
834                        mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
835                        text: String::new(),
836                        meta: None,
837                    })
838                };
839                super::ContentBlock::Resource(super::EmbeddedResource {
840                    resource: protocol_resource,
841                    annotations: annotations.map(Into::into),
842                    meta: None,
843                })
844            }
845        }
846    }
847}
848
849/// Convert core CallToolResult to protocol CallToolResult
850///
851/// This enables the unified IntoToolResponse pattern for native handlers.
852///
853/// **Note**: `structured_content` and `task_id` fields are set to `None` since
854/// core types don't have these fields. Round-trip conversion (protocol→core→protocol)
855/// will lose these values.
856impl From<turbomcp_core::types::tools::CallToolResult> for CallToolResult {
857    fn from(core_result: turbomcp_core::types::tools::CallToolResult) -> Self {
858        Self {
859            content: core_result.content.into_iter().map(Into::into).collect(),
860            is_error: core_result.is_error,
861            structured_content: None,
862            _meta: core_result._meta,
863            task_id: None,
864        }
865    }
866}
867
868#[cfg(test)]
869mod conversion_tests {
870    use super::*;
871    use turbomcp_core::types::content::Content as CoreContent;
872    use turbomcp_core::types::tools::CallToolResult as CoreCallToolResult;
873
874    #[test]
875    fn test_core_content_to_protocol_text() {
876        let core = CoreContent::text("hello world");
877        let protocol: ContentBlock = core.into();
878
879        match protocol {
880            ContentBlock::Text(text) => {
881                assert_eq!(text.text, "hello world");
882                assert!(text.annotations.is_none());
883            }
884            _ => panic!("Expected Text variant"),
885        }
886    }
887
888    #[test]
889    fn test_core_content_to_protocol_image() {
890        let core = CoreContent::image("base64data", "image/png");
891        let protocol: ContentBlock = core.into();
892
893        match protocol {
894            ContentBlock::Image(img) => {
895                assert_eq!(img.data, "base64data");
896                assert_eq!(img.mime_type, "image/png");
897            }
898            _ => panic!("Expected Image variant"),
899        }
900    }
901
902    #[test]
903    fn test_core_call_tool_result_to_protocol() {
904        let core = CoreCallToolResult::text("success");
905        let protocol: CallToolResult = core.into();
906
907        assert_eq!(protocol.content.len(), 1);
908        assert!(protocol.is_error.is_none());
909        assert!(protocol.structured_content.is_none());
910        assert!(protocol.task_id.is_none());
911
912        match &protocol.content[0] {
913            ContentBlock::Text(text) => assert_eq!(text.text, "success"),
914            _ => panic!("Expected Text content"),
915        }
916    }
917
918    #[test]
919    fn test_core_call_tool_result_error_preserved() {
920        let core = CoreCallToolResult::error("something failed");
921        let protocol: CallToolResult = core.into();
922
923        assert_eq!(protocol.is_error, Some(true));
924        match &protocol.content[0] {
925            ContentBlock::Text(text) => assert_eq!(text.text, "something failed"),
926            _ => panic!("Expected Text content"),
927        }
928    }
929
930    #[test]
931    fn test_annotations_conversion() {
932        use crate::types::Annotations;
933        use turbomcp_core::types::core::Annotations as CoreAnnotations;
934
935        // Create core annotations (custom field uses Default for simplicity)
936        let core = CoreAnnotations {
937            audience: Some(vec!["user".to_string(), "assistant".to_string()]),
938            priority: Some(0.75),
939            last_modified: Some("2025-01-13T12:00:00Z".to_string()),
940            custom: Default::default(),
941        };
942
943        let protocol: Annotations = core.into();
944
945        // Verify all fields are correctly converted
946        assert_eq!(
947            protocol.audience,
948            Some(vec!["user".to_string(), "assistant".to_string()])
949        );
950        assert_eq!(protocol.priority, Some(0.75));
951        assert_eq!(
952            protocol.last_modified,
953            Some("2025-01-13T12:00:00Z".to_string())
954        );
955        assert!(protocol.custom.is_empty());
956    }
957}