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