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-11-25: 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 #[serde(skip_serializing_if = "Option::is_none")]
168 pub icons: Option<Vec<super::core::Icon>>,
169
170 /// A general-purpose metadata field for custom data.
171 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
172 pub meta: Option<HashMap<String, serde_json::Value>>,
173}
174
175impl Default for Tool {
176 fn default() -> Self {
177 Self {
178 name: "unnamed_tool".to_string(), // Must have a valid name for MCP compliance
179 title: None,
180 description: None,
181 input_schema: ToolInputSchema::default(),
182 output_schema: None,
183 execution: None,
184 annotations: None,
185 icons: None,
186 meta: None,
187 }
188 }
189}
190
191impl Tool {
192 /// Creates a new `Tool` with a given name.
193 ///
194 /// # Panics
195 /// Panics if the name is empty or contains only whitespace.
196 pub fn new(name: impl Into<String>) -> Self {
197 let name = name.into();
198 assert!(!name.trim().is_empty(), "Tool name cannot be empty");
199 Self {
200 name,
201 title: None,
202 description: None,
203 input_schema: ToolInputSchema::default(),
204 output_schema: None,
205 execution: None,
206 annotations: None,
207 icons: None,
208 meta: None,
209 }
210 }
211
212 /// Creates a new `Tool` with a name and a description.
213 ///
214 /// # Panics
215 /// Panics if the name is empty or contains only whitespace.
216 pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
217 let name = name.into();
218 assert!(!name.trim().is_empty(), "Tool name cannot be empty");
219 Self {
220 name,
221 title: None,
222 description: Some(description.into()),
223 input_schema: ToolInputSchema::default(),
224 output_schema: None,
225 execution: None,
226 annotations: None,
227 icons: None,
228 meta: None,
229 }
230 }
231
232 /// Sets the execution properties for this tool.
233 pub fn with_execution(mut self, execution: ToolExecution) -> Self {
234 self.execution = Some(execution);
235 self
236 }
237
238 /// Sets the input schema for this tool.
239 ///
240 /// # Example
241 /// ```
242 /// # use turbomcp_protocol::types::{Tool, ToolInputSchema};
243 /// let schema = ToolInputSchema::empty();
244 /// let tool = Tool::new("my_tool").with_input_schema(schema);
245 /// ```
246 pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
247 self.input_schema = schema;
248 self
249 }
250
251 /// Sets the output schema for this tool.
252 pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
253 self.output_schema = Some(schema);
254 self
255 }
256
257 /// Sets the user-friendly title for this tool.
258 pub fn with_title(mut self, title: impl Into<String>) -> Self {
259 self.title = Some(title.into());
260 self
261 }
262
263 /// Sets the annotations for this tool.
264 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
265 self.annotations = Some(annotations);
266 self
267 }
268}
269
270/// Defines the structure of the arguments a tool accepts, as a JSON Schema object.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ToolInputSchema {
273 /// The schema type declaration. This may be a string or an array of strings.
274 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
275 pub schema_type: Option<serde_json::Value>,
276 /// A map defining the properties (parameters) the tool accepts.
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub properties: Option<HashMap<String, serde_json::Value>>,
279 /// A list of property names that are required.
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub required: Option<Vec<String>>,
282 /// Whether additional, unspecified properties are allowed, or a schema constraining them.
283 #[serde(
284 rename = "additionalProperties",
285 skip_serializing_if = "Option::is_none"
286 )]
287 pub additional_properties: Option<serde_json::Value>,
288 /// Additional JSON Schema keywords preserved losslessly.
289 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
290 pub extra_keywords: HashMap<String, serde_json::Value>,
291}
292
293impl Default for ToolInputSchema {
294 /// Creates a default `ToolInputSchema` that accepts an empty object.
295 fn default() -> Self {
296 Self {
297 schema_type: Some(serde_json::Value::String("object".to_string())),
298 properties: None,
299 required: None,
300 additional_properties: None,
301 extra_keywords: HashMap::new(),
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: Some(serde_json::Value::String("object".to_string())),
316 properties: Some(properties),
317 required: None,
318 additional_properties: None,
319 extra_keywords: HashMap::new(),
320 }
321 }
322
323 /// Creates a new schema with a given set of properties and a list of required properties.
324 pub fn with_required_properties(
325 properties: HashMap<String, serde_json::Value>,
326 required: Vec<String>,
327 ) -> Self {
328 Self {
329 schema_type: Some(serde_json::Value::String("object".to_string())),
330 properties: Some(properties),
331 required: Some(required),
332 additional_properties: Some(serde_json::Value::Bool(false)),
333 extra_keywords: HashMap::new(),
334 }
335 }
336
337 /// Adds a property to the schema using a builder pattern.
338 ///
339 /// # Example
340 /// ```
341 /// # use turbomcp_protocol::types::ToolInputSchema;
342 /// # use serde_json::json;
343 /// let schema = ToolInputSchema::empty()
344 /// .add_property("name".to_string(), json!({ "type": "string" }));
345 /// ```
346 pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
347 self.properties
348 .get_or_insert_with(HashMap::new)
349 .insert(name, property);
350 self
351 }
352
353 /// Marks a property as required using a builder pattern.
354 ///
355 /// # Example
356 /// ```
357 /// # use turbomcp_protocol::types::ToolInputSchema;
358 /// # use serde_json::json;
359 /// let schema = ToolInputSchema::empty()
360 /// .add_property("name".to_string(), json!({ "type": "string" }))
361 /// .require_property("name".to_string());
362 /// ```
363 pub fn require_property(mut self, name: String) -> Self {
364 let required = self.required.get_or_insert_with(Vec::new);
365 if !required.contains(&name) {
366 required.push(name);
367 }
368 self
369 }
370}
371
372/// Defines the structure of a tool's successful output, as a JSON Schema object.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct ToolOutputSchema {
375 /// The schema type declaration. This may be a string or an array of strings.
376 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
377 pub schema_type: Option<serde_json::Value>,
378 /// A map defining the properties of the output object.
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub properties: Option<HashMap<String, serde_json::Value>>,
381 /// A list of property names in the output that are required.
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub required: Option<Vec<String>>,
384 /// Whether additional, unspecified properties are allowed in the output, or a schema constraining them.
385 #[serde(
386 rename = "additionalProperties",
387 skip_serializing_if = "Option::is_none"
388 )]
389 pub additional_properties: Option<serde_json::Value>,
390 /// Additional JSON Schema keywords preserved losslessly.
391 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
392 pub extra_keywords: HashMap<String, serde_json::Value>,
393}
394
395/// A request to list the available tools on a server.
396#[derive(Debug, Clone, Serialize, Deserialize, Default)]
397pub struct ListToolsRequest {
398 /// An optional cursor for pagination. If provided, the server should return
399 /// the next page of results starting after this cursor.
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub cursor: Option<Cursor>,
402 /// Optional metadata for the request.
403 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
404 pub _meta: Option<serde_json::Value>,
405}
406
407/// The result of a `ListToolsRequest`.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ListToolsResult {
410 /// The list of available tools for the current page.
411 pub tools: Vec<Tool>,
412 /// An optional continuation token for retrieving the next page of results.
413 /// If `None`, there are no more results.
414 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
415 pub next_cursor: Option<Cursor>,
416 /// Optional metadata for the result.
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub _meta: Option<serde_json::Value>,
419}
420
421/// A request to execute a specific tool.
422///
423/// ## Version Support
424/// - MCP 2025-11-25: name, arguments, _meta
425/// - MCP 2025-11-25 draft (SEP-1686): + task (optional task augmentation)
426///
427/// ## Task Augmentation
428///
429/// When the `task` field is present, the receiver responds immediately with
430/// a `CreateTaskResult` containing a task ID. The actual tool result is available
431/// later via `tasks/result`.
432///
433/// ```rust,ignore
434/// use turbomcp_protocol::types::{CallToolRequest, tasks::TaskMetadata};
435///
436/// let request = CallToolRequest {
437/// name: "long_running_tool".to_string(),
438/// arguments: Some(json!({"data": "value"})),
439/// task: Some(TaskMetadata { ttl: Some(300_000) }), // 5 minute lifetime
440/// _meta: None,
441/// };
442/// ```
443#[derive(Debug, Clone, Serialize, Deserialize, Default)]
444pub struct CallToolRequest {
445 /// The programmatic name of the tool to call.
446 pub name: String,
447
448 /// The arguments to pass to the tool, conforming to its `input_schema`.
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub arguments: Option<HashMap<String, serde_json::Value>>,
451
452 /// Optional task metadata for task-augmented requests (MCP 2025-11-25 draft)
453 ///
454 /// When present, this request will be executed asynchronously and the receiver
455 /// will respond immediately with a `CreateTaskResult`. The actual tool result
456 /// is available later via `tasks/result`.
457 ///
458 /// Requires:
459 /// - Server capability: `tasks.requests.tools.call`
460 /// - Tool annotation: `taskHint` must be "optional" or "always" (or absent/"never" for default)
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub task: Option<crate::types::tasks::TaskMetadata>,
463
464 /// Optional metadata for the request.
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub _meta: Option<serde_json::Value>,
467}
468
469/// The result of a `CallToolRequest`.
470#[derive(Debug, Clone, Serialize, Deserialize, Default)]
471pub struct CallToolResult {
472 /// The output of the tool, typically as a series of text or other content blocks. This is required.
473 pub content: Vec<ContentBlock>,
474 /// An optional boolean indicating whether the tool execution resulted in an error.
475 ///
476 /// When `is_error` is `true`, all content blocks should be treated as error information.
477 /// The error message may span multiple text blocks for structured error reporting.
478 #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
479 pub is_error: Option<bool>,
480 /// Optional structured output from the tool, conforming to its `output_schema`.
481 ///
482 /// When present, this contains schema-validated JSON output that clients can parse
483 /// and use programmatically. Tools that return structured content SHOULD also include
484 /// the serialized JSON in a TextContent block for backward compatibility with clients
485 /// that don't support structured output.
486 ///
487 /// See [`Tool::output_schema`] for defining the expected structure.
488 #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
489 pub structured_content: Option<serde_json::Value>,
490 /// Optional metadata for the result.
491 ///
492 /// This field is for client applications and tools to pass additional context that
493 /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
494 /// cache status, or internal state information.
495 #[serde(skip_serializing_if = "Option::is_none")]
496 pub _meta: Option<serde_json::Value>,
497 /// Optional task ID when tool execution is augmented with task tracking (MCP 2025-11-25 draft - SEP-1686).
498 ///
499 /// When a tool call includes task metadata, the server creates a task to track the operation
500 /// and returns the task_id here. Clients can use this to monitor progress via tasks/get
501 /// or retrieve final results via tasks/result.
502 #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
503 pub task_id: Option<String>,
504}
505
506impl CallToolResult {
507 /// Extracts and concatenates all text content from the result.
508 ///
509 /// This is useful for simple text-only tools or when you want to present
510 /// all textual output as a single string.
511 ///
512 /// # Returns
513 ///
514 /// A single string containing all text blocks concatenated with newlines.
515 /// Returns an empty string if there are no text blocks.
516 ///
517 /// # Example
518 ///
519 /// ```rust
520 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
521 ///
522 /// let result = CallToolResult {
523 /// content: vec![
524 /// ContentBlock::Text(TextContent {
525 /// text: "Line 1".to_string(),
526 /// annotations: None,
527 /// meta: None,
528 /// }),
529 /// ContentBlock::Text(TextContent {
530 /// text: "Line 2".to_string(),
531 /// annotations: None,
532 /// meta: None,
533 /// }),
534 /// ],
535 /// is_error: None,
536 /// structured_content: None,
537 /// _meta: None,
538 /// task_id: None,
539 /// };
540 ///
541 /// assert_eq!(result.all_text(), "Line 1\nLine 2");
542 /// ```
543 pub fn all_text(&self) -> String {
544 self.content
545 .iter()
546 .filter_map(|block| match block {
547 ContentBlock::Text(text) => Some(text.text.as_str()),
548 _ => None,
549 })
550 .collect::<Vec<_>>()
551 .join("\n")
552 }
553
554 /// Returns the text content of the first text block, if any.
555 ///
556 /// This is a common pattern for simple tools that return a single text response.
557 ///
558 /// # Returns
559 ///
560 /// `Some(&str)` if the first content block is text, `None` otherwise.
561 ///
562 /// # Example
563 ///
564 /// ```rust
565 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
566 ///
567 /// let result = CallToolResult {
568 /// content: vec![
569 /// ContentBlock::Text(TextContent {
570 /// text: "Hello, world!".to_string(),
571 /// annotations: None,
572 /// meta: None,
573 /// }),
574 /// ],
575 /// is_error: None,
576 /// structured_content: None,
577 /// _meta: None,
578 /// task_id: None,
579 /// };
580 ///
581 /// assert_eq!(result.first_text(), Some("Hello, world!"));
582 /// ```
583 pub fn first_text(&self) -> Option<&str> {
584 self.content.first().and_then(|block| match block {
585 ContentBlock::Text(text) => Some(text.text.as_str()),
586 _ => None,
587 })
588 }
589
590 /// Checks if the tool execution resulted in an error.
591 ///
592 /// # Returns
593 ///
594 /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
595 /// (including when `is_error` is `None`).
596 ///
597 /// # Example
598 ///
599 /// ```rust
600 /// use turbomcp_protocol::types::CallToolResult;
601 ///
602 /// let success_result = CallToolResult {
603 /// content: vec![],
604 /// is_error: Some(false),
605 /// structured_content: None,
606 /// _meta: None,
607 /// task_id: None,
608 /// };
609 /// assert!(!success_result.has_error());
610 ///
611 /// let error_result = CallToolResult {
612 /// content: vec![],
613 /// is_error: Some(true),
614 /// structured_content: None,
615 /// _meta: None,
616 /// task_id: None,
617 /// };
618 /// assert!(error_result.has_error());
619 ///
620 /// let unspecified_result = CallToolResult {
621 /// content: vec![],
622 /// is_error: None,
623 /// structured_content: None,
624 /// _meta: None,
625 /// task_id: None,
626 /// };
627 /// assert!(!unspecified_result.has_error());
628 /// ```
629 pub fn has_error(&self) -> bool {
630 self.is_error.unwrap_or(false)
631 }
632
633 /// Creates a user-friendly display string for the tool result.
634 ///
635 /// This method provides a formatted representation suitable for logging,
636 /// debugging, or displaying to end users. It handles multiple content types
637 /// and includes structured content and error information when present.
638 ///
639 /// # Returns
640 ///
641 /// A formatted string representing the tool result.
642 ///
643 /// # Example
644 ///
645 /// ```rust
646 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
647 ///
648 /// let result = CallToolResult {
649 /// content: vec![
650 /// ContentBlock::Text(TextContent {
651 /// text: "Operation completed".to_string(),
652 /// annotations: None,
653 /// meta: None,
654 /// }),
655 /// ],
656 /// is_error: Some(false),
657 /// structured_content: None,
658 /// _meta: None,
659 /// task_id: None,
660 /// };
661 ///
662 /// let display = result.to_display_string();
663 /// assert!(display.contains("Operation completed"));
664 /// ```
665 pub fn to_display_string(&self) -> String {
666 let mut parts = Vec::new();
667
668 // Add error indicator if present
669 if self.has_error() {
670 parts.push("ERROR:".to_string());
671 }
672
673 // Process content blocks
674 for (i, block) in self.content.iter().enumerate() {
675 match block {
676 ContentBlock::Text(text) => {
677 parts.push(text.text.clone());
678 }
679 ContentBlock::Image(img) => {
680 parts.push(format!(
681 "[Image: {} bytes, type: {}]",
682 img.data.len(),
683 img.mime_type
684 ));
685 }
686 ContentBlock::Audio(audio) => {
687 parts.push(format!(
688 "[Audio: {} bytes, type: {}]",
689 audio.data.len(),
690 audio.mime_type
691 ));
692 }
693 ContentBlock::ResourceLink(link) => {
694 let desc = link.description.as_deref().unwrap_or("");
695 let mime = link
696 .mime_type
697 .as_deref()
698 .map(|m| format!(" [{}]", m))
699 .unwrap_or_default();
700 parts.push(format!(
701 "[Resource: {}{}{}{}]",
702 link.name,
703 mime,
704 if !desc.is_empty() { ": " } else { "" },
705 desc
706 ));
707 }
708 ContentBlock::Resource(_resource) => {
709 parts.push(format!("[Embedded Resource #{}]", i + 1));
710 }
711 ContentBlock::ToolUse(tool_use) => {
712 parts.push(format!(
713 "[Tool Use: {} (id: {})]",
714 tool_use.name, tool_use.id
715 ));
716 }
717 ContentBlock::ToolResult(tool_result) => {
718 parts.push(format!(
719 "[Tool Result for: {}{}]",
720 tool_result.tool_use_id,
721 if tool_result.is_error.unwrap_or(false) {
722 " (ERROR)"
723 } else {
724 ""
725 }
726 ));
727 }
728 }
729 }
730
731 // Add structured content indicator if present
732 if self.structured_content.is_some() {
733 parts.push("[Includes structured output]".to_string());
734 }
735
736 parts.join("\n")
737 }
738}
739
740// =============================================================================
741// Conversions from turbomcp-core types (for unified handler support)
742// =============================================================================
743//
744// These conversions enable the unified IntoToolResponse pattern, allowing
745// handlers to return core types that are automatically converted to protocol types.
746//
747// IMPORTANT NOTES:
748// - HashMap conversion: O(n) overhead due to hashbrown→std HashMap conversion
749// - Lossy conversion: Protocol's `structured_content` and `task_id` fields are
750// NOT present in core types, so round-trip (protocol→core→protocol) loses them
751// - Resource fallback: Empty text is used if ResourceContent has neither text nor blob
752
753/// Convert core Annotations to protocol Annotations.
754///
755/// Note: Incurs O(n) conversion overhead from `hashbrown::HashMap` to `std::collections::HashMap`.
756impl From<turbomcp_core::types::core::Annotations> for super::Annotations {
757 fn from(core_ann: turbomcp_core::types::core::Annotations) -> Self {
758 // Convert hashbrown::HashMap to std::collections::HashMap
759 // Both implementations guarantee unique keys, so no data loss occurs
760 let custom: std::collections::HashMap<String, serde_json::Value> =
761 core_ann.custom.into_iter().collect();
762 Self {
763 audience: core_ann.audience,
764 priority: core_ann.priority,
765 last_modified: core_ann.last_modified,
766 custom,
767 }
768 }
769}
770
771/// Convert core Content to protocol ContentBlock
772impl From<turbomcp_core::types::content::Content> for super::ContentBlock {
773 fn from(content: turbomcp_core::types::content::Content) -> Self {
774 use turbomcp_core::types::content::Content as CoreContent;
775 match content {
776 CoreContent::Text { text, annotations } => {
777 super::ContentBlock::Text(super::TextContent {
778 text,
779 annotations: annotations.map(Into::into),
780 meta: None,
781 })
782 }
783 CoreContent::Image {
784 data,
785 mime_type,
786 annotations,
787 } => super::ContentBlock::Image(super::ImageContent {
788 data: data.into(),
789 mime_type: mime_type.to_string().into(),
790 annotations: annotations.map(Into::into),
791 meta: None,
792 }),
793 CoreContent::Audio {
794 data,
795 mime_type,
796 annotations,
797 } => super::ContentBlock::Audio(super::AudioContent {
798 data: data.into(),
799 mime_type: mime_type.to_string().into(),
800 annotations: annotations.map(Into::into),
801 meta: None,
802 }),
803 CoreContent::Resource {
804 resource,
805 annotations,
806 } => {
807 // Convert core ResourceContent to protocol EmbeddedResource
808 // Core uses a flat struct with optional text/blob, protocol uses an enum
809 let protocol_resource = if let Some(text) = resource.text {
810 super::ResourceContent::Text(super::TextResourceContents {
811 uri: resource.uri.to_string().into(),
812 mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
813 text,
814 meta: None,
815 })
816 } else if let Some(blob) = resource.blob {
817 super::ResourceContent::Blob(super::BlobResourceContents {
818 uri: resource.uri.to_string().into(),
819 mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
820 blob: blob.into(),
821 meta: None,
822 })
823 } else {
824 // Default to empty text if neither is set.
825 // NOTE: This is a fallback for malformed core resources - callers should
826 // ensure ResourceContent has either text or blob set.
827 #[cfg(feature = "std")]
828 eprintln!(
829 "[turbomcp-protocol] WARNING: Resource '{}' has neither text nor blob content",
830 resource.uri
831 );
832 super::ResourceContent::Text(super::TextResourceContents {
833 uri: resource.uri.to_string().into(),
834 mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
835 text: String::new(),
836 meta: None,
837 })
838 };
839 super::ContentBlock::Resource(super::EmbeddedResource {
840 resource: protocol_resource,
841 annotations: annotations.map(Into::into),
842 meta: None,
843 })
844 }
845 }
846 }
847}
848
849/// Convert core CallToolResult to protocol CallToolResult
850///
851/// This enables the unified IntoToolResponse pattern for native handlers.
852///
853/// **Note**: `structured_content` and `task_id` fields are set to `None` since
854/// core types don't have these fields. Round-trip conversion (protocol→core→protocol)
855/// will lose these values.
856impl From<turbomcp_core::types::tools::CallToolResult> for CallToolResult {
857 fn from(core_result: turbomcp_core::types::tools::CallToolResult) -> Self {
858 Self {
859 content: core_result.content.into_iter().map(Into::into).collect(),
860 is_error: core_result.is_error,
861 structured_content: None,
862 _meta: core_result._meta,
863 task_id: None,
864 }
865 }
866}
867
868#[cfg(test)]
869mod conversion_tests {
870 use super::*;
871 use turbomcp_core::types::content::Content as CoreContent;
872 use turbomcp_core::types::tools::CallToolResult as CoreCallToolResult;
873
874 #[test]
875 fn test_core_content_to_protocol_text() {
876 let core = CoreContent::text("hello world");
877 let protocol: ContentBlock = core.into();
878
879 match protocol {
880 ContentBlock::Text(text) => {
881 assert_eq!(text.text, "hello world");
882 assert!(text.annotations.is_none());
883 }
884 _ => panic!("Expected Text variant"),
885 }
886 }
887
888 #[test]
889 fn test_core_content_to_protocol_image() {
890 let core = CoreContent::image("base64data", "image/png");
891 let protocol: ContentBlock = core.into();
892
893 match protocol {
894 ContentBlock::Image(img) => {
895 assert_eq!(img.data, "base64data");
896 assert_eq!(img.mime_type, "image/png");
897 }
898 _ => panic!("Expected Image variant"),
899 }
900 }
901
902 #[test]
903 fn test_core_call_tool_result_to_protocol() {
904 let core = CoreCallToolResult::text("success");
905 let protocol: CallToolResult = core.into();
906
907 assert_eq!(protocol.content.len(), 1);
908 assert!(protocol.is_error.is_none());
909 assert!(protocol.structured_content.is_none());
910 assert!(protocol.task_id.is_none());
911
912 match &protocol.content[0] {
913 ContentBlock::Text(text) => assert_eq!(text.text, "success"),
914 _ => panic!("Expected Text content"),
915 }
916 }
917
918 #[test]
919 fn test_core_call_tool_result_error_preserved() {
920 let core = CoreCallToolResult::error("something failed");
921 let protocol: CallToolResult = core.into();
922
923 assert_eq!(protocol.is_error, Some(true));
924 match &protocol.content[0] {
925 ContentBlock::Text(text) => assert_eq!(text.text, "something failed"),
926 _ => panic!("Expected Text content"),
927 }
928 }
929
930 #[test]
931 fn test_annotations_conversion() {
932 use crate::types::Annotations;
933 use turbomcp_core::types::core::Annotations as CoreAnnotations;
934
935 // Create core annotations (custom field uses Default for simplicity)
936 let core = CoreAnnotations {
937 audience: Some(vec!["user".to_string(), "assistant".to_string()]),
938 priority: Some(0.75),
939 last_modified: Some("2025-01-13T12:00:00Z".to_string()),
940 custom: Default::default(),
941 };
942
943 let protocol: Annotations = core.into();
944
945 // Verify all fields are correctly converted
946 assert_eq!(
947 protocol.audience,
948 Some(vec!["user".to_string(), "assistant".to_string()])
949 );
950 assert_eq!(protocol.priority, Some(0.75));
951 assert_eq!(
952 protocol.last_modified,
953 Some("2025-01-13T12:00:00Z".to_string())
954 );
955 assert!(protocol.custom.is_empty());
956 }
957}