Skip to main content

thulp_core/
tool.rs

1//! Tool types for thulp.
2
3use crate::{Error, Parameter, ParameterType, Result};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Definition of an available tool.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ToolDefinition {
10    /// The tool name (unique identifier).
11    pub name: String,
12
13    /// Human-readable description of what the tool does.
14    #[serde(default)]
15    pub description: String,
16
17    /// Parameters accepted by the tool.
18    #[serde(default)]
19    pub parameters: Vec<Parameter>,
20}
21
22impl ToolDefinition {
23    /// Create a new tool definition.
24    pub fn new(name: impl Into<String>) -> Self {
25        Self {
26            name: name.into(),
27            description: String::new(),
28            parameters: Vec::new(),
29        }
30    }
31
32    /// Create a builder for a tool definition.
33    pub fn builder(name: impl Into<String>) -> ToolDefinitionBuilder {
34        ToolDefinitionBuilder::new(name)
35    }
36
37    /// Get a parameter by name.
38    pub fn get_parameter(&self, name: &str) -> Option<&Parameter> {
39        self.parameters.iter().find(|p| p.name == name)
40    }
41
42    /// Get all required parameters.
43    pub fn required_parameters(&self) -> impl Iterator<Item = &Parameter> {
44        self.parameters.iter().filter(|p| p.required)
45    }
46
47    /// Validate arguments against this tool's parameters.
48    pub fn validate_args(&self, args: &Value) -> Result<()> {
49        let empty_map = serde_json::Map::new();
50        let args_obj = args.as_object().unwrap_or(&empty_map);
51
52        // Check required parameters
53        for param in self.required_parameters() {
54            if !args_obj.contains_key(&param.name) {
55                // Check if there's a default
56                if param.default.is_none() {
57                    return Err(Error::MissingParameter(param.name.clone()));
58                }
59            }
60        }
61
62        // Check parameter types
63        for (key, value) in args_obj {
64            if let Some(param) = self.get_parameter(key) {
65                if !param.param_type.matches(value) {
66                    return Err(Error::InvalidParameterType {
67                        name: key.clone(),
68                        expected: param.param_type.as_str().to_string(),
69                        actual: json_type_name(value).to_string(),
70                    });
71                }
72
73                // Check enum values if defined
74                if !param.enum_values.is_empty() && !param.enum_values.contains(value) {
75                    return Err(Error::InvalidConfig(format!(
76                        "parameter '{}' must be one of: {:?}",
77                        key, param.enum_values
78                    )));
79                }
80            }
81        }
82
83        Ok(())
84    }
85
86    /// Convert this tool definition into an MCP-compatible JSON Schema `Value`
87    /// suitable for the `tools[].function.parameters` field that
88    /// OpenAI-compatible LLM APIs expect.
89    ///
90    /// Inverse of `parse_mcp_input_schema`. Round-trip is structurally stable
91    /// for `name`, `param_type`, `required`, `description`, `default`, and
92    /// `enum_values`. Round-trip is exact when no extra schema fields are
93    /// present.
94    pub fn to_mcp_input_schema(&self) -> serde_json::Value {
95        let mut properties = serde_json::Map::new();
96        let mut required: Vec<serde_json::Value> = Vec::new();
97
98        for param in &self.parameters {
99            let mut prop = serde_json::Map::new();
100            prop.insert(
101                "type".to_string(),
102                serde_json::Value::String(param.param_type.as_str().to_string()),
103            );
104            if !param.description.is_empty() {
105                prop.insert(
106                    "description".to_string(),
107                    serde_json::Value::String(param.description.clone()),
108                );
109            }
110            if !param.enum_values.is_empty() {
111                prop.insert(
112                    "enum".to_string(),
113                    serde_json::Value::Array(param.enum_values.clone()),
114                );
115            }
116            if let Some(default) = &param.default {
117                prop.insert("default".to_string(), default.clone());
118            }
119            properties.insert(param.name.clone(), serde_json::Value::Object(prop));
120
121            if param.required {
122                required.push(serde_json::Value::String(param.name.clone()));
123            }
124        }
125
126        serde_json::json!({
127            "type": "object",
128            "properties": properties,
129            "required": required,
130        })
131    }
132
133    /// Parse MCP inputSchema into Parameters
134    pub fn parse_mcp_input_schema(schema: &serde_json::Value) -> Result<Vec<Parameter>> {
135        let mut params = Vec::new();
136
137        if let Some(properties) = schema.get("properties") {
138            if let Some(props_obj) = properties.as_object() {
139                for (name, prop) in props_obj {
140                    let param_type = if let Some(type_val) = prop.get("type") {
141                        match type_val.as_str() {
142                            Some("string") => ParameterType::String,
143                            Some("integer") => ParameterType::Integer,
144                            Some("number") => ParameterType::Number,
145                            Some("boolean") => ParameterType::Boolean,
146                            Some("array") => ParameterType::Array,
147                            Some("object") => ParameterType::Object,
148                            _ => ParameterType::String,
149                        }
150                    } else {
151                        ParameterType::String
152                    };
153
154                    let description = prop
155                        .get("description")
156                        .and_then(|v| v.as_str())
157                        .unwrap_or("")
158                        .to_string();
159
160                    let required = if let Some(req_array) = schema.get("required") {
161                        if let Some(arr) = req_array.as_array() {
162                            arr.iter().any(|v| v.as_str() == Some(name))
163                        } else {
164                            false
165                        }
166                    } else {
167                        false
168                    };
169
170                    params.push(Parameter {
171                        name: name.to_string(),
172                        param_type,
173                        description,
174                        required,
175                        default: None,
176                        enum_values: vec![],
177                    });
178                }
179            }
180        }
181
182        Ok(params)
183    }
184}
185
186/// Get the JSON type name for a value.
187fn json_type_name(value: &Value) -> &'static str {
188    match value {
189        Value::Null => "null",
190        Value::Bool(_) => "boolean",
191        Value::Number(_) => "number",
192        Value::String(_) => "string",
193        Value::Array(_) => "array",
194        Value::Object(_) => "object",
195    }
196}
197
198/// Builder for [`ToolDefinition`].
199#[derive(Debug, Default)]
200pub struct ToolDefinitionBuilder {
201    name: String,
202    description: String,
203    parameters: Vec<Parameter>,
204}
205
206impl ToolDefinitionBuilder {
207    /// Create a new tool definition builder.
208    pub fn new(name: impl Into<String>) -> Self {
209        Self {
210            name: name.into(),
211            ..Default::default()
212        }
213    }
214
215    /// Set the tool description.
216    pub fn description(mut self, description: impl Into<String>) -> Self {
217        self.description = description.into();
218        self
219    }
220
221    /// Add a parameter.
222    pub fn parameter(mut self, parameter: Parameter) -> Self {
223        self.parameters.push(parameter);
224        self
225    }
226
227    /// Add multiple parameters.
228    pub fn parameters(mut self, parameters: impl IntoIterator<Item = Parameter>) -> Self {
229        self.parameters.extend(parameters);
230        self
231    }
232
233    /// Build the tool definition.
234    pub fn build(self) -> ToolDefinition {
235        ToolDefinition {
236            name: self.name,
237            description: self.description,
238            parameters: self.parameters,
239        }
240    }
241}
242
243/// A request to execute a tool.
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct ToolCall {
246    /// The name of the tool to call.
247    pub tool: String,
248
249    /// Arguments to pass to the tool.
250    #[serde(default)]
251    pub arguments: Value,
252}
253
254impl ToolCall {
255    /// Create a new tool call.
256    pub fn new(tool: impl Into<String>) -> Self {
257        Self {
258            tool: tool.into(),
259            arguments: Value::Object(serde_json::Map::new()),
260        }
261    }
262
263    /// Create a tool call with arguments.
264    pub fn with_args(tool: impl Into<String>, arguments: Value) -> Self {
265        Self {
266            tool: tool.into(),
267            arguments,
268        }
269    }
270
271    /// Create a builder for a tool call.
272    pub fn builder(tool: impl Into<String>) -> ToolCallBuilder {
273        ToolCallBuilder::new(tool)
274    }
275}
276
277/// Builder for [`ToolCall`].
278#[derive(Debug, Default)]
279pub struct ToolCallBuilder {
280    tool: String,
281    arguments: serde_json::Map<String, Value>,
282}
283
284impl ToolCallBuilder {
285    /// Create a new tool call builder.
286    pub fn new(tool: impl Into<String>) -> Self {
287        Self {
288            tool: tool.into(),
289            arguments: serde_json::Map::new(),
290        }
291    }
292
293    /// Add an argument.
294    pub fn arg(mut self, name: impl Into<String>, value: Value) -> Self {
295        self.arguments.insert(name.into(), value);
296        self
297    }
298
299    /// Add a string argument.
300    pub fn arg_str(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
301        self.arguments
302            .insert(name.into(), Value::String(value.into()));
303        self
304    }
305
306    /// Add an integer argument.
307    pub fn arg_int(mut self, name: impl Into<String>, value: i64) -> Self {
308        self.arguments
309            .insert(name.into(), Value::Number(value.into()));
310        self
311    }
312
313    /// Add a boolean argument.
314    pub fn arg_bool(mut self, name: impl Into<String>, value: bool) -> Self {
315        self.arguments.insert(name.into(), Value::Bool(value));
316        self
317    }
318
319    /// Build the tool call.
320    pub fn build(self) -> ToolCall {
321        ToolCall {
322            tool: self.tool,
323            arguments: Value::Object(self.arguments),
324        }
325    }
326}
327
328/// The result of a tool execution.
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330pub struct ToolResult {
331    /// Whether the execution was successful.
332    pub success: bool,
333
334    /// The result data (if successful).
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub data: Option<Value>,
337
338    /// Error message (if failed).
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub error: Option<String>,
341
342    /// Execution duration in milliseconds.
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub duration_ms: Option<u64>,
345}
346
347impl ToolResult {
348    /// Create a successful result.
349    pub fn success(data: Value) -> Self {
350        Self {
351            success: true,
352            data: Some(data),
353            error: None,
354            duration_ms: None,
355        }
356    }
357
358    /// Create a failed result.
359    pub fn failure(error: impl Into<String>) -> Self {
360        Self {
361            success: false,
362            data: None,
363            error: Some(error.into()),
364            duration_ms: None,
365        }
366    }
367
368    /// Set the duration.
369    pub fn with_duration(mut self, duration_ms: u64) -> Self {
370        self.duration_ms = Some(duration_ms);
371        self
372    }
373
374    /// Check if the result is successful.
375    pub fn is_success(&self) -> bool {
376        self.success
377    }
378
379    /// Get the data, returning an error if the result failed.
380    pub fn into_data(self) -> Result<Value> {
381        if self.success {
382            self.data
383                .ok_or_else(|| Error::ExecutionFailed("no data returned".to_string()))
384        } else {
385            Err(Error::ExecutionFailed(
386                self.error.unwrap_or_else(|| "unknown error".to_string()),
387            ))
388        }
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::ParameterType;
396    use serde_json::json;
397
398    #[test]
399    fn tool_definition_new() {
400        let tool = ToolDefinition::new("test_tool");
401        assert_eq!(tool.name, "test_tool");
402        assert!(tool.description.is_empty());
403        assert!(tool.parameters.is_empty());
404    }
405
406    #[test]
407    fn tool_definition_builder() {
408        let tool = ToolDefinition::builder("read_file")
409            .description("Read a file")
410            .parameter(Parameter::required_string("path"))
411            .build();
412
413        assert_eq!(tool.name, "read_file");
414        assert_eq!(tool.description, "Read a file");
415        assert_eq!(tool.parameters.len(), 1);
416    }
417
418    #[test]
419    fn tool_definition_get_parameter() {
420        let tool = ToolDefinition::builder("test")
421            .parameter(Parameter::required_string("path"))
422            .parameter(Parameter::optional_string("encoding"))
423            .build();
424
425        assert!(tool.get_parameter("path").is_some());
426        assert!(tool.get_parameter("encoding").is_some());
427        assert!(tool.get_parameter("nonexistent").is_none());
428    }
429
430    #[test]
431    fn tool_definition_required_parameters() {
432        let tool = ToolDefinition::builder("test")
433            .parameter(Parameter::required_string("required1"))
434            .parameter(Parameter::optional_string("optional1"))
435            .parameter(Parameter::required_string("required2"))
436            .build();
437
438        let required: Vec<_> = tool.required_parameters().collect();
439        assert_eq!(required.len(), 2);
440        assert!(required.iter().any(|p| p.name == "required1"));
441        assert!(required.iter().any(|p| p.name == "required2"));
442    }
443
444    #[test]
445    fn tool_definition_validate_args_success() {
446        let tool = ToolDefinition::builder("test")
447            .parameter(Parameter::required_string("name"))
448            .parameter(
449                Parameter::builder("count")
450                    .param_type(ParameterType::Integer)
451                    .build(),
452            )
453            .build();
454
455        let args = json!({"name": "test", "count": 5});
456        assert!(tool.validate_args(&args).is_ok());
457    }
458
459    #[test]
460    fn tool_definition_validate_args_missing_required() {
461        let tool = ToolDefinition::builder("test")
462            .parameter(Parameter::required_string("name"))
463            .build();
464
465        let args = json!({});
466        let result = tool.validate_args(&args);
467        assert!(matches!(result, Err(Error::MissingParameter(_))));
468    }
469
470    #[test]
471    fn tool_definition_validate_args_with_default() {
472        let tool = ToolDefinition::builder("test")
473            .parameter(
474                Parameter::builder("count")
475                    .required(true)
476                    .default(json!(10))
477                    .build(),
478            )
479            .build();
480
481        let args = json!({});
482        assert!(tool.validate_args(&args).is_ok());
483    }
484
485    #[test]
486    fn tool_definition_validate_args_wrong_type() {
487        let tool = ToolDefinition::builder("test")
488            .parameter(
489                Parameter::builder("count")
490                    .param_type(ParameterType::Integer)
491                    .build(),
492            )
493            .build();
494
495        let args = json!({"count": "not a number"});
496        let result = tool.validate_args(&args);
497        assert!(matches!(result, Err(Error::InvalidParameterType { .. })));
498    }
499
500    #[test]
501    fn tool_definition_validate_args_enum() {
502        let tool = ToolDefinition::builder("test")
503            .parameter(
504                Parameter::builder("format")
505                    .enum_value(json!("json"))
506                    .enum_value(json!("yaml"))
507                    .build(),
508            )
509            .build();
510
511        assert!(tool.validate_args(&json!({"format": "json"})).is_ok());
512        assert!(tool.validate_args(&json!({"format": "yaml"})).is_ok());
513        assert!(tool.validate_args(&json!({"format": "toml"})).is_err());
514    }
515
516    #[test]
517    fn tool_call_new() {
518        let call = ToolCall::new("test_tool");
519        assert_eq!(call.tool, "test_tool");
520        assert!(call.arguments.is_object());
521    }
522
523    #[test]
524    fn tool_call_with_args() {
525        let args = json!({"path": "/tmp/test.txt"});
526        let call = ToolCall::with_args("read_file", args.clone());
527        assert_eq!(call.tool, "read_file");
528        assert_eq!(call.arguments, args);
529    }
530
531    #[test]
532    fn tool_call_builder() {
533        let call = ToolCall::builder("github.list_repos")
534            .arg_str("owner", "octocat")
535            .arg_int("per_page", 10)
536            .arg_bool("include_forks", false)
537            .build();
538
539        assert_eq!(call.tool, "github.list_repos");
540        assert_eq!(call.arguments["owner"], "octocat");
541        assert_eq!(call.arguments["per_page"], 10);
542        assert_eq!(call.arguments["include_forks"], false);
543    }
544
545    #[test]
546    fn tool_result_success() {
547        let result = ToolResult::success(json!({"status": "ok"}));
548        assert!(result.is_success());
549        assert!(result.data.is_some());
550        assert!(result.error.is_none());
551    }
552
553    #[test]
554    fn tool_result_failure() {
555        let result = ToolResult::failure("Something went wrong");
556        assert!(!result.is_success());
557        assert!(result.data.is_none());
558        assert_eq!(result.error, Some("Something went wrong".to_string()));
559    }
560
561    #[test]
562    fn tool_result_with_duration() {
563        let result = ToolResult::success(json!(null)).with_duration(250);
564        assert_eq!(result.duration_ms, Some(250));
565    }
566
567    #[test]
568    fn tool_result_into_data_success() {
569        let result = ToolResult::success(json!({"value": 42}));
570        let data = result.into_data().unwrap();
571        assert_eq!(data["value"], 42);
572    }
573
574    #[test]
575    fn tool_result_into_data_failure() {
576        let result = ToolResult::failure("error");
577        let err = result.into_data().unwrap_err();
578        assert!(matches!(err, Error::ExecutionFailed(_)));
579    }
580
581    #[test]
582    fn tool_definition_serialization() {
583        let tool = ToolDefinition::builder("test")
584            .description("A test tool")
585            .parameter(Parameter::required_string("name"))
586            .build();
587
588        let json = serde_json::to_string(&tool).unwrap();
589        let parsed: ToolDefinition = serde_json::from_str(&json).unwrap();
590
591        assert_eq!(tool, parsed);
592    }
593
594    #[test]
595    fn tool_definition_validate_args_edge_cases() {
596        let tool = ToolDefinition::builder("test")
597            .parameter(
598                Parameter::builder("array_param")
599                    .param_type(ParameterType::Array)
600                    .required(true)
601                    .build(),
602            )
603            .parameter(
604                Parameter::builder("object_param")
605                    .param_type(ParameterType::Object)
606                    .required(false)
607                    .default(json!({}))
608                    .build(),
609            )
610            .build();
611
612        // Valid array and object
613        assert!(tool
614            .validate_args(&json!({"array_param": [1, 2, 3], "object_param": {"key": "value"}}))
615            .is_ok());
616
617        // Valid with default for optional object
618        assert!(tool.validate_args(&json!({"array_param": []})).is_ok());
619
620        // Invalid array type (should be array, not object)
621        assert!(tool.validate_args(&json!({"array_param": {}})).is_err());
622
623        // Invalid object type (should be object, not string)
624        assert!(tool
625            .validate_args(&json!({"array_param": [], "object_param": "not an object"}))
626            .is_err());
627    }
628
629    #[test]
630    fn tool_definition_validate_args_with_all_types() {
631        let tool = ToolDefinition::builder("test")
632            .parameter(Parameter::required_string("str_param"))
633            .parameter(
634                Parameter::builder("int_param")
635                    .param_type(ParameterType::Integer)
636                    .required(true)
637                    .build(),
638            )
639            .parameter(
640                Parameter::builder("num_param")
641                    .param_type(ParameterType::Number)
642                    .required(true)
643                    .build(),
644            )
645            .parameter(
646                Parameter::builder("bool_param")
647                    .param_type(ParameterType::Boolean)
648                    .required(true)
649                    .build(),
650            )
651            .parameter(
652                Parameter::builder("arr_param")
653                    .param_type(ParameterType::Array)
654                    .required(true)
655                    .build(),
656            )
657            .parameter(
658                Parameter::builder("obj_param")
659                    .param_type(ParameterType::Object)
660                    .required(true)
661                    .build(),
662            )
663            .build();
664
665        let args = json!({
666            "str_param": "test",
667            "int_param": 42,
668            "num_param": 3.14,
669            "bool_param": true,
670            "arr_param": [1, 2, 3],
671            "obj_param": {"key": "value"}
672        });
673
674        assert!(tool.validate_args(&args).is_ok());
675
676        // Test each wrong type individually
677        assert!(tool.validate_args(&json!({"str_param": 42, "int_param": 42, "num_param": 3.14, "bool_param": true, "arr_param": [], "obj_param": {}})).is_err());
678        assert!(tool.validate_args(&json!({"str_param": "test", "int_param": "not int", "num_param": 3.14, "bool_param": true, "arr_param": [], "obj_param": {}})).is_err());
679    }
680
681    #[test]
682    fn tool_definition_validate_args_empty_required() {
683        let tool = ToolDefinition::builder("test")
684            .parameter(Parameter::required_string("param1"))
685            .parameter(Parameter::required_string("param2"))
686            .parameter(Parameter::required_string("param3"))
687            .build();
688
689        // All missing
690        assert!(tool.validate_args(&json!({})).is_err());
691
692        // Partial missing
693        assert!(tool.validate_args(&json!({"param1": "value"})).is_err());
694
695        // All present
696        assert!(tool
697            .validate_args(&json!({"param1": "v1", "param2": "v2", "param3": "v3"}))
698            .is_ok());
699    }
700
701    #[test]
702    fn tool_call_serialization() {
703        let call = ToolCall::builder("test").arg_str("name", "value").build();
704
705        let json = serde_json::to_string(&call).unwrap();
706        let parsed: ToolCall = serde_json::from_str(&json).unwrap();
707
708        assert_eq!(call, parsed);
709    }
710
711    #[test]
712    fn tool_result_serialization() {
713        let result = ToolResult::success(json!({"data": [1, 2, 3]})).with_duration(100);
714
715        let json = serde_json::to_string(&result).unwrap();
716        let parsed: ToolResult = serde_json::from_str(&json).unwrap();
717
718        assert_eq!(result, parsed);
719    }
720
721    #[test]
722    fn parse_mcp_input_schema_basic() {
723        let schema = json!({
724            "type": "object",
725            "properties": {
726                "name": {
727                    "type": "string",
728                    "description": "The name"
729                },
730                "age": {
731                    "type": "integer",
732                    "description": "The age"
733                }
734            },
735            "required": ["name"]
736        });
737
738        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
739        assert_eq!(params.len(), 2);
740
741        let name_param = params.iter().find(|p| p.name == "name").unwrap();
742        assert_eq!(name_param.param_type, ParameterType::String);
743        assert_eq!(name_param.description, "The name");
744        assert!(name_param.required);
745
746        let age_param = params.iter().find(|p| p.name == "age").unwrap();
747        assert_eq!(age_param.param_type, ParameterType::Integer);
748        assert_eq!(age_param.description, "The age");
749        assert!(!age_param.required);
750    }
751
752    #[test]
753    fn parse_mcp_input_schema_all_types() {
754        let schema = json!({
755            "type": "object",
756            "properties": {
757                "str": {"type": "string"},
758                "num": {"type": "number"},
759                "int": {"type": "integer"},
760                "bool": {"type": "boolean"},
761                "arr": {"type": "array"},
762                "obj": {"type": "object"}
763            }
764        });
765
766        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
767        assert_eq!(params.len(), 6);
768
769        assert_eq!(
770            params.iter().find(|p| p.name == "str").unwrap().param_type,
771            ParameterType::String
772        );
773        assert_eq!(
774            params.iter().find(|p| p.name == "num").unwrap().param_type,
775            ParameterType::Number
776        );
777        assert_eq!(
778            params.iter().find(|p| p.name == "int").unwrap().param_type,
779            ParameterType::Integer
780        );
781        assert_eq!(
782            params.iter().find(|p| p.name == "bool").unwrap().param_type,
783            ParameterType::Boolean
784        );
785        assert_eq!(
786            params.iter().find(|p| p.name == "arr").unwrap().param_type,
787            ParameterType::Array
788        );
789        assert_eq!(
790            params.iter().find(|p| p.name == "obj").unwrap().param_type,
791            ParameterType::Object
792        );
793    }
794
795    #[test]
796    fn parse_mcp_input_schema_empty() {
797        let schema = json!({});
798        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
799        assert_eq!(params.len(), 0);
800    }
801
802    #[test]
803    fn parse_mcp_input_schema_no_properties() {
804        let schema = json!({"type": "object"});
805        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
806        assert_eq!(params.len(), 0);
807    }
808
809    #[test]
810    fn parse_mcp_input_schema_unknown_type() {
811        let schema = json!({
812            "type": "object",
813            "properties": {
814                "unknown": {"type": "unknown_type"}
815            }
816        });
817
818        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
819        assert_eq!(params.len(), 1);
820        // Unknown types default to String
821        assert_eq!(params[0].param_type, ParameterType::String);
822    }
823
824    #[test]
825    fn parse_mcp_input_schema_missing_type() {
826        let schema = json!({
827            "type": "object",
828            "properties": {
829                "field": {"description": "A field without type"}
830            }
831        });
832
833        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
834        assert_eq!(params.len(), 1);
835        // Missing type defaults to String
836        assert_eq!(params[0].param_type, ParameterType::String);
837    }
838
839    #[test]
840    fn parse_mcp_input_schema_all_required() {
841        let schema = json!({
842            "type": "object",
843            "properties": {
844                "field1": {"type": "string"},
845                "field2": {"type": "string"},
846                "field3": {"type": "string"}
847            },
848            "required": ["field1", "field2", "field3"]
849        });
850
851        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
852        assert_eq!(params.len(), 3);
853        assert!(params.iter().all(|p| p.required));
854    }
855
856    #[test]
857    fn parse_mcp_input_schema_no_required() {
858        let schema = json!({
859            "type": "object",
860            "properties": {
861                "field1": {"type": "string"},
862                "field2": {"type": "string"}
863            }
864        });
865
866        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
867        assert_eq!(params.len(), 2);
868        assert!(params.iter().all(|p| !p.required));
869    }
870
871    #[test]
872    fn parse_mcp_input_schema_no_description() {
873        let schema = json!({
874            "type": "object",
875            "properties": {
876                "field": {"type": "string"}
877            }
878        });
879
880        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
881        assert_eq!(params.len(), 1);
882        assert_eq!(params[0].description, "");
883    }
884
885    #[test]
886    fn to_mcp_input_schema_basic() {
887        let def = ToolDefinition::builder("test_tool")
888            .description("A test tool")
889            .parameter(Parameter::required_string("path"))
890            .parameter(Parameter::optional_string("encoding"))
891            .build();
892        let schema = def.to_mcp_input_schema();
893        assert_eq!(schema["type"], "object");
894        assert_eq!(schema["properties"]["path"]["type"], "string");
895        assert_eq!(schema["properties"]["encoding"]["type"], "string");
896        let required = schema["required"].as_array().expect("required is array");
897        assert_eq!(required.len(), 1);
898        assert_eq!(required[0], "path");
899    }
900
901    #[test]
902    fn to_mcp_input_schema_round_trip() {
903        let original = ToolDefinition::builder("rt")
904            .description("round trip")
905            .parameter(Parameter::required_string("name"))
906            .parameter(Parameter::optional_string("note"))
907            .build();
908        let schema = original.to_mcp_input_schema();
909        let parsed = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
910        assert_eq!(parsed.len(), original.parameters.len());
911        for orig in &original.parameters {
912            let p = parsed
913                .iter()
914                .find(|p| p.name == orig.name)
915                .unwrap_or_else(|| panic!("missing param {}", orig.name));
916            assert_eq!(p.param_type, orig.param_type);
917            assert_eq!(p.required, orig.required);
918        }
919    }
920
921    #[test]
922    fn to_mcp_input_schema_carries_enum_and_default() {
923        let mut param = Parameter::new("level");
924        param.param_type = ParameterType::String;
925        param.required = false;
926        param.enum_values = vec![json!("low"), json!("med"), json!("high")];
927        param.default = Some(json!("med"));
928        let def = ToolDefinition::builder("with_enum").parameter(param).build();
929
930        let schema = def.to_mcp_input_schema();
931        let level = &schema["properties"]["level"];
932        assert_eq!(level["type"], "string");
933        assert_eq!(level["enum"][0], "low");
934        assert_eq!(level["default"], "med");
935    }
936
937    #[test]
938    fn to_mcp_input_schema_empty_definition_yields_empty_properties() {
939        let def = ToolDefinition::new("noargs");
940        let schema = def.to_mcp_input_schema();
941        assert_eq!(schema["type"], "object");
942        assert!(schema["properties"].as_object().unwrap().is_empty());
943        assert!(schema["required"].as_array().unwrap().is_empty());
944    }
945}