turul_mcp_protocol_2025_06_18/
tools.rs

1//! MCP Tools Protocol Types
2//!
3//! This module defines the types used for the MCP tools functionality.
4
5use crate::meta::Cursor;
6use crate::schema::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11
12/// Tool annotations structure (matches TypeScript ToolAnnotations)
13/// NOTE: all properties in ToolAnnotations are **hints**.
14/// They are not guaranteed to provide a faithful description of tool behavior.
15/// Clients should never make tool use decisions based on ToolAnnotations from untrusted servers.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct ToolAnnotations {
19    /// A human-readable title for the tool
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub title: Option<String>,
22    /// If true, the tool does not modify its environment. Default: false
23    #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
24    pub read_only_hint: Option<bool>,
25    /// If true, the tool may perform destructive updates to its environment.
26    /// If false, the tool performs only additive updates.
27    /// (This property is meaningful only when `readOnlyHint == false`) Default: true
28    #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
29    pub destructive_hint: Option<bool>,
30    /// If true, calling the tool repeatedly with the same arguments
31    /// will have no additional effect on its environment.
32    /// (This property is meaningful only when `readOnlyHint == false`) Default: false
33    #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
34    pub idempotent_hint: Option<bool>,
35    /// If true, this tool may interact with an "open world" of external entities.
36    /// If false, the tool's domain of interaction is closed.
37    /// For example, the world of a web search tool is open, whereas that of a memory tool is not.
38    /// Default: true
39    #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
40    pub open_world_hint: Option<bool>,
41}
42
43impl Default for ToolAnnotations {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl ToolAnnotations {
50    pub fn new() -> Self {
51        Self {
52            title: None,
53            read_only_hint: None,
54            destructive_hint: None,
55            idempotent_hint: None,
56            open_world_hint: None,
57        }
58    }
59
60    pub fn with_title(mut self, title: impl Into<String>) -> Self {
61        self.title = Some(title.into());
62        self
63    }
64
65    pub fn with_read_only_hint(mut self, read_only: bool) -> Self {
66        self.read_only_hint = Some(read_only);
67        self
68    }
69
70    pub fn with_destructive_hint(mut self, destructive: bool) -> Self {
71        self.destructive_hint = Some(destructive);
72        self
73    }
74
75    pub fn with_idempotent_hint(mut self, idempotent: bool) -> Self {
76        self.idempotent_hint = Some(idempotent);
77        self
78    }
79
80    pub fn with_open_world_hint(mut self, open_world: bool) -> Self {
81        self.open_world_hint = Some(open_world);
82        self
83    }
84}
85
86// === Protocol Types ===
87
88/// JSON Schema definition for tool input/output (matches TypeScript spec exactly)
89/// Must be an object with type: "object", properties, and required fields
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ToolSchema {
92    /// The schema type (must be "object" for tools)
93    #[serde(rename = "type")]
94    pub schema_type: String,
95    /// Property definitions
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub properties: Option<HashMap<String, JsonSchema>>,
98    /// Required property names
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub required: Option<Vec<String>>,
101    /// Additional schema properties
102    #[serde(flatten)]
103    pub additional: HashMap<String, Value>,
104}
105
106impl ToolSchema {
107    pub fn object() -> Self {
108        Self {
109            schema_type: "object".to_string(),
110            properties: None,
111            required: None,
112            additional: HashMap::new(),
113        }
114    }
115
116    pub fn with_properties(mut self, properties: HashMap<String, JsonSchema>) -> Self {
117        self.properties = Some(properties);
118        self
119    }
120
121    pub fn with_required(mut self, required: Vec<String>) -> Self {
122        self.required = Some(required);
123        self
124    }
125}
126
127/// Tool definition
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct Tool {
131    /// The tool's name - used as identifier when calling
132    pub name: String,
133    /// Intended for UI and end-user contexts — optimized to be human-readable
134    /// and easily understood, even by those unfamiliar with domain-specific terminology.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub title: Option<String>,
137    /// Optional human-readable description
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub description: Option<String>,
140    /// JSON Schema for input parameters
141    pub input_schema: ToolSchema,
142    /// Optional JSON Schema for output results
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub output_schema: Option<ToolSchema>,
145    /// Optional annotations for client hints
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub annotations: Option<ToolAnnotations>,
148
149    #[serde(
150        default,
151        skip_serializing_if = "Option::is_none",
152        alias = "_meta",
153        rename = "_meta"
154    )]
155    pub meta: Option<HashMap<String, Value>>,
156}
157
158impl Tool {
159    pub fn new(name: impl Into<String>, input_schema: ToolSchema) -> Self {
160        Self {
161            name: name.into(),
162            title: None,
163            description: None,
164            input_schema,
165            output_schema: None,
166            annotations: None,
167            meta: None,
168        }
169    }
170
171    pub fn with_title(mut self, title: impl Into<String>) -> Self {
172        self.title = Some(title.into());
173        self
174    }
175
176    pub fn with_description(mut self, description: impl Into<String>) -> Self {
177        self.description = Some(description.into());
178        self
179    }
180
181    pub fn with_output_schema(mut self, output_schema: ToolSchema) -> Self {
182        self.output_schema = Some(output_schema);
183        self
184    }
185
186    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
187        self.annotations = Some(annotations);
188        self
189    }
190
191    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
192        self.meta = Some(meta);
193        self
194    }
195}
196
197/// Parameters for tools/list request
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct ListToolsParams {
201    /// Optional cursor for pagination
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub cursor: Option<Cursor>,
204    /// Optional limit for page size
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub limit: Option<u32>,
207    /// Meta information (optional _meta field inside params)
208    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
209    pub meta: Option<HashMap<String, Value>>,
210}
211
212impl ListToolsParams {
213    pub fn new() -> Self {
214        Self {
215            cursor: None,
216            limit: None,
217            meta: None,
218        }
219    }
220
221    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
222        self.cursor = Some(cursor);
223        self
224    }
225
226    pub fn with_limit(mut self, limit: u32) -> Self {
227        self.limit = Some(limit);
228        self
229    }
230
231    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
232        self.meta = Some(meta);
233        self
234    }
235}
236
237impl Default for ListToolsParams {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Complete tools/list request (matches TypeScript ListToolsRequest interface)
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct ListToolsRequest {
247    /// Method name (always "tools/list")
248    pub method: String,
249    /// Request parameters
250    pub params: ListToolsParams,
251}
252
253impl Default for ListToolsRequest {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259impl ListToolsRequest {
260    pub fn new() -> Self {
261        Self {
262            method: "tools/list".to_string(),
263            params: ListToolsParams::new(),
264        }
265    }
266
267    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
268        self.params = self.params.with_cursor(cursor);
269        self
270    }
271
272    pub fn with_limit(mut self, limit: u32) -> Self {
273        self.params = self.params.with_limit(limit);
274        self
275    }
276
277    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
278        self.params = self.params.with_meta(meta);
279        self
280    }
281}
282
283/// Result for tools/list (per MCP spec) - extends PaginatedResult
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ListToolsResult {
287    /// Available tools
288    pub tools: Vec<Tool>,
289    /// Optional cursor for next page
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub next_cursor: Option<Cursor>,
292    /// Meta information (from PaginatedResult)
293    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
294    pub meta: Option<HashMap<String, Value>>,
295}
296
297impl ListToolsResult {
298    pub fn new(tools: Vec<Tool>) -> Self {
299        Self {
300            tools,
301            next_cursor: None,
302            meta: None,
303        }
304    }
305
306    pub fn with_next_cursor(mut self, cursor: Cursor) -> Self {
307        self.next_cursor = Some(cursor);
308        self
309    }
310
311    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
312        self.meta = Some(meta);
313        self
314    }
315}
316
317/// Parameters for tools/call request (matches TypeScript CallToolRequest.params)
318#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct CallToolParams {
321    /// Name of the tool to call
322    pub name: String,
323    /// Arguments to pass to the tool - matches TypeScript { [key: string]: unknown }
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub arguments: Option<HashMap<String, Value>>,
326    /// Meta information (optional _meta field inside params)
327    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
328    pub meta: Option<HashMap<String, Value>>,
329}
330
331impl CallToolParams {
332    pub fn new(name: impl Into<String>) -> Self {
333        Self {
334            name: name.into(),
335            arguments: None,
336            meta: None,
337        }
338    }
339
340    /// Get arguments as HashMap - CRITICAL: Use this instead of the trait method
341    /// The trait method has limitations due to lifetime issues with HashMap->Value conversion
342    pub fn get_arguments(&self) -> Option<&HashMap<String, Value>> {
343        self.arguments.as_ref()
344    }
345
346    /// Get arguments as Value (converted from HashMap)
347    pub fn get_arguments_as_value(&self) -> Option<Value> {
348        self.arguments
349            .as_ref()
350            .map(|map| Value::Object(map.clone().into_iter().collect()))
351    }
352
353    pub fn with_arguments(mut self, arguments: HashMap<String, Value>) -> Self {
354        self.arguments = Some(arguments);
355        self
356    }
357
358    pub fn with_arguments_value(mut self, arguments: Value) -> Self {
359        // Helper for backward compatibility - convert Value to HashMap if it's an object
360        if let Value::Object(map) = arguments {
361            self.arguments = Some(map.into_iter().collect());
362        }
363        self
364    }
365
366    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
367        self.meta = Some(meta);
368        self
369    }
370}
371
372/// Complete tools/call request (matches TypeScript CallToolRequest interface)
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct CallToolRequest {
376    /// Method name (always "tools/call")
377    pub method: String,
378    /// Request parameters
379    pub params: CallToolParams,
380}
381
382impl CallToolRequest {
383    pub fn new(name: impl Into<String>) -> Self {
384        Self {
385            method: "tools/call".to_string(),
386            params: CallToolParams::new(name),
387        }
388    }
389
390    pub fn with_arguments(mut self, arguments: HashMap<String, Value>) -> Self {
391        self.params = self.params.with_arguments(arguments);
392        self
393    }
394
395    pub fn with_arguments_value(mut self, arguments: Value) -> Self {
396        self.params = self.params.with_arguments_value(arguments);
397        self
398    }
399
400    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
401        self.params = self.params.with_meta(meta);
402        self
403    }
404}
405
406/// Tool result type - an alias for ContentBlock to maintain backward compatibility
407/// while ensuring MCP 2025-06-18 specification compliance
408pub type ToolResult = crate::content::ContentBlock;
409
410/// Result for tools/call (per MCP spec)
411#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct CallToolResult {
414    /// Content returned by the tool
415    pub content: Vec<ToolResult>,
416    /// Whether the tool call resulted in an error
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub is_error: Option<bool>,
419    /// Structured content that matches the tool's output schema (MCP 2025-06-18)
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub structured_content: Option<Value>,
422    /// Meta information (follows MCP Result interface)
423    #[serde(
424        default,
425        skip_serializing_if = "Option::is_none",
426        alias = "_meta",
427        rename = "_meta"
428    )]
429    pub meta: Option<HashMap<String, Value>>,
430}
431
432impl CallToolResult {
433    pub fn new(content: Vec<ToolResult>) -> Self {
434        Self {
435            content,
436            is_error: None,
437            structured_content: None,
438            meta: None,
439        }
440    }
441
442    pub fn success(content: Vec<ToolResult>) -> Self {
443        Self {
444            content,
445            is_error: Some(false),
446            structured_content: None,
447            meta: None,
448        }
449    }
450
451    pub fn error(content: Vec<ToolResult>) -> Self {
452        Self {
453            content,
454            is_error: Some(true),
455            structured_content: None,
456            meta: None,
457        }
458    }
459
460    pub fn with_error_flag(mut self, is_error: bool) -> Self {
461        self.is_error = Some(is_error);
462        self
463    }
464
465    pub fn with_structured_content(mut self, structured_content: Value) -> Self {
466        self.structured_content = Some(structured_content);
467        self
468    }
469
470    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
471        self.meta = Some(meta);
472        self
473    }
474
475    // ===========================================
476    // === Smart Response Builders ===
477    // ===========================================
478
479    /// Create response from serializable result with optional structured content
480    pub fn from_result<T: serde::Serialize>(result: &T) -> Result<Self, crate::McpError> {
481        let text_content = serde_json::to_string(result)
482            .map_err(|e| crate::McpError::tool_execution(&format!("Serialization error: {}", e)))?;
483
484        Ok(Self::success(vec![ToolResult::text(text_content)]))
485    }
486
487    /// Create response with both text and structured content
488    pub fn from_result_with_structured<T: serde::Serialize>(
489        result: &T,
490    ) -> Result<Self, crate::McpError> {
491        let text_content = serde_json::to_string(result)
492            .map_err(|e| crate::McpError::tool_execution(&format!("Serialization error: {}", e)))?;
493
494        let structured = serde_json::to_value(result).map_err(|e| {
495            crate::McpError::tool_execution(&format!("Structured content error: {}", e))
496        })?;
497
498        Ok(Self::success(vec![ToolResult::text(text_content)])
499            .with_structured_content(structured))
500    }
501
502    /// Create response from serializable result with automatic structured content based on schema
503    pub fn from_result_with_schema<T: serde::Serialize>(
504        result: &T,
505        schema: Option<&ToolSchema>,
506    ) -> Result<Self, crate::McpError> {
507        let text_content = serde_json::to_string(result)
508            .map_err(|e| crate::McpError::tool_execution(&format!("Serialization error: {}", e)))?;
509
510        let response = Self::success(vec![ToolResult::text(text_content)]);
511
512        // Auto-add structured content if schema exists
513        if schema.is_some() {
514            let structured = serde_json::to_value(result).map_err(|e| {
515                crate::McpError::tool_execution(&format!("Structured content error: {}", e))
516            })?;
517            Ok(response.with_structured_content(structured))
518        } else {
519            Ok(response)
520        }
521    }
522
523    /// Create response with automatic structured content for primitives (zero-config)
524    pub fn from_result_auto<T: serde::Serialize>(
525        result: &T,
526        schema: Option<&ToolSchema>,
527    ) -> Result<Self, crate::McpError> {
528        let text_content = serde_json::to_string(result)
529            .map_err(|e| crate::McpError::tool_execution(&format!("Serialization error: {}", e)))?;
530
531        let response = Self::success(vec![ToolResult::text(text_content)]);
532
533        // Auto-detect structured content for common types
534        let structured = serde_json::to_value(result).map_err(|e| {
535            crate::McpError::tool_execution(&format!("Structured content error: {}", e))
536        })?;
537
538        let should_add_structured = schema.is_some()
539            || match &structured {
540                // Auto-add structured content for primitive types (zero-config)
541                Value::Number(_) | Value::Bool(_) => true,
542                // Auto-add for arrays and objects (structured data)
543                Value::Array(_) | Value::Object(_) => true,
544                // Skip for plain strings (text is sufficient)
545                Value::String(_) => false,
546                Value::Null => false,
547            };
548
549        if should_add_structured {
550            Ok(response.with_structured_content(structured))
551        } else {
552            Ok(response)
553        }
554    }
555
556    /// Create response from JSON value with automatic structured content
557    pub fn from_json_with_schema(json_result: Value, schema: Option<&ToolSchema>) -> Self {
558        let text_content = json_result.to_string();
559        let response = Self::success(vec![ToolResult::text(text_content)]);
560
561        if schema.is_some() {
562            response.with_structured_content(json_result)
563        } else {
564            response
565        }
566    }
567}
568
569// Trait implementations for CallToolResult
570
571use crate::traits::*;
572
573impl HasData for CallToolResult {
574    fn data(&self) -> HashMap<String, Value> {
575        let mut data = HashMap::new();
576        data.insert(
577            "content".to_string(),
578            serde_json::to_value(&self.content).unwrap_or(Value::Null),
579        );
580        if let Some(is_error) = self.is_error {
581            data.insert("isError".to_string(), Value::Bool(is_error));
582        }
583        if let Some(ref structured_content) = self.structured_content {
584            data.insert("structuredContent".to_string(), structured_content.clone());
585        }
586        data
587    }
588}
589
590impl HasMeta for CallToolResult {
591    fn meta(&self) -> Option<HashMap<String, Value>> {
592        self.meta.clone()
593    }
594}
595
596impl RpcResult for CallToolResult {}
597
598impl crate::traits::CallToolResult for CallToolResult {
599    fn content(&self) -> &Vec<ToolResult> {
600        &self.content
601    }
602
603    fn is_error(&self) -> Option<bool> {
604        self.is_error
605    }
606
607    fn structured_content(&self) -> Option<&Value> {
608        self.structured_content.as_ref()
609    }
610}
611
612// Trait implementations for ListToolsParams
613impl Params for ListToolsParams {}
614
615impl HasListToolsParams for ListToolsParams {
616    fn cursor(&self) -> Option<&Cursor> {
617        self.cursor.as_ref()
618    }
619}
620
621impl HasMetaParam for ListToolsParams {
622    fn meta(&self) -> Option<&HashMap<String, Value>> {
623        self.meta.as_ref()
624    }
625}
626
627// Trait implementations for ListToolsRequest
628impl HasMethod for ListToolsRequest {
629    fn method(&self) -> &str {
630        &self.method
631    }
632}
633
634impl HasParams for ListToolsRequest {
635    fn params(&self) -> Option<&dyn Params> {
636        Some(&self.params)
637    }
638}
639
640// Trait implementations for ListToolsResult
641impl HasData for ListToolsResult {
642    fn data(&self) -> HashMap<String, Value> {
643        let mut data = HashMap::new();
644        data.insert(
645            "tools".to_string(),
646            serde_json::to_value(&self.tools).unwrap_or(Value::Null),
647        );
648        if let Some(ref next_cursor) = self.next_cursor {
649            data.insert(
650                "nextCursor".to_string(),
651                Value::String(next_cursor.as_str().to_string()),
652            );
653        }
654        data
655    }
656}
657
658impl HasMeta for ListToolsResult {
659    fn meta(&self) -> Option<HashMap<String, Value>> {
660        self.meta.clone()
661    }
662}
663
664impl RpcResult for ListToolsResult {}
665
666impl crate::traits::ListToolsResult for ListToolsResult {
667    fn tools(&self) -> &Vec<Tool> {
668        &self.tools
669    }
670
671    fn next_cursor(&self) -> Option<&Cursor> {
672        self.next_cursor.as_ref()
673    }
674}
675
676// Trait implementations for CallToolParams
677impl Params for CallToolParams {}
678
679impl HasCallToolParams for CallToolParams {
680    fn name(&self) -> &String {
681        &self.name
682    }
683
684    fn arguments(&self) -> Option<&Value> {
685        // Note: This trait method has limitations due to HashMap<String, Value> -> Value conversion
686        // The conversion creates a temporary Value that can't be borrowed for the required lifetime.
687        //
688        // For now, use CallToolParams::get_arguments() for HashMap access or
689        // get_arguments_as_value() for owned Value access in downstream code.
690        //
691        // The direct .arguments field access works fine and is used by the framework.
692        None
693    }
694
695    fn meta(&self) -> Option<&HashMap<String, Value>> {
696        self.meta.as_ref()
697    }
698}
699
700// Trait implementations for CallToolRequest
701impl HasMethod for CallToolRequest {
702    fn method(&self) -> &str {
703        &self.method
704    }
705}
706
707impl HasParams for CallToolRequest {
708    fn params(&self) -> Option<&dyn Params> {
709        Some(&self.params)
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use crate::content::ResourceContents;
717    use serde_json::json;
718
719    #[test]
720    fn test_tool_creation() {
721        let schema = ToolSchema::object()
722            .with_properties(HashMap::from([("text".to_string(), JsonSchema::string())]))
723            .with_required(vec!["text".to_string()]);
724
725        let tool = Tool::new("test_tool", schema).with_description("A test tool");
726
727        assert_eq!(tool.name, "test_tool");
728        assert!(tool.description.is_some());
729        assert_eq!(tool.input_schema.schema_type, "object");
730    }
731
732    #[test]
733    fn test_tool_result_creation() {
734        let text_result = ToolResult::text("Hello, world!");
735        let image_result = ToolResult::image("base64data", "image/png");
736        let resource_result = ToolResult::resource(ResourceContents::text(
737            "file:///test/resource.json",
738            serde_json::to_string(&json!({"key": "value"})).unwrap(),
739        ));
740
741        assert!(matches!(text_result, ToolResult::Text { .. }));
742        assert!(matches!(image_result, ToolResult::Image { .. }));
743        assert!(matches!(resource_result, ToolResult::Resource { .. }));
744    }
745
746    #[test]
747    fn test_call_tool_response() {
748        let response =
749            CallToolResult::success(vec![ToolResult::text("Operation completed successfully")]);
750
751        assert_eq!(response.is_error, Some(false));
752        assert_eq!(response.content.len(), 1);
753        assert!(response.structured_content.is_none());
754    }
755
756    #[test]
757    fn test_call_tool_response_with_structured_content() {
758        let structured_data = serde_json::json!({
759            "result": "success",
760            "value": 42
761        });
762
763        let response =
764            CallToolResult::success(vec![ToolResult::text("Operation completed successfully")])
765                .with_structured_content(structured_data.clone());
766
767        assert_eq!(response.is_error, Some(false));
768        assert_eq!(response.content.len(), 1);
769        assert_eq!(response.structured_content, Some(structured_data));
770    }
771
772    #[test]
773    fn test_serialization() {
774        let tool = Tool::new("echo", ToolSchema::object()).with_description("Echo tool");
775
776        let json = serde_json::to_string(&tool).unwrap();
777        assert!(json.contains("echo"));
778        assert!(json.contains("Echo tool"));
779
780        let parsed: Tool = serde_json::from_str(&json).unwrap();
781        assert_eq!(parsed.name, "echo");
782    }
783}