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/// Typed content block from an MCP tool result.
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330#[serde(tag = "type")]
331pub enum ContentBlock {
332    /// Text content.
333    #[serde(rename = "text")]
334    Text { text: String },
335    /// Base64-encoded image content.
336    #[serde(rename = "image")]
337    Image { data: String, mime_type: String },
338    /// Embedded resource content.
339    #[serde(rename = "resource")]
340    Resource { uri: String, text: Option<String> },
341}
342
343impl ContentBlock {
344    pub fn text(s: impl Into<String>) -> Self {
345        Self::Text { text: s.into() }
346    }
347
348    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
349        Self::Image {
350            data: data.into(),
351            mime_type: mime_type.into(),
352        }
353    }
354
355    pub fn from_value(v: &Value) -> Option<Self> {
356        match v.get("type").and_then(|t| t.as_str()) {
357            Some("text") => Some(Self::Text {
358                text: v.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string(),
359            }),
360            Some("image") => Some(Self::Image {
361                data: v.get("data").and_then(|d| d.as_str()).unwrap_or("").to_string(),
362                mime_type: v.get("mimeType").and_then(|m| m.as_str()).unwrap_or("image/png").to_string(),
363            }),
364            Some("resource") => {
365                let res = v.get("resource").unwrap_or(v);
366                Some(Self::Resource {
367                    uri: res.get("uri").and_then(|u| u.as_str()).unwrap_or("").to_string(),
368                    text: res.get("text").and_then(|t| t.as_str()).map(String::from),
369                })
370            }
371            _ => None,
372        }
373    }
374
375    pub fn as_text(&self) -> Option<&str> {
376        match self {
377            Self::Text { text } => Some(text),
378            _ => None,
379        }
380    }
381}
382
383/// The result of a tool execution.
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct ToolResult {
386    /// Whether the execution was successful.
387    pub success: bool,
388
389    /// The result data (if successful).
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub data: Option<Value>,
392
393    /// Error message (if failed).
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub error: Option<String>,
396
397    /// Execution duration in milliseconds.
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub duration_ms: Option<u64>,
400}
401
402impl ToolResult {
403    /// Create a successful result.
404    pub fn success(data: Value) -> Self {
405        Self {
406            success: true,
407            data: Some(data),
408            error: None,
409            duration_ms: None,
410        }
411    }
412
413    /// Create a failed result.
414    pub fn failure(error: impl Into<String>) -> Self {
415        Self {
416            success: false,
417            data: None,
418            error: Some(error.into()),
419            duration_ms: None,
420        }
421    }
422
423    /// Set the duration.
424    pub fn with_duration(mut self, duration_ms: u64) -> Self {
425        self.duration_ms = Some(duration_ms);
426        self
427    }
428
429    /// Create a failed result (alias for `failure`; matches rmcp naming).
430    pub fn error(message: impl Into<String>) -> Self {
431        Self::failure(message)
432    }
433
434    /// Check if the result is successful.
435    pub fn is_success(&self) -> bool {
436        self.success
437    }
438
439    /// Check if the result is an error.
440    pub fn is_error(&self) -> bool {
441        !self.success
442    }
443
444    /// Extract typed content blocks from the result data.
445    pub fn content_blocks(&self) -> Vec<ContentBlock> {
446        let Some(data) = &self.data else {
447            return vec![];
448        };
449        if let Some(arr) = data.get("content").and_then(|v| v.as_array()) {
450            arr.iter().filter_map(ContentBlock::from_value).collect()
451        } else if let Some(text) = data.as_str() {
452            vec![ContentBlock::Text {
453                text: text.to_string(),
454            }]
455        } else if let Some(text) = data.get("text").and_then(|v| v.as_str()) {
456            vec![ContentBlock::Text {
457                text: text.to_string(),
458            }]
459        } else {
460            vec![]
461        }
462    }
463
464    /// Extract all text content concatenated.
465    pub fn text_content(&self) -> String {
466        self.content_blocks()
467            .iter()
468            .filter_map(|b| match b {
469                ContentBlock::Text { text } => Some(text.as_str()),
470                _ => None,
471            })
472            .collect::<Vec<_>>()
473            .join("\n")
474    }
475
476    /// Get the data, returning an error if the result failed.
477    pub fn into_data(self) -> Result<Value> {
478        if self.success {
479            self.data
480                .ok_or_else(|| Error::ExecutionFailed("no data returned".to_string()))
481        } else {
482            Err(Error::ExecutionFailed(
483                self.error.unwrap_or_else(|| "unknown error".to_string()),
484            ))
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::ParameterType;
493    use serde_json::json;
494
495    #[test]
496    fn tool_definition_new() {
497        let tool = ToolDefinition::new("test_tool");
498        assert_eq!(tool.name, "test_tool");
499        assert!(tool.description.is_empty());
500        assert!(tool.parameters.is_empty());
501    }
502
503    #[test]
504    fn tool_definition_builder() {
505        let tool = ToolDefinition::builder("read_file")
506            .description("Read a file")
507            .parameter(Parameter::required_string("path"))
508            .build();
509
510        assert_eq!(tool.name, "read_file");
511        assert_eq!(tool.description, "Read a file");
512        assert_eq!(tool.parameters.len(), 1);
513    }
514
515    #[test]
516    fn tool_definition_get_parameter() {
517        let tool = ToolDefinition::builder("test")
518            .parameter(Parameter::required_string("path"))
519            .parameter(Parameter::optional_string("encoding"))
520            .build();
521
522        assert!(tool.get_parameter("path").is_some());
523        assert!(tool.get_parameter("encoding").is_some());
524        assert!(tool.get_parameter("nonexistent").is_none());
525    }
526
527    #[test]
528    fn tool_definition_required_parameters() {
529        let tool = ToolDefinition::builder("test")
530            .parameter(Parameter::required_string("required1"))
531            .parameter(Parameter::optional_string("optional1"))
532            .parameter(Parameter::required_string("required2"))
533            .build();
534
535        let required: Vec<_> = tool.required_parameters().collect();
536        assert_eq!(required.len(), 2);
537        assert!(required.iter().any(|p| p.name == "required1"));
538        assert!(required.iter().any(|p| p.name == "required2"));
539    }
540
541    #[test]
542    fn tool_definition_validate_args_success() {
543        let tool = ToolDefinition::builder("test")
544            .parameter(Parameter::required_string("name"))
545            .parameter(
546                Parameter::builder("count")
547                    .param_type(ParameterType::Integer)
548                    .build(),
549            )
550            .build();
551
552        let args = json!({"name": "test", "count": 5});
553        assert!(tool.validate_args(&args).is_ok());
554    }
555
556    #[test]
557    fn tool_definition_validate_args_missing_required() {
558        let tool = ToolDefinition::builder("test")
559            .parameter(Parameter::required_string("name"))
560            .build();
561
562        let args = json!({});
563        let result = tool.validate_args(&args);
564        assert!(matches!(result, Err(Error::MissingParameter(_))));
565    }
566
567    #[test]
568    fn tool_definition_validate_args_with_default() {
569        let tool = ToolDefinition::builder("test")
570            .parameter(
571                Parameter::builder("count")
572                    .required(true)
573                    .default(json!(10))
574                    .build(),
575            )
576            .build();
577
578        let args = json!({});
579        assert!(tool.validate_args(&args).is_ok());
580    }
581
582    #[test]
583    fn tool_definition_validate_args_wrong_type() {
584        let tool = ToolDefinition::builder("test")
585            .parameter(
586                Parameter::builder("count")
587                    .param_type(ParameterType::Integer)
588                    .build(),
589            )
590            .build();
591
592        let args = json!({"count": "not a number"});
593        let result = tool.validate_args(&args);
594        assert!(matches!(result, Err(Error::InvalidParameterType { .. })));
595    }
596
597    #[test]
598    fn tool_definition_validate_args_enum() {
599        let tool = ToolDefinition::builder("test")
600            .parameter(
601                Parameter::builder("format")
602                    .enum_value(json!("json"))
603                    .enum_value(json!("yaml"))
604                    .build(),
605            )
606            .build();
607
608        assert!(tool.validate_args(&json!({"format": "json"})).is_ok());
609        assert!(tool.validate_args(&json!({"format": "yaml"})).is_ok());
610        assert!(tool.validate_args(&json!({"format": "toml"})).is_err());
611    }
612
613    #[test]
614    fn tool_call_new() {
615        let call = ToolCall::new("test_tool");
616        assert_eq!(call.tool, "test_tool");
617        assert!(call.arguments.is_object());
618    }
619
620    #[test]
621    fn tool_call_with_args() {
622        let args = json!({"path": "/tmp/test.txt"});
623        let call = ToolCall::with_args("read_file", args.clone());
624        assert_eq!(call.tool, "read_file");
625        assert_eq!(call.arguments, args);
626    }
627
628    #[test]
629    fn tool_call_builder() {
630        let call = ToolCall::builder("github.list_repos")
631            .arg_str("owner", "octocat")
632            .arg_int("per_page", 10)
633            .arg_bool("include_forks", false)
634            .build();
635
636        assert_eq!(call.tool, "github.list_repos");
637        assert_eq!(call.arguments["owner"], "octocat");
638        assert_eq!(call.arguments["per_page"], 10);
639        assert_eq!(call.arguments["include_forks"], false);
640    }
641
642    #[test]
643    fn tool_result_success() {
644        let result = ToolResult::success(json!({"status": "ok"}));
645        assert!(result.is_success());
646        assert!(result.data.is_some());
647        assert!(result.error.is_none());
648    }
649
650    #[test]
651    fn tool_result_failure() {
652        let result = ToolResult::failure("Something went wrong");
653        assert!(!result.is_success());
654        assert!(result.data.is_none());
655        assert_eq!(result.error, Some("Something went wrong".to_string()));
656    }
657
658    #[test]
659    fn tool_result_with_duration() {
660        let result = ToolResult::success(json!(null)).with_duration(250);
661        assert_eq!(result.duration_ms, Some(250));
662    }
663
664    #[test]
665    fn tool_result_into_data_success() {
666        let result = ToolResult::success(json!({"value": 42}));
667        let data = result.into_data().unwrap();
668        assert_eq!(data["value"], 42);
669    }
670
671    #[test]
672    fn tool_result_into_data_failure() {
673        let result = ToolResult::failure("error");
674        let err = result.into_data().unwrap_err();
675        assert!(matches!(err, Error::ExecutionFailed(_)));
676    }
677
678    #[test]
679    fn tool_definition_serialization() {
680        let tool = ToolDefinition::builder("test")
681            .description("A test tool")
682            .parameter(Parameter::required_string("name"))
683            .build();
684
685        let json = serde_json::to_string(&tool).unwrap();
686        let parsed: ToolDefinition = serde_json::from_str(&json).unwrap();
687
688        assert_eq!(tool, parsed);
689    }
690
691    #[test]
692    fn tool_definition_validate_args_edge_cases() {
693        let tool = ToolDefinition::builder("test")
694            .parameter(
695                Parameter::builder("array_param")
696                    .param_type(ParameterType::Array)
697                    .required(true)
698                    .build(),
699            )
700            .parameter(
701                Parameter::builder("object_param")
702                    .param_type(ParameterType::Object)
703                    .required(false)
704                    .default(json!({}))
705                    .build(),
706            )
707            .build();
708
709        // Valid array and object
710        assert!(tool
711            .validate_args(&json!({"array_param": [1, 2, 3], "object_param": {"key": "value"}}))
712            .is_ok());
713
714        // Valid with default for optional object
715        assert!(tool.validate_args(&json!({"array_param": []})).is_ok());
716
717        // Invalid array type (should be array, not object)
718        assert!(tool.validate_args(&json!({"array_param": {}})).is_err());
719
720        // Invalid object type (should be object, not string)
721        assert!(tool
722            .validate_args(&json!({"array_param": [], "object_param": "not an object"}))
723            .is_err());
724    }
725
726    #[test]
727    fn tool_definition_validate_args_with_all_types() {
728        let tool = ToolDefinition::builder("test")
729            .parameter(Parameter::required_string("str_param"))
730            .parameter(
731                Parameter::builder("int_param")
732                    .param_type(ParameterType::Integer)
733                    .required(true)
734                    .build(),
735            )
736            .parameter(
737                Parameter::builder("num_param")
738                    .param_type(ParameterType::Number)
739                    .required(true)
740                    .build(),
741            )
742            .parameter(
743                Parameter::builder("bool_param")
744                    .param_type(ParameterType::Boolean)
745                    .required(true)
746                    .build(),
747            )
748            .parameter(
749                Parameter::builder("arr_param")
750                    .param_type(ParameterType::Array)
751                    .required(true)
752                    .build(),
753            )
754            .parameter(
755                Parameter::builder("obj_param")
756                    .param_type(ParameterType::Object)
757                    .required(true)
758                    .build(),
759            )
760            .build();
761
762        let args = json!({
763            "str_param": "test",
764            "int_param": 42,
765            "num_param": 3.14,
766            "bool_param": true,
767            "arr_param": [1, 2, 3],
768            "obj_param": {"key": "value"}
769        });
770
771        assert!(tool.validate_args(&args).is_ok());
772
773        // Test each wrong type individually
774        assert!(tool.validate_args(&json!({"str_param": 42, "int_param": 42, "num_param": 3.14, "bool_param": true, "arr_param": [], "obj_param": {}})).is_err());
775        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());
776    }
777
778    #[test]
779    fn tool_definition_validate_args_empty_required() {
780        let tool = ToolDefinition::builder("test")
781            .parameter(Parameter::required_string("param1"))
782            .parameter(Parameter::required_string("param2"))
783            .parameter(Parameter::required_string("param3"))
784            .build();
785
786        // All missing
787        assert!(tool.validate_args(&json!({})).is_err());
788
789        // Partial missing
790        assert!(tool.validate_args(&json!({"param1": "value"})).is_err());
791
792        // All present
793        assert!(tool
794            .validate_args(&json!({"param1": "v1", "param2": "v2", "param3": "v3"}))
795            .is_ok());
796    }
797
798    #[test]
799    fn tool_call_serialization() {
800        let call = ToolCall::builder("test").arg_str("name", "value").build();
801
802        let json = serde_json::to_string(&call).unwrap();
803        let parsed: ToolCall = serde_json::from_str(&json).unwrap();
804
805        assert_eq!(call, parsed);
806    }
807
808    #[test]
809    fn tool_result_serialization() {
810        let result = ToolResult::success(json!({"data": [1, 2, 3]})).with_duration(100);
811
812        let json = serde_json::to_string(&result).unwrap();
813        let parsed: ToolResult = serde_json::from_str(&json).unwrap();
814
815        assert_eq!(result, parsed);
816    }
817
818    #[test]
819    fn parse_mcp_input_schema_basic() {
820        let schema = json!({
821            "type": "object",
822            "properties": {
823                "name": {
824                    "type": "string",
825                    "description": "The name"
826                },
827                "age": {
828                    "type": "integer",
829                    "description": "The age"
830                }
831            },
832            "required": ["name"]
833        });
834
835        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
836        assert_eq!(params.len(), 2);
837
838        let name_param = params.iter().find(|p| p.name == "name").unwrap();
839        assert_eq!(name_param.param_type, ParameterType::String);
840        assert_eq!(name_param.description, "The name");
841        assert!(name_param.required);
842
843        let age_param = params.iter().find(|p| p.name == "age").unwrap();
844        assert_eq!(age_param.param_type, ParameterType::Integer);
845        assert_eq!(age_param.description, "The age");
846        assert!(!age_param.required);
847    }
848
849    #[test]
850    fn parse_mcp_input_schema_all_types() {
851        let schema = json!({
852            "type": "object",
853            "properties": {
854                "str": {"type": "string"},
855                "num": {"type": "number"},
856                "int": {"type": "integer"},
857                "bool": {"type": "boolean"},
858                "arr": {"type": "array"},
859                "obj": {"type": "object"}
860            }
861        });
862
863        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
864        assert_eq!(params.len(), 6);
865
866        assert_eq!(
867            params.iter().find(|p| p.name == "str").unwrap().param_type,
868            ParameterType::String
869        );
870        assert_eq!(
871            params.iter().find(|p| p.name == "num").unwrap().param_type,
872            ParameterType::Number
873        );
874        assert_eq!(
875            params.iter().find(|p| p.name == "int").unwrap().param_type,
876            ParameterType::Integer
877        );
878        assert_eq!(
879            params.iter().find(|p| p.name == "bool").unwrap().param_type,
880            ParameterType::Boolean
881        );
882        assert_eq!(
883            params.iter().find(|p| p.name == "arr").unwrap().param_type,
884            ParameterType::Array
885        );
886        assert_eq!(
887            params.iter().find(|p| p.name == "obj").unwrap().param_type,
888            ParameterType::Object
889        );
890    }
891
892    #[test]
893    fn parse_mcp_input_schema_empty() {
894        let schema = json!({});
895        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
896        assert_eq!(params.len(), 0);
897    }
898
899    #[test]
900    fn parse_mcp_input_schema_no_properties() {
901        let schema = json!({"type": "object"});
902        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
903        assert_eq!(params.len(), 0);
904    }
905
906    #[test]
907    fn parse_mcp_input_schema_unknown_type() {
908        let schema = json!({
909            "type": "object",
910            "properties": {
911                "unknown": {"type": "unknown_type"}
912            }
913        });
914
915        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
916        assert_eq!(params.len(), 1);
917        // Unknown types default to String
918        assert_eq!(params[0].param_type, ParameterType::String);
919    }
920
921    #[test]
922    fn parse_mcp_input_schema_missing_type() {
923        let schema = json!({
924            "type": "object",
925            "properties": {
926                "field": {"description": "A field without type"}
927            }
928        });
929
930        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
931        assert_eq!(params.len(), 1);
932        // Missing type defaults to String
933        assert_eq!(params[0].param_type, ParameterType::String);
934    }
935
936    #[test]
937    fn parse_mcp_input_schema_all_required() {
938        let schema = json!({
939            "type": "object",
940            "properties": {
941                "field1": {"type": "string"},
942                "field2": {"type": "string"},
943                "field3": {"type": "string"}
944            },
945            "required": ["field1", "field2", "field3"]
946        });
947
948        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
949        assert_eq!(params.len(), 3);
950        assert!(params.iter().all(|p| p.required));
951    }
952
953    #[test]
954    fn parse_mcp_input_schema_no_required() {
955        let schema = json!({
956            "type": "object",
957            "properties": {
958                "field1": {"type": "string"},
959                "field2": {"type": "string"}
960            }
961        });
962
963        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
964        assert_eq!(params.len(), 2);
965        assert!(params.iter().all(|p| !p.required));
966    }
967
968    #[test]
969    fn parse_mcp_input_schema_no_description() {
970        let schema = json!({
971            "type": "object",
972            "properties": {
973                "field": {"type": "string"}
974            }
975        });
976
977        let params = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
978        assert_eq!(params.len(), 1);
979        assert_eq!(params[0].description, "");
980    }
981
982    #[test]
983    fn to_mcp_input_schema_basic() {
984        let def = ToolDefinition::builder("test_tool")
985            .description("A test tool")
986            .parameter(Parameter::required_string("path"))
987            .parameter(Parameter::optional_string("encoding"))
988            .build();
989        let schema = def.to_mcp_input_schema();
990        assert_eq!(schema["type"], "object");
991        assert_eq!(schema["properties"]["path"]["type"], "string");
992        assert_eq!(schema["properties"]["encoding"]["type"], "string");
993        let required = schema["required"].as_array().expect("required is array");
994        assert_eq!(required.len(), 1);
995        assert_eq!(required[0], "path");
996    }
997
998    #[test]
999    fn to_mcp_input_schema_round_trip() {
1000        let original = ToolDefinition::builder("rt")
1001            .description("round trip")
1002            .parameter(Parameter::required_string("name"))
1003            .parameter(Parameter::optional_string("note"))
1004            .build();
1005        let schema = original.to_mcp_input_schema();
1006        let parsed = ToolDefinition::parse_mcp_input_schema(&schema).unwrap();
1007        assert_eq!(parsed.len(), original.parameters.len());
1008        for orig in &original.parameters {
1009            let p = parsed
1010                .iter()
1011                .find(|p| p.name == orig.name)
1012                .unwrap_or_else(|| panic!("missing param {}", orig.name));
1013            assert_eq!(p.param_type, orig.param_type);
1014            assert_eq!(p.required, orig.required);
1015        }
1016    }
1017
1018    #[test]
1019    fn to_mcp_input_schema_carries_enum_and_default() {
1020        let mut param = Parameter::new("level");
1021        param.param_type = ParameterType::String;
1022        param.required = false;
1023        param.enum_values = vec![json!("low"), json!("med"), json!("high")];
1024        param.default = Some(json!("med"));
1025        let def = ToolDefinition::builder("with_enum").parameter(param).build();
1026
1027        let schema = def.to_mcp_input_schema();
1028        let level = &schema["properties"]["level"];
1029        assert_eq!(level["type"], "string");
1030        assert_eq!(level["enum"][0], "low");
1031        assert_eq!(level["default"], "med");
1032    }
1033
1034    #[test]
1035    fn to_mcp_input_schema_empty_definition_yields_empty_properties() {
1036        let def = ToolDefinition::new("noargs");
1037        let schema = def.to_mcp_input_schema();
1038        assert_eq!(schema["type"], "object");
1039        assert!(schema["properties"].as_object().unwrap().is_empty());
1040        assert!(schema["required"].as_array().unwrap().is_empty());
1041    }
1042}