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 from protocol via alias
12use 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
18/// Type alias for dynamic tool execution function
19pub type DynamicToolFn =
20    Box<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> + Send + Sync>;
21
22/// Builder for creating tools at runtime
23pub 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    /// Create a new tool builder with the given name
36    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    /// Set the tool title (display name)
50    pub fn title(mut self, title: impl Into<String>) -> Self {
51        self.title = Some(title.into());
52        self
53    }
54
55    /// Set the tool description
56    pub fn description(mut self, description: impl Into<String>) -> Self {
57        self.description = Some(description.into());
58        self
59    }
60
61    /// Add a parameter to the input schema
62    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            // Store JsonSchema directly
66            properties.insert(param_name, schema);
67        } else {
68            // Store JsonSchema directly
69            self.input_schema.properties = Some(HashMap::from([(param_name, schema)]));
70        }
71        self
72    }
73
74    /// Add a required parameter
75    pub fn required_param<T: Into<String>>(mut self, name: T, schema: JsonSchema) -> Self {
76        let param_name = name.into();
77        self = self.param(&param_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    /// Add a string parameter
87    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    /// Add a number parameter
92    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    /// Add an integer parameter
97    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    /// Add a boolean parameter
102    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    /// Set the output schema
107    pub fn output_schema(mut self, schema: ToolSchema) -> Self {
108        self.output_schema = Some(schema);
109        self
110    }
111
112    /// Set the output schema to expect a number result
113    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    /// Set the output schema to expect a string result
126    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    /// Set annotations
139    pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
140        self.annotations = Some(annotations);
141        self
142    }
143
144    /// Set meta information
145    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
146        self.meta = Some(meta);
147        self
148    }
149
150    /// Set the execution function
151    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    /// Build the dynamic tool
161    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
177/// Dynamic tool created by ToolBuilder
178pub 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    /// Execute the tool with the given arguments
191    pub async fn execute(&self, args: Value) -> Result<Value, String> {
192        (self.execute_fn)(args).await
193    }
194}
195
196// Implement all fine-grained traits for DynamicTool
197/// Implements HasBaseMetadata for DynamicTool providing name and title access
198impl 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
208/// Implements HasDescription for DynamicTool providing description text access
209impl HasDescription for DynamicTool {
210    fn description(&self) -> Option<&str> {
211        self.description.as_deref()
212    }
213}
214
215/// Implements HasInputSchema for DynamicTool providing parameter schema access
216impl HasInputSchema for DynamicTool {
217    fn input_schema(&self) -> &ToolSchema {
218        &self.input_schema
219    }
220}
221
222/// Implements HasOutputSchema for DynamicTool providing result schema access
223impl HasOutputSchema for DynamicTool {
224    fn output_schema(&self) -> Option<&ToolSchema> {
225        self.output_schema.as_ref()
226    }
227}
228
229/// Implements HasAnnotations for DynamicTool providing metadata annotations
230impl HasAnnotations for DynamicTool {
231    fn annotations(&self) -> Option<&ToolAnnotations> {
232        self.annotations.as_ref()
233    }
234}
235
236/// Implements HasToolMeta for DynamicTool providing additional metadata fields
237impl HasToolMeta for DynamicTool {
238    fn tool_meta(&self) -> Option<&HashMap<String, Value>> {
239        self.meta.as_ref()
240    }
241}
242
243// ToolDefinition is automatically implemented via blanket impl!
244
245// Note: McpTool implementation will be provided by the turul-mcp-server crate
246// since it depends on types from that crate (SessionContext, etc.)
247
248#[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        // Verify schema was generated correctly
295        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}