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