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 execution: None,
182 title: metadata.title.clone(),
183 icons: None,
184 meta: None,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193
194 fn create_test_metadata(method: &str) -> ToolMetadata {
196 ToolMetadata {
197 name: "test_tool".to_string(),
198 title: None,
199 description: None,
200 parameters: json!({}),
201 output_schema: None,
202 method: method.to_string(),
203 path: "/test".to_string(),
204 security: None,
205 parameter_mappings: HashMap::new(),
206 }
207 }
208
209 #[test]
210 fn test_get_annotations() {
211 let metadata = create_test_metadata("GET");
212 let annotations = metadata
213 .generate_annotations()
214 .expect("GET should return annotations");
215
216 assert_eq!(annotations.title, None);
217 assert_eq!(annotations.read_only_hint, Some(true));
218 assert_eq!(annotations.destructive_hint, Some(false));
219 assert_eq!(annotations.idempotent_hint, Some(true));
220 assert_eq!(annotations.open_world_hint, Some(true));
221 }
222
223 #[test]
224 fn test_post_annotations() {
225 let metadata = create_test_metadata("POST");
226 let annotations = metadata
227 .generate_annotations()
228 .expect("POST should return annotations");
229
230 assert_eq!(annotations.title, None);
231 assert_eq!(annotations.read_only_hint, Some(false));
232 assert_eq!(annotations.destructive_hint, Some(false));
233 assert_eq!(annotations.idempotent_hint, Some(false));
234 assert_eq!(annotations.open_world_hint, Some(true));
235 }
236
237 #[test]
238 fn test_put_annotations() {
239 let metadata = create_test_metadata("PUT");
240 let annotations = metadata
241 .generate_annotations()
242 .expect("PUT should return annotations");
243
244 assert_eq!(annotations.title, None);
245 assert_eq!(annotations.read_only_hint, Some(false));
246 assert_eq!(annotations.destructive_hint, Some(true));
247 assert_eq!(annotations.idempotent_hint, Some(true));
248 assert_eq!(annotations.open_world_hint, Some(true));
249 }
250
251 #[test]
252 fn test_patch_annotations() {
253 let metadata = create_test_metadata("PATCH");
254 let annotations = metadata
255 .generate_annotations()
256 .expect("PATCH should return annotations");
257
258 assert_eq!(annotations.title, None);
259 assert_eq!(annotations.read_only_hint, Some(false));
260 assert_eq!(annotations.destructive_hint, Some(true));
261 assert_eq!(annotations.idempotent_hint, Some(false));
262 assert_eq!(annotations.open_world_hint, Some(true));
263 }
264
265 #[test]
266 fn test_delete_annotations() {
267 let metadata = create_test_metadata("DELETE");
268 let annotations = metadata
269 .generate_annotations()
270 .expect("DELETE should return annotations");
271
272 assert_eq!(annotations.title, None);
273 assert_eq!(annotations.read_only_hint, Some(false));
274 assert_eq!(annotations.destructive_hint, Some(true));
275 assert_eq!(annotations.idempotent_hint, Some(true));
276 assert_eq!(annotations.open_world_hint, Some(true));
277 }
278
279 #[test]
280 fn test_head_annotations() {
281 let metadata = create_test_metadata("HEAD");
282 let annotations = metadata
283 .generate_annotations()
284 .expect("HEAD should return annotations");
285
286 assert_eq!(annotations.title, None);
288 assert_eq!(annotations.read_only_hint, Some(true));
289 assert_eq!(annotations.destructive_hint, Some(false));
290 assert_eq!(annotations.idempotent_hint, Some(true));
291 assert_eq!(annotations.open_world_hint, Some(true));
292 }
293
294 #[test]
295 fn test_options_annotations() {
296 let metadata = create_test_metadata("OPTIONS");
297 let annotations = metadata
298 .generate_annotations()
299 .expect("OPTIONS should return annotations");
300
301 assert_eq!(annotations.title, None);
303 assert_eq!(annotations.read_only_hint, Some(true));
304 assert_eq!(annotations.destructive_hint, Some(false));
305 assert_eq!(annotations.idempotent_hint, Some(true));
306 assert_eq!(annotations.open_world_hint, Some(true));
307 }
308
309 #[test]
310 fn test_unknown_method_returns_none() {
311 let unknown_methods = vec!["TRACE", "CONNECT", "CUSTOM", "INVALID", "UNKNOWN"];
313
314 for method in unknown_methods {
315 let metadata = create_test_metadata(method);
316 let annotations = metadata.generate_annotations();
317 assert_eq!(
318 annotations, None,
319 "Unknown method '{}' should return None",
320 method
321 );
322 }
323 }
324
325 #[test]
326 fn test_case_insensitive_method_matching() {
327 let get_variations = vec!["GET", "get", "Get", "gEt", "GeT"];
329
330 for method in get_variations {
331 let metadata = create_test_metadata(method);
332 let annotations = metadata
333 .generate_annotations()
334 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
335
336 assert_eq!(annotations.read_only_hint, Some(true));
338 assert_eq!(annotations.destructive_hint, Some(false));
339 assert_eq!(annotations.idempotent_hint, Some(true));
340 assert_eq!(annotations.open_world_hint, Some(true));
341 }
342
343 let post_variations = vec!["POST", "post", "Post"];
345
346 for method in post_variations {
347 let metadata = create_test_metadata(method);
348 let annotations = metadata
349 .generate_annotations()
350 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
351
352 assert_eq!(annotations.read_only_hint, Some(false));
354 assert_eq!(annotations.destructive_hint, Some(false));
355 assert_eq!(annotations.idempotent_hint, Some(false));
356 assert_eq!(annotations.open_world_hint, Some(true));
357 }
358 }
359
360 #[test]
361 fn test_annotations_title_always_none() {
362 let all_methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
364
365 for method in all_methods {
366 let metadata = create_test_metadata(method);
367 let annotations = metadata
368 .generate_annotations()
369 .unwrap_or_else(|| panic!("'{}' should return annotations", method));
370
371 assert_eq!(
372 annotations.title, None,
373 "Method '{}' should have title=None in annotations",
374 method
375 );
376 }
377 }
378}