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    #[cfg(feature = "mcp-icons")]
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub icons: Option<Vec<super::core::Icon>>,
170
171    /// A general-purpose metadata field for custom data.
172    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
173    pub meta: Option<HashMap<String, serde_json::Value>>,
174}
175
176impl Default for Tool {
177    fn default() -> Self {
178        Self {
179            name: "unnamed_tool".to_string(), // Must have a valid name for MCP compliance
180            title: None,
181            description: None,
182            input_schema: ToolInputSchema::default(),
183            output_schema: None,
184            execution: None,
185            annotations: None,
186            #[cfg(feature = "mcp-icons")]
187            icons: None,
188            meta: None,
189        }
190    }
191}
192
193impl Tool {
194    /// Creates a new `Tool` with a given name.
195    ///
196    /// # Panics
197    /// Panics if the name is empty or contains only whitespace.
198    pub fn new(name: impl Into<String>) -> Self {
199        let name = name.into();
200        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
201        Self {
202            name,
203            title: None,
204            description: None,
205            input_schema: ToolInputSchema::default(),
206            output_schema: None,
207            execution: None,
208            annotations: None,
209            #[cfg(feature = "mcp-icons")]
210            icons: None,
211            meta: None,
212        }
213    }
214
215    /// Creates a new `Tool` with a name and a description.
216    ///
217    /// # Panics
218    /// Panics if the name is empty or contains only whitespace.
219    pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
220        let name = name.into();
221        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
222        Self {
223            name,
224            title: None,
225            description: Some(description.into()),
226            input_schema: ToolInputSchema::default(),
227            output_schema: None,
228            execution: None,
229            annotations: None,
230            #[cfg(feature = "mcp-icons")]
231            icons: None,
232            meta: None,
233        }
234    }
235
236    /// Sets the execution properties for this tool.
237    pub fn with_execution(mut self, execution: ToolExecution) -> Self {
238        self.execution = Some(execution);
239        self
240    }
241
242    /// Sets the input schema for this tool.
243    ///
244    /// # Example
245    /// ```
246    /// # use turbomcp_protocol::types::{Tool, ToolInputSchema};
247    /// let schema = ToolInputSchema::empty();
248    /// let tool = Tool::new("my_tool").with_input_schema(schema);
249    /// ```
250    pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
251        self.input_schema = schema;
252        self
253    }
254
255    /// Sets the output schema for this tool.
256    pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
257        self.output_schema = Some(schema);
258        self
259    }
260
261    /// Sets the user-friendly title for this tool.
262    pub fn with_title(mut self, title: impl Into<String>) -> Self {
263        self.title = Some(title.into());
264        self
265    }
266
267    /// Sets the annotations for this tool.
268    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
269        self.annotations = Some(annotations);
270        self
271    }
272}
273
274/// Defines the structure of the arguments a tool accepts, as a JSON Schema object.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ToolInputSchema {
277    /// The type of the schema, which must be "object" for tool inputs.
278    #[serde(rename = "type")]
279    pub schema_type: String,
280    /// A map defining the properties (parameters) the tool accepts.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub properties: Option<HashMap<String, serde_json::Value>>,
283    /// A list of property names that are required.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub required: Option<Vec<String>>,
286    /// Whether additional, unspecified properties are allowed.
287    #[serde(
288        rename = "additionalProperties",
289        skip_serializing_if = "Option::is_none"
290    )]
291    pub additional_properties: Option<bool>,
292}
293
294impl Default for ToolInputSchema {
295    /// Creates a default `ToolInputSchema` that accepts an empty object.
296    fn default() -> Self {
297        Self {
298            schema_type: "object".to_string(),
299            properties: None,
300            required: None,
301            additional_properties: None,
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: "object".to_string(),
316            properties: Some(properties),
317            required: None,
318            additional_properties: None,
319        }
320    }
321
322    /// Creates a new schema with a given set of properties and a list of required properties.
323    pub fn with_required_properties(
324        properties: HashMap<String, serde_json::Value>,
325        required: Vec<String>,
326    ) -> Self {
327        Self {
328            schema_type: "object".to_string(),
329            properties: Some(properties),
330            required: Some(required),
331            additional_properties: Some(false),
332        }
333    }
334
335    /// Adds a property to the schema using a builder pattern.
336    ///
337    /// # Example
338    /// ```
339    /// # use turbomcp_protocol::types::ToolInputSchema;
340    /// # use serde_json::json;
341    /// let schema = ToolInputSchema::empty()
342    ///     .add_property("name".to_string(), json!({ "type": "string" }));
343    /// ```
344    pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
345        self.properties
346            .get_or_insert_with(HashMap::new)
347            .insert(name, property);
348        self
349    }
350
351    /// Marks a property as required using a builder pattern.
352    ///
353    /// # Example
354    /// ```
355    /// # use turbomcp_protocol::types::ToolInputSchema;
356    /// # use serde_json::json;
357    /// let schema = ToolInputSchema::empty()
358    ///     .add_property("name".to_string(), json!({ "type": "string" }))
359    ///     .require_property("name".to_string());
360    /// ```
361    pub fn require_property(mut self, name: String) -> Self {
362        let required = self.required.get_or_insert_with(Vec::new);
363        if !required.contains(&name) {
364            required.push(name);
365        }
366        self
367    }
368}
369
370/// Defines the structure of a tool's successful output, as a JSON Schema object.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ToolOutputSchema {
373    /// The type of the schema, which must be "object" for tool outputs.
374    #[serde(rename = "type")]
375    pub schema_type: String,
376    /// A map defining the properties of the output object.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub properties: Option<HashMap<String, serde_json::Value>>,
379    /// A list of property names in the output that are required.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub required: Option<Vec<String>>,
382    /// Whether additional, unspecified properties are allowed in the output.
383    #[serde(
384        rename = "additionalProperties",
385        skip_serializing_if = "Option::is_none"
386    )]
387    pub additional_properties: Option<bool>,
388}
389
390/// A request to list the available tools on a server.
391#[derive(Debug, Clone, Serialize, Deserialize, Default)]
392pub struct ListToolsRequest {
393    /// An optional cursor for pagination. If provided, the server should return
394    /// the next page of results starting after this cursor.
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub cursor: Option<Cursor>,
397    /// Optional metadata for the request.
398    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
399    pub _meta: Option<serde_json::Value>,
400}
401
402/// The result of a `ListToolsRequest`.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct ListToolsResult {
405    /// The list of available tools for the current page.
406    pub tools: Vec<Tool>,
407    /// An optional continuation token for retrieving the next page of results.
408    /// If `None`, there are no more results.
409    #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
410    pub next_cursor: Option<Cursor>,
411    /// Optional metadata for the result.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub _meta: Option<serde_json::Value>,
414}
415
416/// A request to execute a specific tool.
417///
418/// ## Version Support
419/// - MCP 2025-06-18: name, arguments, _meta
420/// - MCP 2025-11-25 draft (SEP-1686): + task (optional task augmentation)
421///
422/// ## Task Augmentation
423///
424/// When the `task` field is present, the receiver responds immediately with
425/// a `CreateTaskResult` containing a task ID. The actual tool result is available
426/// later via `tasks/result`.
427///
428/// ```rust,ignore
429/// use turbomcp_protocol::types::{CallToolRequest, tasks::TaskMetadata};
430///
431/// let request = CallToolRequest {
432///     name: "long_running_tool".to_string(),
433///     arguments: Some(json!({"data": "value"})),
434///     task: Some(TaskMetadata { ttl: Some(300_000) }), // 5 minute lifetime
435///     _meta: None,
436/// };
437/// ```
438#[derive(Debug, Clone, Serialize, Deserialize, Default)]
439pub struct CallToolRequest {
440    /// The programmatic name of the tool to call.
441    pub name: String,
442
443    /// The arguments to pass to the tool, conforming to its `input_schema`.
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub arguments: Option<HashMap<String, serde_json::Value>>,
446
447    /// Optional task metadata for task-augmented requests (MCP 2025-11-25 draft)
448    ///
449    /// When present, this request will be executed asynchronously and the receiver
450    /// will respond immediately with a `CreateTaskResult`. The actual tool result
451    /// is available later via `tasks/result`.
452    ///
453    /// Requires:
454    /// - Server capability: `tasks.requests.tools.call`
455    /// - Tool annotation: `taskHint` must be "optional" or "always" (or absent/"never" for default)
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub task: Option<crate::types::tasks::TaskMetadata>,
458
459    /// Optional metadata for the request.
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub _meta: Option<serde_json::Value>,
462}
463
464/// The result of a `CallToolRequest`.
465#[derive(Debug, Clone, Serialize, Deserialize, Default)]
466pub struct CallToolResult {
467    /// The output of the tool, typically as a series of text or other content blocks. This is required.
468    pub content: Vec<ContentBlock>,
469    /// An optional boolean indicating whether the tool execution resulted in an error.
470    ///
471    /// When `is_error` is `true`, all content blocks should be treated as error information.
472    /// The error message may span multiple text blocks for structured error reporting.
473    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
474    pub is_error: Option<bool>,
475    /// Optional structured output from the tool, conforming to its `output_schema`.
476    ///
477    /// When present, this contains schema-validated JSON output that clients can parse
478    /// and use programmatically. Tools that return structured content SHOULD also include
479    /// the serialized JSON in a TextContent block for backward compatibility with clients
480    /// that don't support structured output.
481    ///
482    /// See [`Tool::output_schema`] for defining the expected structure.
483    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
484    pub structured_content: Option<serde_json::Value>,
485    /// Optional metadata for the result.
486    ///
487    /// This field is for client applications and tools to pass additional context that
488    /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
489    /// cache status, or internal state information.
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub _meta: Option<serde_json::Value>,
492    /// Optional task ID when tool execution is augmented with task tracking (MCP 2025-11-25 draft - SEP-1686).
493    ///
494    /// When a tool call includes task metadata, the server creates a task to track the operation
495    /// and returns the task_id here. Clients can use this to monitor progress via tasks/get
496    /// or retrieve final results via tasks/result.
497    #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
498    pub task_id: Option<String>,
499}
500
501impl CallToolResult {
502    /// Extracts and concatenates all text content from the result.
503    ///
504    /// This is useful for simple text-only tools or when you want to present
505    /// all textual output as a single string.
506    ///
507    /// # Returns
508    ///
509    /// A single string containing all text blocks concatenated with newlines.
510    /// Returns an empty string if there are no text blocks.
511    ///
512    /// # Example
513    ///
514    /// ```rust
515    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
516    ///
517    /// let result = CallToolResult {
518    ///     content: vec![
519    ///         ContentBlock::Text(TextContent {
520    ///             text: "Line 1".to_string(),
521    ///             annotations: None,
522    ///             meta: None,
523    ///         }),
524    ///         ContentBlock::Text(TextContent {
525    ///             text: "Line 2".to_string(),
526    ///             annotations: None,
527    ///             meta: None,
528    ///         }),
529    ///     ],
530    ///     is_error: None,
531    ///     structured_content: None,
532    ///     _meta: None,
533    ///     task_id: None,
534    /// };
535    ///
536    /// assert_eq!(result.all_text(), "Line 1\nLine 2");
537    /// ```
538    pub fn all_text(&self) -> String {
539        self.content
540            .iter()
541            .filter_map(|block| match block {
542                ContentBlock::Text(text) => Some(text.text.as_str()),
543                _ => None,
544            })
545            .collect::<Vec<_>>()
546            .join("\n")
547    }
548
549    /// Returns the text content of the first text block, if any.
550    ///
551    /// This is a common pattern for simple tools that return a single text response.
552    ///
553    /// # Returns
554    ///
555    /// `Some(&str)` if the first content block is text, `None` otherwise.
556    ///
557    /// # Example
558    ///
559    /// ```rust
560    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
561    ///
562    /// let result = CallToolResult {
563    ///     content: vec![
564    ///         ContentBlock::Text(TextContent {
565    ///             text: "Hello, world!".to_string(),
566    ///             annotations: None,
567    ///             meta: None,
568    ///         }),
569    ///     ],
570    ///     is_error: None,
571    ///     structured_content: None,
572    ///     _meta: None,
573    ///     task_id: None,
574    /// };
575    ///
576    /// assert_eq!(result.first_text(), Some("Hello, world!"));
577    /// ```
578    pub fn first_text(&self) -> Option<&str> {
579        self.content.first().and_then(|block| match block {
580            ContentBlock::Text(text) => Some(text.text.as_str()),
581            _ => None,
582        })
583    }
584
585    /// Checks if the tool execution resulted in an error.
586    ///
587    /// # Returns
588    ///
589    /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
590    /// (including when `is_error` is `None`).
591    ///
592    /// # Example
593    ///
594    /// ```rust
595    /// use turbomcp_protocol::types::CallToolResult;
596    ///
597    /// let success_result = CallToolResult {
598    ///     content: vec![],
599    ///     is_error: Some(false),
600    ///     structured_content: None,
601    ///     _meta: None,
602    ///     task_id: None,
603    /// };
604    /// assert!(!success_result.has_error());
605    ///
606    /// let error_result = CallToolResult {
607    ///     content: vec![],
608    ///     is_error: Some(true),
609    ///     structured_content: None,
610    ///     _meta: None,
611    ///     task_id: None,
612    /// };
613    /// assert!(error_result.has_error());
614    ///
615    /// let unspecified_result = CallToolResult {
616    ///     content: vec![],
617    ///     is_error: None,
618    ///     structured_content: None,
619    ///     _meta: None,
620    ///     task_id: None,
621    /// };
622    /// assert!(!unspecified_result.has_error());
623    /// ```
624    pub fn has_error(&self) -> bool {
625        self.is_error.unwrap_or(false)
626    }
627
628    /// Creates a user-friendly display string for the tool result.
629    ///
630    /// This method provides a formatted representation suitable for logging,
631    /// debugging, or displaying to end users. It handles multiple content types
632    /// and includes structured content and error information when present.
633    ///
634    /// # Returns
635    ///
636    /// A formatted string representing the tool result.
637    ///
638    /// # Example
639    ///
640    /// ```rust
641    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
642    ///
643    /// let result = CallToolResult {
644    ///     content: vec![
645    ///         ContentBlock::Text(TextContent {
646    ///             text: "Operation completed".to_string(),
647    ///             annotations: None,
648    ///             meta: None,
649    ///         }),
650    ///     ],
651    ///     is_error: Some(false),
652    ///     structured_content: None,
653    ///     _meta: None,
654    ///     task_id: None,
655    /// };
656    ///
657    /// let display = result.to_display_string();
658    /// assert!(display.contains("Operation completed"));
659    /// ```
660    pub fn to_display_string(&self) -> String {
661        let mut parts = Vec::new();
662
663        // Add error indicator if present
664        if self.has_error() {
665            parts.push("ERROR:".to_string());
666        }
667
668        // Process content blocks
669        for (i, block) in self.content.iter().enumerate() {
670            match block {
671                ContentBlock::Text(text) => {
672                    parts.push(text.text.clone());
673                }
674                ContentBlock::Image(img) => {
675                    parts.push(format!(
676                        "[Image: {} bytes, type: {}]",
677                        img.data.len(),
678                        img.mime_type
679                    ));
680                }
681                ContentBlock::Audio(audio) => {
682                    parts.push(format!(
683                        "[Audio: {} bytes, type: {}]",
684                        audio.data.len(),
685                        audio.mime_type
686                    ));
687                }
688                ContentBlock::ResourceLink(link) => {
689                    let desc = link.description.as_deref().unwrap_or("");
690                    let mime = link
691                        .mime_type
692                        .as_deref()
693                        .map(|m| format!(" [{}]", m))
694                        .unwrap_or_default();
695                    parts.push(format!(
696                        "[Resource: {}{}{}{}]",
697                        link.name,
698                        mime,
699                        if !desc.is_empty() { ": " } else { "" },
700                        desc
701                    ));
702                }
703                ContentBlock::Resource(_resource) => {
704                    parts.push(format!("[Embedded Resource #{}]", i + 1));
705                }
706                #[cfg(feature = "mcp-sampling-tools")]
707                ContentBlock::ToolUse(tool_use) => {
708                    parts.push(format!(
709                        "[Tool Use: {} (id: {})]",
710                        tool_use.name, tool_use.id
711                    ));
712                }
713                #[cfg(feature = "mcp-sampling-tools")]
714                ContentBlock::ToolResult(tool_result) => {
715                    parts.push(format!(
716                        "[Tool Result for: {}{}]",
717                        tool_result.tool_use_id,
718                        if tool_result.is_error.unwrap_or(false) {
719                            " (ERROR)"
720                        } else {
721                            ""
722                        }
723                    ));
724                }
725            }
726        }
727
728        // Add structured content indicator if present
729        if self.structured_content.is_some() {
730            parts.push("[Includes structured output]".to_string());
731        }
732
733        parts.join("\n")
734    }
735}