use async_trait::async_trait;
use rmcp::service::{RequestContext, RoleServer};
use rmcp_openapi::{HttpClient, Server, Tool, ToolCollection, ToolFilter, ToolMetadata};
use serde_json::json;
use std::sync::Arc;
use url::Url;
mod common;
use common::mock_server::MockPetstoreServer;
struct GetOnlyFilter;
#[async_trait]
impl ToolFilter for GetOnlyFilter {
async fn allow(&self, tool: &Tool, _context: &RequestContext<RoleServer>) -> bool {
tool.metadata.name.starts_with("get")
}
}
struct BlockAllFilter;
#[async_trait]
impl ToolFilter for BlockAllFilter {
async fn allow(&self, _tool: &Tool, _context: &RequestContext<RoleServer>) -> bool {
false
}
}
struct AllowAllFilter;
#[async_trait]
impl ToolFilter for AllowAllFilter {
async fn allow(&self, _tool: &Tool, _context: &RequestContext<RoleServer>) -> bool {
true
}
}
struct ReadOnlyFilter;
#[async_trait]
impl ToolFilter for ReadOnlyFilter {
async fn allow(&self, tool: &Tool, _context: &RequestContext<RoleServer>) -> bool {
tool.metadata.method == "GET"
}
}
fn create_test_server() -> Server {
let tool1_metadata = ToolMetadata {
name: "getPetById".to_string(),
title: Some("Get Pet by ID".to_string()),
description: Some("Find pet by ID".to_string()),
parameters: json!({
"type": "object",
"properties": {
"petId": {
"type": "integer"
}
},
"required": ["petId"]
}),
output_schema: None,
method: "GET".to_string(),
path: "/pet/{petId}".to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let tool2_metadata = ToolMetadata {
name: "addPet".to_string(),
title: Some("Add a Pet".to_string()),
description: Some("Add a new pet to the store".to_string()),
parameters: json!({
"type": "object",
"properties": {
"request_body": {
"type": "object"
}
},
"required": ["request_body"]
}),
output_schema: None,
method: "POST".to_string(),
path: "/pet".to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let tool3_metadata = ToolMetadata {
name: "getStoreInventory".to_string(),
title: Some("Get Store Inventory".to_string()),
description: Some("Returns pet inventories by status".to_string()),
parameters: json!({
"type": "object",
"properties": {}
}),
output_schema: None,
method: "GET".to_string(),
path: "/store/inventory".to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let tool4_metadata = ToolMetadata {
name: "deletePet".to_string(),
title: Some("Delete a Pet".to_string()),
description: Some("Deletes a pet".to_string()),
parameters: json!({
"type": "object",
"properties": {
"petId": {
"type": "integer"
}
},
"required": ["petId"]
}),
output_schema: None,
method: "DELETE".to_string(),
path: "/pet/{petId}".to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let http_client = HttpClient::new();
let tool1 = Tool::new(tool1_metadata, http_client.clone()).unwrap();
let tool2 = Tool::new(tool2_metadata, http_client.clone()).unwrap();
let tool3 = Tool::new(tool3_metadata, http_client.clone()).unwrap();
let tool4 = Tool::new(tool4_metadata, http_client.clone()).unwrap();
let mut server = Server::new(
serde_json::Value::Null,
Url::parse("http://example.com").unwrap(),
None,
None,
false,
false,
false,
);
server.tool_collection = ToolCollection::from_tools(vec![tool1, tool2, tool3, tool4]);
server
}
fn create_petstore_server(base_url: Url) -> anyhow::Result<Server> {
let spec_content = include_str!("assets/petstore-openapi-norefs.json");
let json_value: serde_json::Value = serde_json::from_str(spec_content)?;
let mut server = Server::builder()
.openapi_spec(json_value)
.base_url(base_url)
.build();
server.load_openapi_spec()?;
Ok(server)
}
#[test]
fn test_no_filter_all_tools_accessible() {
let server = create_test_server();
let tool_names = server.get_tool_names();
assert_eq!(tool_names.len(), 4);
assert!(tool_names.contains(&"getPetById".to_string()));
assert!(tool_names.contains(&"addPet".to_string()));
assert!(tool_names.contains(&"getStoreInventory".to_string()));
assert!(tool_names.contains(&"deletePet".to_string()));
}
#[test]
fn test_set_tool_filter() {
let mut server = create_test_server();
assert!(server.tool_filter.is_none());
server.set_tool_filter(Arc::new(GetOnlyFilter));
assert!(server.tool_filter.is_some());
}
#[test]
fn test_builder_tool_filter() {
let server = Server::builder()
.openapi_spec(serde_json::Value::Null)
.base_url(Url::parse("http://example.com").unwrap())
.tool_filter(Arc::new(GetOnlyFilter))
.build();
assert!(server.tool_filter.is_some());
}
#[test]
fn test_tool_collection_has_all_tools() {
let server = create_test_server();
assert_eq!(server.tool_count(), 4);
assert!(server.has_tool("getPetById"));
assert!(server.has_tool("addPet"));
assert!(server.has_tool("getStoreInventory"));
assert!(server.has_tool("deletePet"));
}
#[test]
fn test_tool_metadata_accessible() {
let server = create_test_server();
let get_pet = server.get_tool_metadata("getPetById");
assert!(get_pet.is_some());
let metadata = get_pet.unwrap();
assert_eq!(metadata.name, "getPetById");
assert_eq!(metadata.method, "GET");
assert_eq!(metadata.path, "/pet/{petId}");
let add_pet = server.get_tool_metadata("addPet");
assert!(add_pet.is_some());
let metadata = add_pet.unwrap();
assert_eq!(metadata.name, "addPet");
assert_eq!(metadata.method, "POST");
}
#[tokio::test]
async fn test_get_only_filter_allow_behavior() {
let _filter = GetOnlyFilter; let server = create_test_server();
let get_pet = server.get_tool("getPetById").unwrap();
let add_pet = server.get_tool("addPet").unwrap();
let get_inventory = server.get_tool("getStoreInventory").unwrap();
let delete_pet = server.get_tool("deletePet").unwrap();
assert!(get_pet.metadata.name.starts_with("get"));
assert!(!add_pet.metadata.name.starts_with("get"));
assert!(get_inventory.metadata.name.starts_with("get"));
assert!(!delete_pet.metadata.name.starts_with("get"));
}
#[test]
fn test_read_only_filter_method_check() {
let server = create_test_server();
let get_pet = server.get_tool("getPetById").unwrap();
let add_pet = server.get_tool("addPet").unwrap();
let get_inventory = server.get_tool("getStoreInventory").unwrap();
let delete_pet = server.get_tool("deletePet").unwrap();
assert_eq!(get_pet.metadata.method, "GET");
assert_eq!(add_pet.metadata.method, "POST");
assert_eq!(get_inventory.metadata.method, "GET");
assert_eq!(delete_pet.metadata.method, "DELETE");
}
#[actix_web::test]
async fn test_list_tools_with_get_only_filter() {
let mock_server = MockPetstoreServer::new_with_port(9201).await;
let mut server =
create_petstore_server(mock_server.base_url()).expect("Failed to create petstore server");
server.set_tool_filter(Arc::new(GetOnlyFilter));
let all_tools = server.get_tool_names();
let get_tools: Vec<_> = all_tools.iter().filter(|n| n.starts_with("get")).collect();
let non_get_tools: Vec<_> = all_tools.iter().filter(|n| !n.starts_with("get")).collect();
assert!(
!get_tools.is_empty(),
"Should have some 'get' prefixed tools"
);
assert!(
!non_get_tools.is_empty(),
"Should have some non-'get' prefixed tools"
);
assert!(server.tool_filter.is_some());
}
#[actix_web::test]
async fn test_various_filter_configurations() {
let mock_server = MockPetstoreServer::new_with_port(9202).await;
{
let mut server = create_petstore_server(mock_server.base_url())
.expect("Failed to create petstore server");
server.set_tool_filter(Arc::new(BlockAllFilter));
assert!(server.tool_filter.is_some());
}
{
let mut server = create_petstore_server(mock_server.base_url())
.expect("Failed to create petstore server");
server.set_tool_filter(Arc::new(AllowAllFilter));
assert!(server.tool_filter.is_some());
}
{
let mut server = create_petstore_server(mock_server.base_url())
.expect("Failed to create petstore server");
server.set_tool_filter(Arc::new(ReadOnlyFilter));
assert!(server.tool_filter.is_some());
}
}
#[test]
fn test_error_suggestions_use_filtered_list() {
use rmcp::model::ErrorData;
use rmcp_openapi::error::{ToolCallError, ToolCallValidationError};
let allowed_tools = vec!["getPetById", "getStoreInventory"];
let error = ToolCallError::Validation(ToolCallValidationError::tool_not_found(
"addPet".to_string(),
&allowed_tools,
));
let error_data: ErrorData = error.into();
let error_json = serde_json::to_value(&error_data).unwrap();
let message = error_json["message"].as_str().unwrap();
assert!(
message.contains("not found"),
"Error message should indicate tool not found. Got: {}",
message
);
assert!(
message.contains("addPet"),
"Error message should mention the requested tool name"
);
if let Some(data) = error_json.get("data")
&& let Some(suggestions) = data.get("suggestions")
{
let suggestions_str = suggestions.to_string();
assert!(
!suggestions_str.contains("deletePet"),
"Suggestions should not include blocked tools"
);
}
}
#[test]
fn test_error_suggestions_similar_names() {
use rmcp::model::ErrorData;
use rmcp_openapi::error::{ToolCallError, ToolCallValidationError};
let allowed_tools = vec!["getPetById", "getStoreInventory"];
let error = ToolCallError::Validation(ToolCallValidationError::tool_not_found(
"getPetByID".to_string(), &allowed_tools,
));
let error_data: ErrorData = error.into();
let error_json = serde_json::to_value(&error_data).unwrap();
let message = error_json["message"].as_str().unwrap();
assert!(
message.contains("not found"),
"Error message should indicate tool not found"
);
assert!(
message.contains("getPetByID"),
"Error message should mention the requested tool name"
);
if let Some(data) = error_json.get("data") {
if let Some(suggestions) = data.get("suggestions") {
let suggestions_array = suggestions
.as_array()
.expect("suggestions should be an array");
let suggestion_names: Vec<&str> = suggestions_array
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
suggestion_names.contains(&"getPetById"),
"Suggestions should include similar tool 'getPetById'. Suggestions: {:?}",
suggestion_names
);
} else {
panic!("Expected suggestions in error data for similar tool name");
}
} else {
panic!("Expected data field with suggestions for similar tool name");
}
}
#[test]
fn test_error_no_suggestions_for_unrelated_names() {
use rmcp::model::ErrorData;
use rmcp_openapi::error::{ToolCallError, ToolCallValidationError};
let allowed_tools = vec!["getPetById", "getStoreInventory"];
let error = ToolCallError::Validation(ToolCallValidationError::tool_not_found(
"completelyUnrelatedTool".to_string(),
&allowed_tools,
));
let error_data: ErrorData = error.into();
let error_json = serde_json::to_value(&error_data).unwrap();
let message = error_json["message"].as_str().unwrap();
assert!(
message.contains("not found"),
"Error message should indicate tool not found. Got: {}",
message
);
assert!(
message.contains("completelyUnrelatedTool"),
"Error message should mention the requested tool name"
);
if let Some(data) = error_json.get("data")
&& let Some(suggestions) = data.get("suggestions")
{
let suggestions_array = suggestions
.as_array()
.expect("suggestions should be an array");
assert!(
suggestions_array.is_empty(),
"Suggestions should be empty for completely unrelated tool names. Got: {:?}",
suggestions_array
);
}
}
#[actix_web::test]
async fn test_petstore_spec_tool_diversity() {
let mock_server = MockPetstoreServer::new_with_port(9203).await;
let server =
create_petstore_server(mock_server.base_url()).expect("Failed to create petstore server");
let tool_names = server.get_tool_names();
let mut has_get = false;
let mut has_post = false;
for name in &tool_names {
if let Some(tool) = server.get_tool(name) {
match tool.metadata.method.as_str() {
"GET" => has_get = true,
"POST" => has_post = true,
_ => {}
}
}
}
assert!(has_get, "Petstore spec should have GET tools");
assert!(has_post, "Petstore spec should have POST tools");
}
#[actix_web::test]
async fn test_filter_reduces_tool_count() {
let mock_server = MockPetstoreServer::new_with_port(9204).await;
let server =
create_petstore_server(mock_server.base_url()).expect("Failed to create petstore server");
let all_tools = server.get_tool_names();
let total_count = all_tools.len();
let get_only_count = all_tools
.iter()
.filter(|name| name.starts_with("get"))
.count();
let read_only_count = all_tools
.iter()
.filter(|name| {
server
.get_tool(name)
.map(|t| t.metadata.method == "GET")
.unwrap_or(false)
})
.count();
assert!(
get_only_count <= total_count,
"GetOnlyFilter should filter some tools"
);
assert!(
read_only_count <= total_count,
"ReadOnlyFilter should filter some tools"
);
println!("Total tools: {}", total_count);
println!("Tools starting with 'get': {}", get_only_count);
println!("Tools with GET method: {}", read_only_count);
}
#[test]
fn test_tool_filter_object_safe() {
let _filter: Arc<dyn ToolFilter> = Arc::new(GetOnlyFilter);
let _filter: Arc<dyn ToolFilter> = Arc::new(BlockAllFilter);
let _filter: Arc<dyn ToolFilter> = Arc::new(AllowAllFilter);
let _filter: Arc<dyn ToolFilter> = Arc::new(ReadOnlyFilter);
}
#[test]
fn test_tool_filter_arc_clone() {
let filter: Arc<dyn ToolFilter> = Arc::new(GetOnlyFilter);
let cloned = filter.clone();
assert_eq!(Arc::strong_count(&filter), 2);
assert_eq!(Arc::strong_count(&cloned), 2);
}
#[test]
fn test_server_with_filter_clone() {
let mut server = create_test_server();
server.set_tool_filter(Arc::new(GetOnlyFilter));
let cloned_server = server.clone();
assert!(server.tool_filter.is_some());
assert!(cloned_server.tool_filter.is_some());
}