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