use rmcp::model::{Tool, ToolAnnotations};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct ParameterMapping {
pub sanitized_name: String,
pub original_name: String,
pub location: String,
pub explode: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ToolMetadata {
pub name: String,
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: Value,
pub output_schema: Option<Value>,
pub method: String,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<Vec<String>>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub parameter_mappings: HashMap<String, ParameterMapping>,
}
impl ToolMetadata {
pub fn requires_auth(&self) -> bool {
self.security.as_ref().is_some_and(|s| !s.is_empty())
}
pub fn generate_annotations(&self) -> Option<ToolAnnotations> {
match self.method.to_uppercase().as_str() {
"GET" | "HEAD" | "OPTIONS" => Some(
ToolAnnotations::new()
.read_only(true)
.destructive(false)
.idempotent(true)
.open_world(true),
),
"POST" => Some(
ToolAnnotations::new()
.read_only(false)
.destructive(false)
.idempotent(false)
.open_world(true),
),
"PUT" => Some(
ToolAnnotations::new()
.read_only(false)
.destructive(true)
.idempotent(true)
.open_world(true),
),
"PATCH" => Some(
ToolAnnotations::new()
.read_only(false)
.destructive(true)
.idempotent(false)
.open_world(true),
),
"DELETE" => Some(
ToolAnnotations::new()
.read_only(false)
.destructive(true)
.idempotent(true)
.open_world(true),
),
_ => None,
}
}
}
impl From<&ToolMetadata> for Tool {
fn from(metadata: &ToolMetadata) -> Self {
let input_schema = if let Value::Object(obj) = &metadata.parameters {
Arc::new(obj.clone())
} else {
Arc::new(serde_json::Map::new())
};
let output_schema = metadata.output_schema.as_ref().and_then(|schema| {
if let Value::Object(obj) = schema {
Some(Arc::new(obj.clone()))
} else {
None
}
});
let mut tool = Tool::new_with_raw(
metadata.name.clone(),
metadata.description.clone().map(|d| d.into()),
input_schema,
);
tool.output_schema = output_schema;
tool.annotations = metadata.generate_annotations();
tool.title = metadata.title.clone();
tool
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_test_metadata(method: &str) -> ToolMetadata {
ToolMetadata {
name: "test_tool".to_string(),
title: None,
description: None,
parameters: json!({}),
output_schema: None,
method: method.to_string(),
path: "/test".to_string(),
security: None,
parameter_mappings: HashMap::new(),
}
}
#[test]
fn test_get_annotations() {
let metadata = create_test_metadata("GET");
let annotations = metadata
.generate_annotations()
.expect("GET should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_post_annotations() {
let metadata = create_test_metadata("POST");
let annotations = metadata
.generate_annotations()
.expect("POST should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(false));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(false));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_put_annotations() {
let metadata = create_test_metadata("PUT");
let annotations = metadata
.generate_annotations()
.expect("PUT should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(false));
assert_eq!(annotations.destructive_hint, Some(true));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_patch_annotations() {
let metadata = create_test_metadata("PATCH");
let annotations = metadata
.generate_annotations()
.expect("PATCH should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(false));
assert_eq!(annotations.destructive_hint, Some(true));
assert_eq!(annotations.idempotent_hint, Some(false));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_delete_annotations() {
let metadata = create_test_metadata("DELETE");
let annotations = metadata
.generate_annotations()
.expect("DELETE should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(false));
assert_eq!(annotations.destructive_hint, Some(true));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_head_annotations() {
let metadata = create_test_metadata("HEAD");
let annotations = metadata
.generate_annotations()
.expect("HEAD should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_options_annotations() {
let metadata = create_test_metadata("OPTIONS");
let annotations = metadata
.generate_annotations()
.expect("OPTIONS should return annotations");
assert_eq!(annotations.title, None);
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_unknown_method_returns_none() {
let unknown_methods = vec!["TRACE", "CONNECT", "CUSTOM", "INVALID", "UNKNOWN"];
for method in unknown_methods {
let metadata = create_test_metadata(method);
let annotations = metadata.generate_annotations();
assert_eq!(
annotations, None,
"Unknown method '{}' should return None",
method
);
}
}
#[test]
fn test_case_insensitive_method_matching() {
let get_variations = vec!["GET", "get", "Get", "gEt", "GeT"];
for method in get_variations {
let metadata = create_test_metadata(method);
let annotations = metadata
.generate_annotations()
.unwrap_or_else(|| panic!("'{}' should return annotations", method));
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
let post_variations = vec!["POST", "post", "Post"];
for method in post_variations {
let metadata = create_test_metadata(method);
let annotations = metadata
.generate_annotations()
.unwrap_or_else(|| panic!("'{}' should return annotations", method));
assert_eq!(annotations.read_only_hint, Some(false));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(false));
assert_eq!(annotations.open_world_hint, Some(true));
}
}
#[test]
fn test_annotations_title_always_none() {
let all_methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
for method in all_methods {
let metadata = create_test_metadata(method);
let annotations = metadata
.generate_annotations()
.unwrap_or_else(|| panic!("'{}' should return annotations", method));
assert_eq!(
annotations.title, None,
"Method '{}' should have title=None in annotations",
method
);
}
}
}