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