1use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ToolKind {
30 Read,
32 Edit,
34 Delete,
36 Move,
38 Search,
40 Execute,
42 Think,
44 Fetch,
46 #[default]
49 Other,
50}
51
52impl ToolKind {
53 pub const ALL: [Self; 9] = [
54 Self::Read,
55 Self::Edit,
56 Self::Delete,
57 Self::Move,
58 Self::Search,
59 Self::Execute,
60 Self::Think,
61 Self::Fetch,
62 Self::Other,
63 ];
64
65 pub fn is_read_only(&self) -> bool {
69 matches!(self, Self::Read | Self::Search | Self::Think | Self::Fetch)
70 }
71
72 pub fn mutation_class(&self) -> &'static str {
76 match self {
77 Self::Read | Self::Search | Self::Think | Self::Fetch => "read_only",
78 Self::Edit => "workspace_write",
79 Self::Delete | Self::Move => "destructive",
80 Self::Execute => "ambient_side_effect",
81 Self::Other => "other",
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum SideEffectLevel {
90 #[default]
93 None,
94 ReadOnly,
96 WorkspaceWrite,
98 ProcessExec,
100 Network,
102}
103
104impl SideEffectLevel {
105 pub const ALL: [Self; 5] = [
106 Self::None,
107 Self::ReadOnly,
108 Self::WorkspaceWrite,
109 Self::ProcessExec,
110 Self::Network,
111 ];
112
113 pub fn rank(&self) -> usize {
116 match self {
117 Self::None => 0,
118 Self::ReadOnly => 1,
119 Self::WorkspaceWrite => 2,
120 Self::ProcessExec => 3,
121 Self::Network => 4,
122 }
123 }
124
125 pub fn as_str(&self) -> &'static str {
128 match self {
129 Self::None => "none",
130 Self::ReadOnly => "read_only",
131 Self::WorkspaceWrite => "workspace_write",
132 Self::ProcessExec => "process_exec",
133 Self::Network => "network",
134 }
135 }
136
137 pub fn parse(value: &str) -> Self {
140 match value {
141 "none" => Self::None,
142 "read_only" => Self::ReadOnly,
143 "workspace_write" => Self::WorkspaceWrite,
144 "process_exec" => Self::ProcessExec,
145 "network" => Self::Network,
146 _ => Self::None,
147 }
148 }
149}
150
151#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
160#[serde(default)]
161pub struct ToolArgSchema {
162 pub path_params: Vec<String>,
165 pub arg_aliases: BTreeMap<String, String>,
169 pub required: Vec<String>,
171}
172
173#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
177#[serde(default)]
178pub struct ToolAnnotations {
179 pub kind: ToolKind,
181 pub side_effect_level: SideEffectLevel,
183 pub arg_schema: ToolArgSchema,
185 pub capabilities: BTreeMap<String, Vec<String>>,
188 pub emits_artifacts: bool,
192 pub result_readers: Vec<String>,
194 pub inline_result: bool,
197 #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
200 pub read_only_hint: Option<bool>,
201 #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
204 pub destructive_hint: Option<bool>,
205 #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
208 pub idempotent_hint: Option<bool>,
209 #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
212 pub open_world_hint: Option<bool>,
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn tool_kind_serde_roundtrip() {
221 for (kind, expected) in [
222 (ToolKind::Read, "\"read\""),
223 (ToolKind::Edit, "\"edit\""),
224 (ToolKind::Delete, "\"delete\""),
225 (ToolKind::Move, "\"move\""),
226 (ToolKind::Search, "\"search\""),
227 (ToolKind::Execute, "\"execute\""),
228 (ToolKind::Think, "\"think\""),
229 (ToolKind::Fetch, "\"fetch\""),
230 (ToolKind::Other, "\"other\""),
231 ] {
232 let encoded = serde_json::to_string(&kind).unwrap();
233 assert_eq!(encoded, expected);
234 let decoded: ToolKind = serde_json::from_str(expected).unwrap();
235 assert_eq!(decoded, kind);
236 }
237 }
238
239 #[test]
240 fn only_read_search_think_fetch_are_read_only() {
241 assert!(ToolKind::Read.is_read_only());
242 assert!(ToolKind::Search.is_read_only());
243 assert!(ToolKind::Think.is_read_only());
244 assert!(ToolKind::Fetch.is_read_only());
245 assert!(!ToolKind::Other.is_read_only());
247 assert!(!ToolKind::Edit.is_read_only());
248 assert!(!ToolKind::Delete.is_read_only());
249 assert!(!ToolKind::Move.is_read_only());
250 assert!(!ToolKind::Execute.is_read_only());
251 }
252
253 #[test]
254 fn mutation_class_derived_from_kind() {
255 assert_eq!(ToolKind::Read.mutation_class(), "read_only");
256 assert_eq!(ToolKind::Search.mutation_class(), "read_only");
257 assert_eq!(ToolKind::Edit.mutation_class(), "workspace_write");
258 assert_eq!(ToolKind::Delete.mutation_class(), "destructive");
259 assert_eq!(ToolKind::Move.mutation_class(), "destructive");
260 assert_eq!(ToolKind::Execute.mutation_class(), "ambient_side_effect");
261 assert_eq!(ToolKind::Other.mutation_class(), "other");
262 }
263
264 #[test]
265 fn side_effect_level_round_trip() {
266 for level in [
267 SideEffectLevel::None,
268 SideEffectLevel::ReadOnly,
269 SideEffectLevel::WorkspaceWrite,
270 SideEffectLevel::ProcessExec,
271 SideEffectLevel::Network,
272 ] {
273 assert_eq!(SideEffectLevel::parse(level.as_str()), level);
274 let encoded = serde_json::to_string(&level).unwrap();
275 let decoded: SideEffectLevel = serde_json::from_str(&encoded).unwrap();
276 assert_eq!(decoded, level);
277 }
278 }
279
280 #[test]
281 fn side_effect_level_rank_orders() {
282 assert!(SideEffectLevel::None.rank() < SideEffectLevel::ReadOnly.rank());
283 assert!(SideEffectLevel::ReadOnly.rank() < SideEffectLevel::WorkspaceWrite.rank());
284 assert!(SideEffectLevel::WorkspaceWrite.rank() < SideEffectLevel::ProcessExec.rank());
285 assert!(SideEffectLevel::ProcessExec.rank() < SideEffectLevel::Network.rank());
286 }
287
288 #[test]
289 fn arg_schema_defaults_empty() {
290 let schema = ToolArgSchema::default();
291 assert!(schema.path_params.is_empty());
292 assert!(schema.arg_aliases.is_empty());
293 assert!(schema.required.is_empty());
294 }
295
296 #[test]
297 fn annotations_default_result_routes_empty() {
298 let annotations = ToolAnnotations::default();
299 assert!(!annotations.emits_artifacts);
300 assert!(annotations.result_readers.is_empty());
301 assert!(!annotations.inline_result);
302 }
303
304 #[test]
305 fn mcp_annotation_hints_round_trip() {
306 let annotations: ToolAnnotations = serde_json::from_value(serde_json::json!({
307 "readOnlyHint": true,
308 "destructiveHint": false,
309 "idempotentHint": true,
310 "openWorldHint": false
311 }))
312 .expect("MCP hints should deserialize");
313 assert_eq!(annotations.read_only_hint, Some(true));
314 assert_eq!(annotations.destructive_hint, Some(false));
315 assert_eq!(annotations.idempotent_hint, Some(true));
316 assert_eq!(annotations.open_world_hint, Some(false));
317
318 let encoded = serde_json::to_value(&annotations).expect("serialize annotations");
319 assert_eq!(encoded["readOnlyHint"], true);
320 assert_eq!(encoded["idempotentHint"], true);
321 }
322}