mcp_probe_core/messages/
tools.rs

1//! Tool-related message types for MCP tool discovery and execution.
2//!
3//! This module provides types for:
4//! - Tool discovery (listing available tools)
5//! - Tool execution (calling tools with parameters)
6//! - Tool schema definitions (parameter validation)
7//! - Tool result handling (success/error responses)
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Request to list available tools from the server.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct ListToolsRequest {
16    /// Optional cursor for pagination
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub cursor: Option<String>,
19}
20
21/// Response containing the list of available tools.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct ListToolsResponse {
24    /// List of available tools
25    pub tools: Vec<Tool>,
26
27    /// Optional cursor for next page of results
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub next_cursor: Option<String>,
30}
31
32/// Tool definition including schema and metadata.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34pub struct Tool {
35    /// Unique name of the tool
36    pub name: String,
37
38    /// Human-readable description of what the tool does
39    pub description: String,
40
41    /// JSON Schema for the tool's input parameters
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub input_schema: Option<Value>,
44
45    /// Additional tool extensions and metadata
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub extensions: Option<Value>,
48
49    /// Whether the tool is read-only
50    #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
51    pub read_only: Option<bool>,
52
53    /// Return type schema for the tool
54    #[serde(rename = "returnType", skip_serializing_if = "Option::is_none")]
55    pub return_type: Option<Value>,
56}
57
58// Custom deserializer for Tool to handle multiple schema field names
59impl<'de> Deserialize<'de> for Tool {
60    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61    where
62        D: serde::Deserializer<'de>,
63    {
64        use serde::de::{self, MapAccess, Visitor};
65        use std::fmt;
66
67        #[derive(Deserialize)]
68        #[serde(field_identifier, rename_all = "camelCase")]
69        enum Field {
70            Name,
71            Description,
72            #[serde(alias = "input_schema")]
73            InputSchema,
74            #[serde(alias = "parameters_schema")]
75            ParametersSchema,
76            Extensions,
77            #[serde(alias = "read_only")]
78            ReadOnly,
79            ReturnType,
80            #[serde(other)]
81            Unknown,
82        }
83
84        struct ToolVisitor;
85
86        impl<'de> Visitor<'de> for ToolVisitor {
87            type Value = Tool;
88
89            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
90                formatter.write_str("struct Tool")
91            }
92
93            fn visit_map<V>(self, mut map: V) -> Result<Tool, V::Error>
94            where
95                V: MapAccess<'de>,
96            {
97                let mut name = None;
98                let mut description = None;
99                let mut input_schema = None;
100                let mut extensions = None;
101                let mut read_only = None;
102                let mut return_type = None;
103
104                while let Some(key) = map.next_key()? {
105                    match key {
106                        Field::Name => {
107                            if name.is_some() {
108                                return Err(de::Error::duplicate_field("name"));
109                            }
110                            name = Some(map.next_value()?);
111                        }
112                        Field::Description => {
113                            if description.is_some() {
114                                return Err(de::Error::duplicate_field("description"));
115                            }
116                            description = Some(map.next_value()?);
117                        }
118                        Field::InputSchema => {
119                            if input_schema.is_none() {
120                                input_schema = Some(map.next_value()?);
121                            } else {
122                                // Skip if we already have a schema
123                                let _: Value = map.next_value()?;
124                            }
125                        }
126                        Field::ParametersSchema => {
127                            if input_schema.is_none() {
128                                input_schema = Some(map.next_value()?);
129                            } else {
130                                // Skip if we already have a schema
131                                let _: Value = map.next_value()?;
132                            }
133                        }
134                        Field::Extensions => {
135                            if extensions.is_some() {
136                                return Err(de::Error::duplicate_field("extensions"));
137                            }
138                            extensions = Some(map.next_value()?);
139                        }
140                        Field::ReadOnly => {
141                            if read_only.is_some() {
142                                return Err(de::Error::duplicate_field("readOnly"));
143                            }
144                            read_only = Some(map.next_value()?);
145                        }
146                        Field::ReturnType => {
147                            if return_type.is_some() {
148                                return Err(de::Error::duplicate_field("returnType"));
149                            }
150                            return_type = Some(map.next_value()?);
151                        }
152                        Field::Unknown => {
153                            // Skip unknown fields
154                            let _: Value = map.next_value()?;
155                        }
156                    }
157                }
158
159                let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
160                let description =
161                    description.ok_or_else(|| de::Error::missing_field("description"))?;
162
163                Ok(Tool {
164                    name,
165                    description,
166                    input_schema,
167                    extensions,
168                    read_only,
169                    return_type,
170                })
171            }
172        }
173
174        const FIELDS: &[&str] = &[
175            "name",
176            "description",
177            "inputSchema",
178            "parametersSchema",
179            "extensions",
180            "readOnly",
181            "returnType",
182        ];
183        deserializer.deserialize_struct("Tool", FIELDS, ToolVisitor)
184    }
185}
186
187impl Tool {
188    /// Create a new tool definition.
189    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
190        Self {
191            name: name.into(),
192            description: description.into(),
193            input_schema: None,
194            extensions: None,
195            read_only: None,
196            return_type: None,
197        }
198    }
199
200    /// Set the input schema for this tool.
201    pub fn with_input_schema(mut self, schema: Value) -> Self {
202        self.input_schema = Some(schema);
203        self
204    }
205
206    /// Set the extensions for this tool.
207    pub fn with_extensions(mut self, extensions: Value) -> Self {
208        self.extensions = Some(extensions);
209        self
210    }
211
212    /// Set the read-only flag for this tool.
213    pub fn with_read_only(mut self, read_only: bool) -> Self {
214        self.read_only = Some(read_only);
215        self
216    }
217
218    /// Set the return type schema for this tool.
219    pub fn with_return_type(mut self, return_type: Value) -> Self {
220        self.return_type = Some(return_type);
221        self
222    }
223}
224
225/// Request to call a tool with specific arguments.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct CallToolRequest {
228    /// Name of the tool to call
229    pub name: String,
230
231    /// Arguments to pass to the tool
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub arguments: Option<Value>,
234}
235
236/// Response from a tool call operation.
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct CallToolResponse {
239    /// Results from the tool execution
240    #[serde(default)]
241    pub content: Vec<ToolResult>,
242
243    /// Whether the tool is making a progress notification
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub is_error: Option<bool>,
246}
247
248/// Result content from a tool execution.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(tag = "type")]
251pub enum ToolResult {
252    /// Text content result
253    #[serde(rename = "text")]
254    Text {
255        /// The text content
256        text: String,
257    },
258
259    /// Image content result
260    #[serde(rename = "image")]
261    Image {
262        /// Image data (base64 encoded)
263        data: String,
264
265        /// MIME type of the image
266        #[serde(rename = "mimeType")]
267        mime_type: String,
268    },
269
270    /// Resource reference result
271    #[serde(rename = "resource")]
272    Resource {
273        /// URI of the resource
274        resource: ResourceReference,
275    },
276}
277
278/// Reference to a resource.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct ResourceReference {
281    /// URI of the resource
282    pub uri: String,
283
284    /// Optional description of the resource
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub text: Option<String>,
287}
288
289/// Notification that the list of tools has changed.
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
291pub struct ToolListChangedNotification {
292    /// Additional metadata about the change
293    #[serde(flatten)]
294    pub metadata: HashMap<String, Value>,
295}
296
297impl ToolListChangedNotification {
298    /// Create a new tool list changed notification.
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// Add metadata to the notification.
304    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
305        self.metadata.insert(key.into(), value);
306        self
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use serde_json::json;
314
315    #[test]
316    fn test_tool_creation() {
317        let tool = Tool::new("calculator", "A simple calculator tool").with_input_schema(json!({
318            "type": "object",
319            "properties": {
320                "expression": {"type": "string"}
321            },
322            "required": ["expression"]
323        }));
324
325        assert_eq!(tool.name, "calculator");
326        assert_eq!(tool.description, "A simple calculator tool");
327        assert!(tool.input_schema.is_some());
328        assert_eq!(tool.extensions, None);
329        assert_eq!(tool.read_only, None);
330        assert_eq!(tool.return_type, None);
331    }
332
333    #[test]
334    fn test_list_tools_request() {
335        let request = ListToolsRequest { cursor: None };
336        let json = serde_json::to_string(&request).unwrap();
337        let deserialized: ListToolsRequest = serde_json::from_str(&json).unwrap();
338        assert_eq!(request, deserialized);
339    }
340
341    #[test]
342    fn test_call_tool_request() {
343        let request = CallToolRequest {
344            name: "calculator".to_string(),
345            arguments: Some(json!({"expression": "2 + 2"})),
346        };
347
348        let json = serde_json::to_string(&request).unwrap();
349        let deserialized: CallToolRequest = serde_json::from_str(&json).unwrap();
350        assert_eq!(request, deserialized);
351    }
352
353    #[test]
354    fn test_tool_result_text() {
355        let result = ToolResult::Text {
356            text: "The answer is 4".to_string(),
357        };
358
359        let json = serde_json::to_value(&result).unwrap();
360        assert_eq!(json["type"], "text");
361        assert_eq!(json["text"], "The answer is 4");
362    }
363
364    #[test]
365    fn test_tool_result_image() {
366        let result = ToolResult::Image {
367            data: "base64data".to_string(),
368            mime_type: "image/png".to_string(),
369        };
370
371        let json = serde_json::to_value(&result).unwrap();
372        assert_eq!(json["type"], "image");
373        assert_eq!(json["mimeType"], "image/png");
374    }
375
376    #[test]
377    fn test_tool_deserialization_with_camel_case() {
378        // Test that we can deserialize tools with camelCase field names
379        let json_str = r#"{
380            "name": "test-tool",
381            "description": "A test tool",
382            "inputSchema": {
383                "type": "object",
384                "properties": {
385                    "param1": {"type": "string"}
386                }
387            },
388            "readOnly": true
389        }"#;
390
391        let tool: Tool = serde_json::from_str(json_str).unwrap();
392        assert_eq!(tool.name, "test-tool");
393        assert_eq!(tool.description, "A test tool");
394        assert!(tool.input_schema.is_some());
395        assert_eq!(tool.read_only, Some(true));
396        assert_eq!(tool.return_type, None);
397    }
398
399    #[test]
400    fn test_tool_deserialization_with_parameters_schema() {
401        // Test that we can deserialize tools with parametersSchema field
402        let json_str = r#"{
403            "name": "test-tool",
404            "description": "A test tool",
405            "parametersSchema": {
406                "type": "object",
407                "properties": {
408                    "param1": {"type": "string"}
409                }
410            }
411        }"#;
412
413        let tool: Tool = serde_json::from_str(json_str).unwrap();
414        assert_eq!(tool.name, "test-tool");
415        assert_eq!(tool.description, "A test tool");
416        assert!(tool.input_schema.is_some());
417    }
418}