Skip to main content

kaish_types/
tool.rs

1//! Tool schema and argument types.
2
3use std::collections::{HashMap, HashSet};
4
5use crate::value::Value;
6
7/// Schema for a tool parameter.
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9pub struct ParamSchema {
10    /// Parameter name.
11    pub name: String,
12    /// Type hint (string, int, bool, array, object, any).
13    pub param_type: String,
14    /// Whether this parameter is required.
15    pub required: bool,
16    /// Default value if not required.
17    pub default: Option<Value>,
18    /// Description for help text.
19    pub description: String,
20    /// Alternative names/flags for this parameter (e.g., "-r", "-R" for "recursive").
21    pub aliases: Vec<String>,
22}
23
24impl ParamSchema {
25    /// Create a required parameter.
26    pub fn required(name: impl Into<String>, param_type: impl Into<String>, description: impl Into<String>) -> Self {
27        Self {
28            name: name.into(),
29            param_type: param_type.into(),
30            required: true,
31            default: None,
32            description: description.into(),
33            aliases: Vec::new(),
34        }
35    }
36
37    /// Create an optional parameter with a default value.
38    pub fn optional(name: impl Into<String>, param_type: impl Into<String>, default: Value, description: impl Into<String>) -> Self {
39        Self {
40            name: name.into(),
41            param_type: param_type.into(),
42            required: false,
43            default: Some(default),
44            description: description.into(),
45            aliases: Vec::new(),
46        }
47    }
48
49    /// Add alternative names/flags for this parameter.
50    ///
51    /// Aliases are used for short flags like `-r`, `-R` that map to `recursive`.
52    pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
53        self.aliases = aliases.into_iter().map(Into::into).collect();
54        self
55    }
56
57    /// Check if a flag name matches this parameter or any of its aliases.
58    pub fn matches_flag(&self, flag: &str) -> bool {
59        if self.name == flag {
60            return true;
61        }
62        self.aliases.iter().any(|a| a == flag)
63    }
64}
65
66/// An example showing how to use a tool.
67#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
68pub struct Example {
69    /// Short description of what the example demonstrates.
70    pub description: String,
71    /// The example command/code.
72    pub code: String,
73}
74
75impl Example {
76    /// Create a new example.
77    pub fn new(description: impl Into<String>, code: impl Into<String>) -> Self {
78        Self {
79            description: description.into(),
80            code: code.into(),
81        }
82    }
83}
84
85/// Schema describing a tool's interface.
86#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
87pub struct ToolSchema {
88    /// Tool name.
89    pub name: String,
90    /// Short description.
91    pub description: String,
92    /// Parameter definitions.
93    pub params: Vec<ParamSchema>,
94    /// Usage examples.
95    pub examples: Vec<Example>,
96    /// Map remaining positional args to named params by schema order.
97    /// Only for MCP/external tools that expect named JSON params.
98    /// Builtins handle their own positionals and should leave this false.
99    pub map_positionals: bool,
100}
101
102impl ToolSchema {
103    /// Create a new tool schema.
104    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
105        Self {
106            name: name.into(),
107            description: description.into(),
108            params: Vec::new(),
109            examples: Vec::new(),
110            map_positionals: false,
111        }
112    }
113
114    /// Enable positional->named parameter mapping for MCP/external tools.
115    pub fn with_positional_mapping(mut self) -> Self {
116        self.map_positionals = true;
117        self
118    }
119
120    /// Add a parameter to the schema.
121    pub fn param(mut self, param: ParamSchema) -> Self {
122        self.params.push(param);
123        self
124    }
125
126    /// Add an example to the schema.
127    pub fn example(mut self, description: impl Into<String>, code: impl Into<String>) -> Self {
128        self.examples.push(Example::new(description, code));
129        self
130    }
131}
132
133/// Parsed arguments ready for tool execution.
134#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
135pub struct ToolArgs {
136    /// Positional arguments in order.
137    pub positional: Vec<Value>,
138    /// Named arguments by key.
139    pub named: HashMap<String, Value>,
140    /// Boolean flags (e.g., -l, --force).
141    pub flags: HashSet<String>,
142}
143
144impl ToolArgs {
145    /// Create empty args.
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Get a positional argument by index.
151    pub fn get_positional(&self, index: usize) -> Option<&Value> {
152        self.positional.get(index)
153    }
154
155    /// Get a named argument by key.
156    pub fn get_named(&self, key: &str) -> Option<&Value> {
157        self.named.get(key)
158    }
159
160    /// Get a named argument or positional fallback.
161    ///
162    /// Useful for tools that accept both `cat file.txt` and `cat path=file.txt`.
163    pub fn get(&self, name: &str, positional_index: usize) -> Option<&Value> {
164        self.named.get(name).or_else(|| self.positional.get(positional_index))
165    }
166
167    /// Get a string value from args.
168    pub fn get_string(&self, name: &str, positional_index: usize) -> Option<String> {
169        self.get(name, positional_index).and_then(|v| match v {
170            Value::String(s) => Some(s.clone()),
171            Value::Int(i) => Some(i.to_string()),
172            Value::Float(f) => Some(f.to_string()),
173            Value::Bool(b) => Some(b.to_string()),
174            _ => None,
175        })
176    }
177
178    /// Get a boolean value from args.
179    pub fn get_bool(&self, name: &str, positional_index: usize) -> Option<bool> {
180        self.get(name, positional_index).and_then(|v| match v {
181            Value::Bool(b) => Some(*b),
182            Value::String(s) => match s.as_str() {
183                "true" | "yes" | "1" => Some(true),
184                "false" | "no" | "0" => Some(false),
185                _ => None,
186            },
187            Value::Int(i) => Some(*i != 0),
188            _ => None,
189        })
190    }
191
192    /// Check if a flag is set (in flags set, or named bool).
193    pub fn has_flag(&self, name: &str) -> bool {
194        // Check the flags set first (from -x or --name syntax)
195        if self.flags.contains(name) {
196            return true;
197        }
198        // Fall back to checking named args (from name=true syntax)
199        self.named.get(name).is_some_and(|v| match v {
200            Value::Bool(b) => *b,
201            Value::String(s) => !s.is_empty() && s != "false" && s != "0",
202            _ => true,
203        })
204    }
205}