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