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    /// };
534    ///
535    /// assert_eq!(result.all_text(), "Line 1\nLine 2");
536    /// ```
537    pub fn all_text(&self) -> String {
538        self.content
539            .iter()
540            .filter_map(|block| match block {
541                ContentBlock::Text(text) => Some(text.text.as_str()),
542                _ => None,
543            })
544            .collect::<Vec<_>>()
545            .join("\n")
546    }
547
548    /// Returns the text content of the first text block, if any.
549    ///
550    /// This is a common pattern for simple tools that return a single text response.
551    ///
552    /// # Returns
553    ///
554    /// `Some(&str)` if the first content block is text, `None` otherwise.
555    ///
556    /// # Example
557    ///
558    /// ```rust
559    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
560    ///
561    /// let result = CallToolResult {
562    ///     content: vec![
563    ///         ContentBlock::Text(TextContent {
564    ///             text: "Hello, world!".to_string(),
565    ///             annotations: None,
566    ///             meta: None,
567    ///         }),
568    ///     ],
569    ///     is_error: None,
570    ///     structured_content: None,
571    ///     _meta: None,
572    /// };
573    ///
574    /// assert_eq!(result.first_text(), Some("Hello, world!"));
575    /// ```
576    pub fn first_text(&self) -> Option<&str> {
577        self.content.first().and_then(|block| match block {
578            ContentBlock::Text(text) => Some(text.text.as_str()),
579            _ => None,
580        })
581    }
582
583    /// Checks if the tool execution resulted in an error.
584    ///
585    /// # Returns
586    ///
587    /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
588    /// (including when `is_error` is `None`).
589    ///
590    /// # Example
591    ///
592    /// ```rust
593    /// use turbomcp_protocol::types::CallToolResult;
594    ///
595    /// let success_result = CallToolResult {
596    ///     content: vec![],
597    ///     is_error: Some(false),
598    ///     structured_content: None,
599    ///     _meta: None,
600    /// };
601    /// assert!(!success_result.has_error());
602    ///
603    /// let error_result = CallToolResult {
604    ///     content: vec![],
605    ///     is_error: Some(true),
606    ///     structured_content: None,
607    ///     _meta: 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    /// };
617    /// assert!(!unspecified_result.has_error());
618    /// ```
619    pub fn has_error(&self) -> bool {
620        self.is_error.unwrap_or(false)
621    }
622
623    /// Creates a user-friendly display string for the tool result.
624    ///
625    /// This method provides a formatted representation suitable for logging,
626    /// debugging, or displaying to end users. It handles multiple content types
627    /// and includes structured content and error information when present.
628    ///
629    /// # Returns
630    ///
631    /// A formatted string representing the tool result.
632    ///
633    /// # Example
634    ///
635    /// ```rust
636    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
637    ///
638    /// let result = CallToolResult {
639    ///     content: vec![
640    ///         ContentBlock::Text(TextContent {
641    ///             text: "Operation completed".to_string(),
642    ///             annotations: None,
643    ///             meta: None,
644    ///         }),
645    ///     ],
646    ///     is_error: Some(false),
647    ///     structured_content: None,
648    ///     _meta: None,
649    /// };
650    ///
651    /// let display = result.to_display_string();
652    /// assert!(display.contains("Operation completed"));
653    /// ```
654    pub fn to_display_string(&self) -> String {
655        let mut parts = Vec::new();
656
657        // Add error indicator if present
658        if self.has_error() {
659            parts.push("ERROR:".to_string());
660        }
661
662        // Process content blocks
663        for (i, block) in self.content.iter().enumerate() {
664            match block {
665                ContentBlock::Text(text) => {
666                    parts.push(text.text.clone());
667                }
668                ContentBlock::Image(img) => {
669                    parts.push(format!(
670                        "[Image: {} bytes, type: {}]",
671                        img.data.len(),
672                        img.mime_type
673                    ));
674                }
675                ContentBlock::Audio(audio) => {
676                    parts.push(format!(
677                        "[Audio: {} bytes, type: {}]",
678                        audio.data.len(),
679                        audio.mime_type
680                    ));
681                }
682                ContentBlock::ResourceLink(link) => {
683                    let desc = link.description.as_deref().unwrap_or("");
684                    let mime = link
685                        .mime_type
686                        .as_deref()
687                        .map(|m| format!(" [{}]", m))
688                        .unwrap_or_default();
689                    parts.push(format!(
690                        "[Resource: {}{}{}{}]",
691                        link.name,
692                        mime,
693                        if !desc.is_empty() { ": " } else { "" },
694                        desc
695                    ));
696                }
697                ContentBlock::Resource(_resource) => {
698                    parts.push(format!("[Embedded Resource #{}]", i + 1));
699                }
700                #[cfg(feature = "mcp-sampling-tools")]
701                ContentBlock::ToolUse(tool_use) => {
702                    parts.push(format!(
703                        "[Tool Use: {} (id: {})]",
704                        tool_use.name, tool_use.id
705                    ));
706                }
707                #[cfg(feature = "mcp-sampling-tools")]
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}