1use crate::{Error, Parameter, ParameterType, Result};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ToolDefinition {
10 pub name: String,
12
13 #[serde(default)]
15 pub description: String,
16
17 #[serde(default)]
19 pub parameters: Vec<Parameter>,
20}
21
22impl ToolDefinition {
23 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 pub fn builder(name: impl Into<String>) -> ToolDefinitionBuilder {
34 ToolDefinitionBuilder::new(name)
35 }
36
37 pub fn get_parameter(&self, name: &str) -> Option<&Parameter> {
39 self.parameters.iter().find(|p| p.name == name)
40 }
41
42 pub fn required_parameters(&self) -> impl Iterator<Item = &Parameter> {
44 self.parameters.iter().filter(|p| p.required)
45 }
46
47 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 for param in self.required_parameters() {
54 if !args_obj.contains_key(¶m.name) {
55 if param.default.is_none() {
57 return Err(Error::MissingParameter(param.name.clone()));
58 }
59 }
60 }
61
62 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 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 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) = ¶m.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 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
186fn 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#[derive(Debug, Default)]
200pub struct ToolDefinitionBuilder {
201 name: String,
202 description: String,
203 parameters: Vec<Parameter>,
204}
205
206impl ToolDefinitionBuilder {
207 pub fn new(name: impl Into<String>) -> Self {
209 Self {
210 name: name.into(),
211 ..Default::default()
212 }
213 }
214
215 pub fn description(mut self, description: impl Into<String>) -> Self {
217 self.description = description.into();
218 self
219 }
220
221 pub fn parameter(mut self, parameter: Parameter) -> Self {
223 self.parameters.push(parameter);
224 self
225 }
226
227 pub fn parameters(mut self, parameters: impl IntoIterator<Item = Parameter>) -> Self {
229 self.parameters.extend(parameters);
230 self
231 }
232
233 pub fn build(self) -> ToolDefinition {
235 ToolDefinition {
236 name: self.name,
237 description: self.description,
238 parameters: self.parameters,
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct ToolCall {
246 pub tool: String,
248
249 #[serde(default)]
251 pub arguments: Value,
252}
253
254impl ToolCall {
255 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 pub fn with_args(tool: impl Into<String>, arguments: Value) -> Self {
265 Self {
266 tool: tool.into(),
267 arguments,
268 }
269 }
270
271 pub fn builder(tool: impl Into<String>) -> ToolCallBuilder {
273 ToolCallBuilder::new(tool)
274 }
275}
276
277#[derive(Debug, Default)]
279pub struct ToolCallBuilder {
280 tool: String,
281 arguments: serde_json::Map<String, Value>,
282}
283
284impl ToolCallBuilder {
285 pub fn new(tool: impl Into<String>) -> Self {
287 Self {
288 tool: tool.into(),
289 arguments: serde_json::Map::new(),
290 }
291 }
292
293 pub fn arg(mut self, name: impl Into<String>, value: Value) -> Self {
295 self.arguments.insert(name.into(), value);
296 self
297 }
298
299 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 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 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 pub fn build(self) -> ToolCall {
321 ToolCall {
322 tool: self.tool,
323 arguments: Value::Object(self.arguments),
324 }
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330#[serde(tag = "type")]
331pub enum ContentBlock {
332 #[serde(rename = "text")]
334 Text { text: String },
335 #[serde(rename = "image")]
337 Image { data: String, mime_type: String },
338 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct ToolResult {
386 pub success: bool,
388
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub data: Option<Value>,
392
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub error: Option<String>,
396
397 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub duration_ms: Option<u64>,
400}
401
402impl ToolResult {
403 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 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 pub fn with_duration(mut self, duration_ms: u64) -> Self {
425 self.duration_ms = Some(duration_ms);
426 self
427 }
428
429 pub fn error(message: impl Into<String>) -> Self {
431 Self::failure(message)
432 }
433
434 pub fn is_success(&self) -> bool {
436 self.success
437 }
438
439 pub fn is_error(&self) -> bool {
441 !self.success
442 }
443
444 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 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 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 assert!(tool
711 .validate_args(&json!({"array_param": [1, 2, 3], "object_param": {"key": "value"}}))
712 .is_ok());
713
714 assert!(tool.validate_args(&json!({"array_param": []})).is_ok());
716
717 assert!(tool.validate_args(&json!({"array_param": {}})).is_err());
719
720 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 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 assert!(tool.validate_args(&json!({})).is_err());
788
789 assert!(tool.validate_args(&json!({"param1": "value"})).is_err());
791
792 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 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 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}