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    /// Custom application-specific hints.
56    #[serde(flatten)]
57    pub custom: HashMap<String, serde_json::Value>,
58}
59
60/// Represents a tool that can be executed by an MCP server, as per the MCP 2025-06-18 specification.
61///
62/// A `Tool` definition includes its programmatic name, a human-readable description,
63/// and JSON schemas for its inputs and outputs.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct Tool {
66    /// The programmatic name of the tool, used to identify it in `CallToolRequest`.
67    pub name: String,
68
69    /// An optional, user-friendly title for the tool. Display name precedence is: `title`, `annotations.title`, then `name`.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub title: Option<String>,
72
73    /// A human-readable description of what the tool does, which can be used by clients or LLMs.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub description: Option<String>,
76
77    /// The JSON Schema object defining the parameters the tool accepts.
78    #[serde(rename = "inputSchema")]
79    pub input_schema: ToolInputSchema,
80
81    /// An optional JSON Schema object defining the structure of the tool's successful output.
82    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
83    pub output_schema: Option<ToolOutputSchema>,
84
85    /// Optional, additional metadata providing hints about the tool's behavior.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub annotations: Option<ToolAnnotations>,
88
89    /// A general-purpose metadata field for custom data.
90    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
91    pub meta: Option<HashMap<String, serde_json::Value>>,
92}
93
94impl Default for Tool {
95    fn default() -> Self {
96        Self {
97            name: "unnamed_tool".to_string(), // Must have a valid name for MCP compliance
98            title: None,
99            description: None,
100            input_schema: ToolInputSchema::default(),
101            output_schema: None,
102            annotations: None,
103            meta: None,
104        }
105    }
106}
107
108impl Tool {
109    /// Creates a new `Tool` with a given name.
110    ///
111    /// # Panics
112    /// Panics if the name is empty or contains only whitespace.
113    pub fn new(name: impl Into<String>) -> Self {
114        let name = name.into();
115        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
116        Self {
117            name,
118            title: None,
119            description: None,
120            input_schema: ToolInputSchema::default(),
121            output_schema: None,
122            annotations: None,
123            meta: None,
124        }
125    }
126
127    /// Creates a new `Tool` with a name and a description.
128    ///
129    /// # Panics
130    /// Panics if the name is empty or contains only whitespace.
131    pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
132        let name = name.into();
133        assert!(!name.trim().is_empty(), "Tool name cannot be empty");
134        Self {
135            name,
136            title: None,
137            description: Some(description.into()),
138            input_schema: ToolInputSchema::default(),
139            output_schema: None,
140            annotations: None,
141            meta: None,
142        }
143    }
144
145    /// Sets the input schema for this tool.
146    ///
147    /// # Example
148    /// ```
149    /// # use turbomcp_protocol::types::{Tool, ToolInputSchema};
150    /// let schema = ToolInputSchema::empty();
151    /// let tool = Tool::new("my_tool").with_input_schema(schema);
152    /// ```
153    pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
154        self.input_schema = schema;
155        self
156    }
157
158    /// Sets the output schema for this tool.
159    pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
160        self.output_schema = Some(schema);
161        self
162    }
163
164    /// Sets the user-friendly title for this tool.
165    pub fn with_title(mut self, title: impl Into<String>) -> Self {
166        self.title = Some(title.into());
167        self
168    }
169
170    /// Sets the annotations for this tool.
171    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
172        self.annotations = Some(annotations);
173        self
174    }
175}
176
177/// Defines the structure of the arguments a tool accepts, as a JSON Schema object.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ToolInputSchema {
180    /// The type of the schema, which must be "object" for tool inputs.
181    #[serde(rename = "type")]
182    pub schema_type: String,
183    /// A map defining the properties (parameters) the tool accepts.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub properties: Option<HashMap<String, serde_json::Value>>,
186    /// A list of property names that are required.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub required: Option<Vec<String>>,
189    /// Whether additional, unspecified properties are allowed.
190    #[serde(
191        rename = "additionalProperties",
192        skip_serializing_if = "Option::is_none"
193    )]
194    pub additional_properties: Option<bool>,
195}
196
197impl Default for ToolInputSchema {
198    /// Creates a default `ToolInputSchema` that accepts an empty object.
199    fn default() -> Self {
200        Self {
201            schema_type: "object".to_string(),
202            properties: None,
203            required: None,
204            additional_properties: None,
205        }
206    }
207}
208
209impl ToolInputSchema {
210    /// Creates a new, empty input schema that accepts no parameters.
211    pub fn empty() -> Self {
212        Self::default()
213    }
214
215    /// Creates a new schema with a given set of properties.
216    pub fn with_properties(properties: HashMap<String, serde_json::Value>) -> Self {
217        Self {
218            schema_type: "object".to_string(),
219            properties: Some(properties),
220            required: None,
221            additional_properties: None,
222        }
223    }
224
225    /// Creates a new schema with a given set of properties and a list of required properties.
226    pub fn with_required_properties(
227        properties: HashMap<String, serde_json::Value>,
228        required: Vec<String>,
229    ) -> Self {
230        Self {
231            schema_type: "object".to_string(),
232            properties: Some(properties),
233            required: Some(required),
234            additional_properties: Some(false),
235        }
236    }
237
238    /// Adds a property to the schema using a builder pattern.
239    ///
240    /// # Example
241    /// ```
242    /// # use turbomcp_protocol::types::ToolInputSchema;
243    /// # use serde_json::json;
244    /// let schema = ToolInputSchema::empty()
245    ///     .add_property("name".to_string(), json!({ "type": "string" }));
246    /// ```
247    pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
248        self.properties
249            .get_or_insert_with(HashMap::new)
250            .insert(name, property);
251        self
252    }
253
254    /// Marks a property as required using a builder pattern.
255    ///
256    /// # Example
257    /// ```
258    /// # use turbomcp_protocol::types::ToolInputSchema;
259    /// # use serde_json::json;
260    /// let schema = ToolInputSchema::empty()
261    ///     .add_property("name".to_string(), json!({ "type": "string" }))
262    ///     .require_property("name".to_string());
263    /// ```
264    pub fn require_property(mut self, name: String) -> Self {
265        let required = self.required.get_or_insert_with(Vec::new);
266        if !required.contains(&name) {
267            required.push(name);
268        }
269        self
270    }
271}
272
273/// Defines the structure of a tool's successful output, as a JSON Schema object.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ToolOutputSchema {
276    /// The type of the schema, which must be "object" for tool outputs.
277    #[serde(rename = "type")]
278    pub schema_type: String,
279    /// A map defining the properties of the output object.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub properties: Option<HashMap<String, serde_json::Value>>,
282    /// A list of property names in the output that are required.
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub required: Option<Vec<String>>,
285    /// Whether additional, unspecified properties are allowed in the output.
286    #[serde(
287        rename = "additionalProperties",
288        skip_serializing_if = "Option::is_none"
289    )]
290    pub additional_properties: Option<bool>,
291}
292
293/// A request to list the available tools on a server.
294#[derive(Debug, Clone, Serialize, Deserialize, Default)]
295pub struct ListToolsRequest {
296    /// An optional cursor for pagination. If provided, the server should return
297    /// the next page of results starting after this cursor.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub cursor: Option<Cursor>,
300    /// Optional metadata for the request.
301    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
302    pub _meta: Option<serde_json::Value>,
303}
304
305/// The result of a `ListToolsRequest`.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ListToolsResult {
308    /// The list of available tools for the current page.
309    pub tools: Vec<Tool>,
310    /// An optional continuation token for retrieving the next page of results.
311    /// If `None`, there are no more results.
312    #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
313    pub next_cursor: Option<Cursor>,
314    /// Optional metadata for the result.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub _meta: Option<serde_json::Value>,
317}
318
319/// A request to execute a specific tool.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct CallToolRequest {
322    /// The programmatic name of the tool to call.
323    pub name: String,
324    /// The arguments to pass to the tool, conforming to its `input_schema`.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub arguments: Option<HashMap<String, serde_json::Value>>,
327    /// Optional metadata for the request.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub _meta: Option<serde_json::Value>,
330}
331
332/// The result of a `CallToolRequest`.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct CallToolResult {
335    /// The output of the tool, typically as a series of text or other content blocks. This is required.
336    pub content: Vec<ContentBlock>,
337    /// An optional boolean indicating whether the tool execution resulted in an error.
338    ///
339    /// When `is_error` is `true`, all content blocks should be treated as error information.
340    /// The error message may span multiple text blocks for structured error reporting.
341    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
342    pub is_error: Option<bool>,
343    /// Optional structured output from the tool, conforming to its `output_schema`.
344    ///
345    /// When present, this contains schema-validated JSON output that clients can parse
346    /// and use programmatically. Tools that return structured content SHOULD also include
347    /// the serialized JSON in a TextContent block for backward compatibility with clients
348    /// that don't support structured output.
349    ///
350    /// See [`Tool::output_schema`] for defining the expected structure.
351    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
352    pub structured_content: Option<serde_json::Value>,
353    /// Optional metadata for the result.
354    ///
355    /// This field is for client applications and tools to pass additional context that
356    /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
357    /// cache status, or internal state information.
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub _meta: Option<serde_json::Value>,
360}
361
362impl CallToolResult {
363    /// Extracts and concatenates all text content from the result.
364    ///
365    /// This is useful for simple text-only tools or when you want to present
366    /// all textual output as a single string.
367    ///
368    /// # Returns
369    ///
370    /// A single string containing all text blocks concatenated with newlines.
371    /// Returns an empty string if there are no text blocks.
372    ///
373    /// # Example
374    ///
375    /// ```rust
376    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
377    ///
378    /// let result = CallToolResult {
379    ///     content: vec![
380    ///         ContentBlock::Text(TextContent {
381    ///             text: "Line 1".to_string(),
382    ///             annotations: None,
383    ///             meta: None,
384    ///         }),
385    ///         ContentBlock::Text(TextContent {
386    ///             text: "Line 2".to_string(),
387    ///             annotations: None,
388    ///             meta: None,
389    ///         }),
390    ///     ],
391    ///     is_error: None,
392    ///     structured_content: None,
393    ///     _meta: None,
394    /// };
395    ///
396    /// assert_eq!(result.all_text(), "Line 1\nLine 2");
397    /// ```
398    pub fn all_text(&self) -> String {
399        self.content
400            .iter()
401            .filter_map(|block| match block {
402                ContentBlock::Text(text) => Some(text.text.as_str()),
403                _ => None,
404            })
405            .collect::<Vec<_>>()
406            .join("\n")
407    }
408
409    /// Returns the text content of the first text block, if any.
410    ///
411    /// This is a common pattern for simple tools that return a single text response.
412    ///
413    /// # Returns
414    ///
415    /// `Some(&str)` if the first content block is text, `None` otherwise.
416    ///
417    /// # Example
418    ///
419    /// ```rust
420    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
421    ///
422    /// let result = CallToolResult {
423    ///     content: vec![
424    ///         ContentBlock::Text(TextContent {
425    ///             text: "Hello, world!".to_string(),
426    ///             annotations: None,
427    ///             meta: None,
428    ///         }),
429    ///     ],
430    ///     is_error: None,
431    ///     structured_content: None,
432    ///     _meta: None,
433    /// };
434    ///
435    /// assert_eq!(result.first_text(), Some("Hello, world!"));
436    /// ```
437    pub fn first_text(&self) -> Option<&str> {
438        self.content.first().and_then(|block| match block {
439            ContentBlock::Text(text) => Some(text.text.as_str()),
440            _ => None,
441        })
442    }
443
444    /// Checks if the tool execution resulted in an error.
445    ///
446    /// # Returns
447    ///
448    /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
449    /// (including when `is_error` is `None`).
450    ///
451    /// # Example
452    ///
453    /// ```rust
454    /// use turbomcp_protocol::types::CallToolResult;
455    ///
456    /// let success_result = CallToolResult {
457    ///     content: vec![],
458    ///     is_error: Some(false),
459    ///     structured_content: None,
460    ///     _meta: None,
461    /// };
462    /// assert!(!success_result.has_error());
463    ///
464    /// let error_result = CallToolResult {
465    ///     content: vec![],
466    ///     is_error: Some(true),
467    ///     structured_content: None,
468    ///     _meta: None,
469    /// };
470    /// assert!(error_result.has_error());
471    ///
472    /// let unspecified_result = CallToolResult {
473    ///     content: vec![],
474    ///     is_error: None,
475    ///     structured_content: None,
476    ///     _meta: None,
477    /// };
478    /// assert!(!unspecified_result.has_error());
479    /// ```
480    pub fn has_error(&self) -> bool {
481        self.is_error.unwrap_or(false)
482    }
483
484    /// Creates a user-friendly display string for the tool result.
485    ///
486    /// This method provides a formatted representation suitable for logging,
487    /// debugging, or displaying to end users. It handles multiple content types
488    /// and includes structured content and error information when present.
489    ///
490    /// # Returns
491    ///
492    /// A formatted string representing the tool result.
493    ///
494    /// # Example
495    ///
496    /// ```rust
497    /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
498    ///
499    /// let result = CallToolResult {
500    ///     content: vec![
501    ///         ContentBlock::Text(TextContent {
502    ///             text: "Operation completed".to_string(),
503    ///             annotations: None,
504    ///             meta: None,
505    ///         }),
506    ///     ],
507    ///     is_error: Some(false),
508    ///     structured_content: None,
509    ///     _meta: None,
510    /// };
511    ///
512    /// let display = result.to_display_string();
513    /// assert!(display.contains("Operation completed"));
514    /// ```
515    pub fn to_display_string(&self) -> String {
516        let mut parts = Vec::new();
517
518        // Add error indicator if present
519        if self.has_error() {
520            parts.push("ERROR:".to_string());
521        }
522
523        // Process content blocks
524        for (i, block) in self.content.iter().enumerate() {
525            match block {
526                ContentBlock::Text(text) => {
527                    parts.push(text.text.clone());
528                }
529                ContentBlock::Image(img) => {
530                    parts.push(format!(
531                        "[Image: {} bytes, type: {}]",
532                        img.data.len(),
533                        img.mime_type
534                    ));
535                }
536                ContentBlock::Audio(audio) => {
537                    parts.push(format!(
538                        "[Audio: {} bytes, type: {}]",
539                        audio.data.len(),
540                        audio.mime_type
541                    ));
542                }
543                ContentBlock::ResourceLink(link) => {
544                    let desc = link.description.as_deref().unwrap_or("");
545                    let mime = link
546                        .mime_type
547                        .as_deref()
548                        .map(|m| format!(" [{}]", m))
549                        .unwrap_or_default();
550                    parts.push(format!(
551                        "[Resource: {}{}{}{}]",
552                        link.name,
553                        mime,
554                        if !desc.is_empty() { ": " } else { "" },
555                        desc
556                    ));
557                }
558                ContentBlock::Resource(_resource) => {
559                    parts.push(format!("[Embedded Resource #{}]", i + 1));
560                }
561            }
562        }
563
564        // Add structured content indicator if present
565        if self.structured_content.is_some() {
566            parts.push("[Includes structured output]".to_string());
567        }
568
569        parts.join("\n")
570    }
571}