turul_mcp_builders/
tool.rs

1//! Tool Builder for Runtime Tool Construction
2//!
3//! This module provides a builder pattern for creating tools at runtime
4//! without requiring procedural macros. This is Level 3 of the tool creation spectrum.
5
6use serde_json::Value;
7use std::collections::HashMap;
8use std::future::Future;
9use std::pin::Pin;
10
11// Import traits from local traits module
12use crate::traits::{
13    HasAnnotations, HasBaseMetadata, HasDescription, HasInputSchema, HasOutputSchema, HasToolMeta,
14};
15// Import protocol types
16use turul_mcp_protocol::schema::JsonSchema;
17use turul_mcp_protocol::tools::{ToolAnnotations, ToolSchema};
18
19/// Type alias for dynamic tool execution function
20pub type DynamicToolFn =
21    Box<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> + Send + Sync>;
22
23/// Builder for creating tools at runtime
24pub 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    /// Create a new tool builder with the given name
37    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    /// Set the tool title (display name)
51    pub fn title(mut self, title: impl Into<String>) -> Self {
52        self.title = Some(title.into());
53        self
54    }
55
56    /// Set the tool description
57    pub fn description(mut self, description: impl Into<String>) -> Self {
58        self.description = Some(description.into());
59        self
60    }
61
62    /// Add a parameter to the input schema
63    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            // Store JsonSchema directly
67            properties.insert(param_name, schema);
68        } else {
69            // Store JsonSchema directly
70            self.input_schema.properties = Some(HashMap::from([(param_name, schema)]));
71        }
72        self
73    }
74
75    /// Add a required parameter
76    pub fn required_param<T: Into<String>>(mut self, name: T, schema: JsonSchema) -> Self {
77        let param_name = name.into();
78        self = self.param(&param_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    /// Add a string parameter
88    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    /// Add a number parameter
93    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    /// Add an integer parameter
98    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    /// Add a boolean parameter
103    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    /// Set the output schema
108    pub fn output_schema(mut self, schema: ToolSchema) -> Self {
109        self.output_schema = Some(schema);
110        self
111    }
112
113    /// Set the output schema to expect a number result
114    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    /// Set the output schema to expect a string result
127    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    /// Set annotations
140    pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
141        self.annotations = Some(annotations);
142        self
143    }
144
145    /// Set meta information
146    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
147        self.meta = Some(meta);
148        self
149    }
150
151    /// Set the execution function
152    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    /// Build the dynamic tool
162    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
178/// Dynamic tool created by ToolBuilder
179pub 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    /// Execute the tool with the given arguments
192    pub async fn execute(&self, args: Value) -> Result<Value, String> {
193        (self.execute_fn)(args).await
194    }
195}
196
197// Implement all fine-grained traits for DynamicTool
198/// Implements HasBaseMetadata for DynamicTool providing name and title access
199impl 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
209/// Implements HasDescription for DynamicTool providing description text access
210impl HasDescription for DynamicTool {
211    fn description(&self) -> Option<&str> {
212        self.description.as_deref()
213    }
214}
215
216/// Implements HasInputSchema for DynamicTool providing parameter schema access
217impl HasInputSchema for DynamicTool {
218    fn input_schema(&self) -> &ToolSchema {
219        &self.input_schema
220    }
221}
222
223/// Implements HasOutputSchema for DynamicTool providing result schema access
224impl HasOutputSchema for DynamicTool {
225    fn output_schema(&self) -> Option<&ToolSchema> {
226        self.output_schema.as_ref()
227    }
228}
229
230/// Implements HasAnnotations for DynamicTool providing metadata annotations
231impl HasAnnotations for DynamicTool {
232    fn annotations(&self) -> Option<&ToolAnnotations> {
233        self.annotations.as_ref()
234    }
235}
236
237/// Implements HasToolMeta for DynamicTool providing additional metadata fields
238impl HasToolMeta for DynamicTool {
239    fn tool_meta(&self) -> Option<&HashMap<String, Value>> {
240        self.meta.as_ref()
241    }
242}
243
244// ToolDefinition is automatically implemented via blanket impl!
245
246// Note: McpTool implementation will be provided by the turul-mcp-server crate
247// since it depends on types from that crate (SessionContext, etc.)
248
249#[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        // Verify schema was generated correctly
296        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}