1use serde_json::Value;
7use std::collections::HashMap;
8use std::future::Future;
9use std::pin::Pin;
10
11use crate::traits::{
13 HasAnnotations, HasBaseMetadata, HasDescription, HasInputSchema, HasOutputSchema, HasToolMeta,
14};
15use turul_mcp_protocol::schema::JsonSchema;
17use turul_mcp_protocol::tools::{ToolAnnotations, ToolSchema};
18
19pub type DynamicToolFn =
21 Box<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> + Send + Sync>;
22
23pub struct ToolBuilder {
25 name: String,
26 title: Option<String>,
27 description: Option<String>,
28 input_schema: ToolSchema,
29 output_schema: Option<ToolSchema>,
30 annotations: Option<ToolAnnotations>,
31 meta: Option<HashMap<String, Value>>,
32 execute_fn: Option<DynamicToolFn>,
33}
34
35impl ToolBuilder {
36 pub fn new(name: impl Into<String>) -> Self {
38 Self {
39 name: name.into(),
40 title: None,
41 description: None,
42 input_schema: ToolSchema::object(),
43 output_schema: None,
44 annotations: None,
45 meta: None,
46 execute_fn: None,
47 }
48 }
49
50 pub fn title(mut self, title: impl Into<String>) -> Self {
52 self.title = Some(title.into());
53 self
54 }
55
56 pub fn description(mut self, description: impl Into<String>) -> Self {
58 self.description = Some(description.into());
59 self
60 }
61
62 pub fn param<T: Into<String>>(mut self, name: T, schema: JsonSchema) -> Self {
64 let param_name = name.into();
65 if let Some(properties) = &mut self.input_schema.properties {
66 properties.insert(param_name, schema);
68 } else {
69 self.input_schema.properties = Some(HashMap::from([(param_name, schema)]));
71 }
72 self
73 }
74
75 pub fn required_param<T: Into<String>>(mut self, name: T, schema: JsonSchema) -> Self {
77 let param_name = name.into();
78 self = self.param(¶m_name, schema);
79 if let Some(required) = &mut self.input_schema.required {
80 required.push(param_name);
81 } else {
82 self.input_schema.required = Some(vec![param_name]);
83 }
84 self
85 }
86
87 pub fn string_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
89 self.required_param(name, JsonSchema::string().with_description(description))
90 }
91
92 pub fn number_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
94 self.required_param(name, JsonSchema::number().with_description(description))
95 }
96
97 pub fn integer_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
99 self.required_param(name, JsonSchema::integer().with_description(description))
100 }
101
102 pub fn boolean_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
104 self.required_param(name, JsonSchema::boolean().with_description(description))
105 }
106
107 pub fn output_schema(mut self, schema: ToolSchema) -> Self {
109 self.output_schema = Some(schema);
110 self
111 }
112
113 pub fn number_output(mut self) -> Self {
115 self.output_schema = Some(
116 ToolSchema::object()
117 .with_properties(HashMap::from([(
118 "result".to_string(),
119 JsonSchema::number(),
120 )]))
121 .with_required(vec!["result".to_string()]),
122 );
123 self
124 }
125
126 pub fn string_output(mut self) -> Self {
128 self.output_schema = Some(
129 ToolSchema::object()
130 .with_properties(HashMap::from([(
131 "result".to_string(),
132 JsonSchema::string(),
133 )]))
134 .with_required(vec!["result".to_string()]),
135 );
136 self
137 }
138
139 pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
141 self.annotations = Some(annotations);
142 self
143 }
144
145 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
147 self.meta = Some(meta);
148 self
149 }
150
151 pub fn execute<F, Fut>(mut self, f: F) -> Self
153 where
154 F: Fn(Value) -> Fut + Send + Sync + 'static,
155 Fut: Future<Output = Result<Value, String>> + Send + 'static,
156 {
157 self.execute_fn = Some(Box::new(move |args| Box::pin(f(args))));
158 self
159 }
160
161 pub fn build(self) -> Result<DynamicTool, String> {
163 let execute_fn = self.execute_fn.ok_or("Execution function is required")?;
164
165 Ok(DynamicTool {
166 name: self.name,
167 title: self.title,
168 description: self.description,
169 input_schema: self.input_schema,
170 output_schema: self.output_schema,
171 annotations: self.annotations,
172 meta: self.meta,
173 execute_fn,
174 })
175 }
176}
177
178pub struct DynamicTool {
180 name: String,
181 title: Option<String>,
182 description: Option<String>,
183 input_schema: ToolSchema,
184 output_schema: Option<ToolSchema>,
185 annotations: Option<ToolAnnotations>,
186 meta: Option<HashMap<String, Value>>,
187 execute_fn: DynamicToolFn,
188}
189
190impl DynamicTool {
191 pub async fn execute(&self, args: Value) -> Result<Value, String> {
193 (self.execute_fn)(args).await
194 }
195}
196
197impl HasBaseMetadata for DynamicTool {
200 fn name(&self) -> &str {
201 &self.name
202 }
203
204 fn title(&self) -> Option<&str> {
205 self.title.as_deref()
206 }
207}
208
209impl HasDescription for DynamicTool {
211 fn description(&self) -> Option<&str> {
212 self.description.as_deref()
213 }
214}
215
216impl HasInputSchema for DynamicTool {
218 fn input_schema(&self) -> &ToolSchema {
219 &self.input_schema
220 }
221}
222
223impl HasOutputSchema for DynamicTool {
225 fn output_schema(&self) -> Option<&ToolSchema> {
226 self.output_schema.as_ref()
227 }
228}
229
230impl HasAnnotations for DynamicTool {
232 fn annotations(&self) -> Option<&ToolAnnotations> {
233 self.annotations.as_ref()
234 }
235}
236
237impl HasToolMeta for DynamicTool {
239 fn tool_meta(&self) -> Option<&HashMap<String, Value>> {
240 self.meta.as_ref()
241 }
242}
243
244#[cfg(test)]
250mod tests {
251 use super::*;
252 use serde_json::json;
253
254 #[tokio::test]
255 async fn test_tool_builder_basic() {
256 let tool = ToolBuilder::new("test_tool")
257 .description("A test tool")
258 .string_param("input", "Test input parameter")
259 .execute(|args| async move {
260 let input = args
261 .get("input")
262 .and_then(|v| v.as_str())
263 .ok_or("Missing input parameter")?;
264 Ok(json!({"result": format!("Hello, {}", input)}))
265 })
266 .build()
267 .expect("Failed to build tool");
268
269 assert_eq!(tool.name(), "test_tool");
270 assert_eq!(tool.description(), Some("A test tool"));
271
272 let result = tool
273 .execute(json!({"input": "World"}))
274 .await
275 .expect("Tool execution failed");
276
277 assert_eq!(result, json!({"result": "Hello, World"}));
278 }
279
280 #[test]
281 fn test_tool_builder_schema_generation() {
282 let tool = ToolBuilder::new("calculator")
283 .description("Simple calculator")
284 .number_param("a", "First number")
285 .number_param("b", "Second number")
286 .number_output()
287 .execute(|args| async move {
288 let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
289 let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
290 Ok(json!({"result": a + b}))
291 })
292 .build()
293 .expect("Failed to build calculator tool");
294
295 let input_schema = tool.input_schema();
297 assert!(input_schema.properties.is_some());
298 assert_eq!(input_schema.required.as_ref().unwrap().len(), 2);
299
300 let output_schema = tool.output_schema();
301 assert!(output_schema.is_some());
302 }
303}