1use rmcp::model::{Tool, ToolAnnotations};
2use serde_json::Value;
3use std::{collections::HashMap, sync::Arc};
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
7pub struct ParameterMapping {
8 pub sanitized_name: String,
10 pub original_name: String,
12 pub location: String,
14 pub explode: bool,
16}
17
18#[derive(Debug, Clone, serde::Serialize)]
33pub struct ToolMetadata {
34 pub name: String,
36 pub title: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub description: Option<String>,
41 pub parameters: Value,
43 pub output_schema: Option<Value>,
45 pub method: String,
47 pub path: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub security: Option<Vec<String>>,
52 #[serde(skip_serializing_if = "HashMap::is_empty")]
54 pub parameter_mappings: HashMap<String, ParameterMapping>,
55}
56
57impl ToolMetadata {
58 pub fn requires_auth(&self) -> bool {
60 self.security.as_ref().is_some_and(|s| !s.is_empty())
61 }
62
63 pub fn generate_annotations(&self) -> Option<ToolAnnotations> {
112 match self.method.to_uppercase().as_str() {
113 "GET" | "HEAD" | "OPTIONS" => Some(ToolAnnotations {
114 title: None,
115 read_only_hint: Some(true),
116 destructive_hint: Some(false),
117 idempotent_hint: Some(true),
118 open_world_hint: Some(true),
119 }),
120 "POST" => Some(ToolAnnotations {
121 title: None,
122 read_only_hint: Some(false),
123 destructive_hint: Some(false),
124 idempotent_hint: Some(false),
125 open_world_hint: Some(true),
126 }),
127 "PUT" => Some(ToolAnnotations {
128 title: None,
129 read_only_hint: Some(false),
130 destructive_hint: Some(true),
131 idempotent_hint: Some(true),
132 open_world_hint: Some(true),
133 }),
134 "PATCH" => Some(ToolAnnotations {
135 title: None,
136 read_only_hint: Some(false),
137 destructive_hint: Some(true),
138 idempotent_hint: Some(false),
139 open_world_hint: Some(true),
140 }),
141 "DELETE" => Some(ToolAnnotations {
142 title: None,
143 read_only_hint: Some(false),
144 destructive_hint: Some(true),
145 idempotent_hint: Some(true),
146 open_world_hint: Some(true),
147 }),
148 _ => None,
149 }
150 }
151}
152
153impl From<&ToolMetadata> for Tool {
158 fn from(metadata: &ToolMetadata) -> Self {
159 let input_schema = if let Value::Object(obj) = &metadata.parameters {
161 Arc::new(obj.clone())
162 } else {
163 Arc::new(serde_json::Map::new())
164 };
165
166 let output_schema = metadata.output_schema.as_ref().and_then(|schema| {
168 if let Value::Object(obj) = schema {
169 Some(Arc::new(obj.clone()))
170 } else {
171 None
172 }
173 });
174
175 Tool {
176 name: metadata.name.clone().into(),
177 description: metadata.description.clone().map(|d| d.into()),
178 input_schema,
179 output_schema,
180 annotations: metadata.generate_annotations(),
181 title: metadata.title.clone(),
182 icons: None,
183 meta: None,
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use serde_json::json;
192
193 fn create_test_metadata(method: &str) -> ToolMetadata {
195 ToolMetadata {
196 name: "test_tool".to_string(),
197 title: None,
198 description: None,
199 parameters: json!({}),
200 output_schema: None,
201 method: method.to_string(),
202 path: "/test".to_string(),
203 security: None,
204 parameter_mappings: HashMap::new(),
205 }
206 }
207
208 #[test]
209 fn test_get_annotations() {
210 let metadata = create_test_metadata("GET");
211 let annotations = metadata
212 .generate_annotations()
213 .expect("GET should return annotations");
214
215 assert_eq!(annotations.title, None);
216 assert_eq!(annotations.read_only_hint, Some(true));
217 assert_eq!(annotations.destructive_hint, Some(false));
218 assert_eq!(annotations.idempotent_hint, Some(true));
219 assert_eq!(annotations.open_world_hint, Some(true));
220 }
221
222 #[test]
223 fn test_post_annotations() {
224 let metadata = create_test_metadata("POST");
225 let annotations = metadata
226 .generate_annotations()
227 .expect("POST should return annotations");
228
229 assert_eq!(annotations.title, None);
230 assert_eq!(annotations.read_only_hint, Some(false));
231 assert_eq!(annotations.destructive_hint, Some(false));
232 assert_eq!(annotations.idempotent_hint, Some(false));
233 assert_eq!(annotations.open_world_hint, Some(true));
234 }
235
236 #[test]
237 fn test_put_annotations() {
238 let metadata = create_test_metadata("PUT");
239 let annotations = metadata
240 .generate_annotations()
241 .expect("PUT should return annotations");
242
243 assert_eq!(annotations.title, None);
244 assert_eq!(annotations.read_only_hint, Some(false));
245 assert_eq!(annotations.destructive_hint, Some(true));
246 assert_eq!(annotations.idempotent_hint, Some(true));
247 assert_eq!(annotations.open_world_hint, Some(true));
248 }
249
250 #[test]
251 fn test_patch_annotations() {
252 let metadata = create_test_metadata("PATCH");
253 let annotations = metadata
254 .generate_annotations()
255 .expect("PATCH should return annotations");
256
257 assert_eq!(annotations.title, None);
258 assert_eq!(annotations.read_only_hint, Some(false));
259 assert_eq!(annotations.destructive_hint, Some(true));
260 assert_eq!(annotations.idempotent_hint, Some(false));
261 assert_eq!(annotations.open_world_hint, Some(true));
262 }
263
264 #[test]
265 fn test_delete_annotations() {
266 let metadata = create_test_metadata("DELETE");
267 let annotations = metadata
268 .generate_annotations()
269 .expect("DELETE should return annotations");
270
271 assert_eq!(annotations.title, None);
272 assert_eq!(annotations.read_only_hint, Some(false));
273 assert_eq!(annotations.destructive_hint, Some(true));
274 assert_eq!(annotations.idempotent_hint, Some(true));
275 assert_eq!(annotations.open_world_hint, Some(true));
276 }
277
278 #[test]
279 fn test_head_annotations() {
280 let metadata = create_test_metadata("HEAD");
281 let annotations = metadata
282 .generate_annotations()
283 .expect("HEAD should return annotations");
284
285 assert_eq!(annotations.title, None);
287 assert_eq!(annotations.read_only_hint, Some(true));
288 assert_eq!(annotations.destructive_hint, Some(false));
289 assert_eq!(annotations.idempotent_hint, Some(true));
290 assert_eq!(annotations.open_world_hint, Some(true));
291 }
292
293 #[test]
294 fn test_options_annotations() {
295 let metadata = create_test_metadata("OPTIONS");
296 let annotations = metadata
297 .generate_annotations()
298 .expect("OPTIONS should return annotations");
299
300 assert_eq!(annotations.title, None);
302 assert_eq!(annotations.read_only_hint, Some(true));
303 assert_eq!(annotations.destructive_hint, Some(false));
304 assert_eq!(annotations.idempotent_hint, Some(true));
305 assert_eq!(annotations.open_world_hint, Some(true));
306 }
307
308 #[test]
309 fn test_unknown_method_returns_none() {
310 let unknown_methods = vec!["TRACE", "CONNECT", "CUSTOM", "INVALID", "UNKNOWN"];
312
313 for method in unknown_methods {
314 let metadata = create_test_metadata(method);
315 let annotations = metadata.generate_annotations();
316 assert_eq!(
317 annotations, None,
318 "Unknown method '{}' should return None",
319 method
320 );
321 }
322 }
323
324 #[test]
325 fn test_case_insensitive_method_matching() {
326 let get_variations = vec!["GET", "get", "Get", "gEt", "GeT"];
328
329 for method in get_variations {
330 let metadata = create_test_metadata(method);
331 let annotations = metadata
332 .generate_annotations()
333 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
334
335 assert_eq!(annotations.read_only_hint, Some(true));
337 assert_eq!(annotations.destructive_hint, Some(false));
338 assert_eq!(annotations.idempotent_hint, Some(true));
339 assert_eq!(annotations.open_world_hint, Some(true));
340 }
341
342 let post_variations = vec!["POST", "post", "Post"];
344
345 for method in post_variations {
346 let metadata = create_test_metadata(method);
347 let annotations = metadata
348 .generate_annotations()
349 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
350
351 assert_eq!(annotations.read_only_hint, Some(false));
353 assert_eq!(annotations.destructive_hint, Some(false));
354 assert_eq!(annotations.idempotent_hint, Some(false));
355 assert_eq!(annotations.open_world_hint, Some(true));
356 }
357 }
358
359 #[test]
360 fn test_annotations_title_always_none() {
361 let all_methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
363
364 for method in all_methods {
365 let metadata = create_test_metadata(method);
366 let annotations = metadata
367 .generate_annotations()
368 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
369
370 assert_eq!(
371 annotations.title, None,
372 "Method '{}' should have title=None in annotations",
373 method
374 );
375 }
376 }
377}