1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct ToolSpec {
17 pub name: String,
20
21 pub description: String,
26
27 #[serde(rename = "inputSchema", alias = "input_schema")]
32 pub input_schema: serde_json::Value,
33}
34
35impl ToolSpec {
36 pub fn new(
39 name: impl Into<String>,
40 description: impl Into<String>,
41 input_schema: serde_json::Value,
42 ) -> Self {
43 Self {
44 name: name.into(),
45 description: description.into(),
46 input_schema,
47 }
48 }
49
50 pub fn no_args(name: impl Into<String>, description: impl Into<String>) -> Self {
52 Self::new(
53 name,
54 description,
55 serde_json::json!({"type": "object", "properties": {}}),
56 )
57 }
58
59 pub fn required_args(&self) -> Vec<&str> {
61 self.input_schema
62 .get("required")
63 .and_then(|r| r.as_array())
64 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
65 .unwrap_or_default()
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
75 fn the_wire_form_is_mcp_camel_case() {
76 let spec = ToolSpec::new(
77 "search",
78 "Search the web. Call this when the answer depends on current information.",
79 serde_json::json!({"type": "object", "required": ["q"]}),
80 );
81
82 let json = serde_json::to_value(&spec).unwrap();
83 assert!(json.get("inputSchema").is_some(), "{json}");
84 assert!(json.get("input_schema").is_none(), "{json}");
85
86 let snake: ToolSpec = serde_json::from_value(serde_json::json!({
88 "name": "search",
89 "description": "d",
90 "input_schema": {"type": "object"}
91 }))
92 .unwrap();
93 assert_eq!(snake.name, "search");
94
95 assert_eq!(
96 serde_json::from_value::<ToolSpec>(json).unwrap(),
97 spec,
98 "the wire form should round-trip"
99 );
100 }
101
102 #[test]
103 fn required_arguments_are_readable() {
104 let spec = ToolSpec::new(
105 "f",
106 "d",
107 serde_json::json!({"type": "object", "required": ["a", "b"]}),
108 );
109 assert_eq!(spec.required_args(), vec!["a", "b"]);
110 assert!(ToolSpec::no_args("g", "d").required_args().is_empty());
111 }
112}