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}