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}