use rmcp_openapi::{HttpClient, Tool, ToolMetadata, config::Authorization};
use serde_json::json;
mod common;
use common::mock_server::MockImageServer;
use mockito::Mock;
fn create_test_png_bytes() -> Vec<u8> {
vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x62, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D,
0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ]
}
fn create_test_jpeg_bytes() -> Vec<u8> {
vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00,
0x01, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xD2, 0x7F, 0xFF, 0xD9,
]
}
fn create_test_gif_bytes() -> Vec<u8> {
vec![
b'G', b'I', b'F', b'8', b'9', b'a', 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4C, 0x01, 0x00, 0x3B, ]
}
fn create_image_tool(mock_server: &MockImageServer, path: &str) -> anyhow::Result<Tool> {
let metadata = ToolMetadata {
name: "get_image".to_string(),
title: None,
description: Some("Get image from endpoint".to_string()),
parameters: json!({"type": "object", "properties": {}}),
output_schema: None,
method: "GET".to_string(),
path: path.to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let http_client = HttpClient::new().with_base_url(mock_server.base_url())?;
Ok(Tool::new(metadata, http_client)?)
}
fn create_text_tool(mock_server: &MockImageServer, path: &str) -> anyhow::Result<Tool> {
let metadata = ToolMetadata {
name: "get_text".to_string(),
title: None,
description: Some("Get text from endpoint".to_string()),
parameters: json!({"type": "object", "properties": {}}),
output_schema: None,
method: "GET".to_string(),
path: path.to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let http_client = HttpClient::new().with_base_url(mock_server.base_url())?;
Ok(Tool::new(metadata, http_client)?)
}
fn create_json_tool(mock_server: &MockImageServer, path: &str) -> anyhow::Result<Tool> {
let metadata = ToolMetadata {
name: "get_json".to_string(),
title: None,
description: Some("Get JSON from endpoint".to_string()),
parameters: json!({"type": "object", "properties": {}}),
output_schema: None,
method: "GET".to_string(),
path: path.to_string(),
security: None,
parameter_mappings: std::collections::HashMap::new(),
};
let http_client = HttpClient::new().with_base_url(mock_server.base_url())?;
Ok(Tool::new(metadata, http_client)?)
}
impl MockImageServer {
pub fn mock_image_endpoint(&mut self, path: &str, content_type: &str, bytes: &[u8]) -> Mock {
self.server
.mock("GET", path)
.with_status(200)
.with_header("content-type", content_type)
.with_body(bytes)
.create()
}
pub fn mock_text_endpoint(&mut self, path: &str, text: &str) -> Mock {
self.server
.mock("GET", path)
.with_status(200)
.with_header("content-type", "text/plain")
.with_body(text)
.create()
}
pub fn mock_json_endpoint(&mut self, path: &str, json: &serde_json::Value) -> Mock {
self.server
.mock("GET", path)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json.to_string())
.create()
}
pub fn mock_image_not_found(&mut self, path: &str) -> Mock {
self.server
.mock("GET", path)
.with_status(404)
.with_header("content-type", "application/json")
.with_body(json!({"error": "Image not found"}).to_string())
.create()
}
}
#[actix_web::test]
async fn test_png_image_response() {
let mut mock_server = MockImageServer::new_with_port(9201).await;
let png_bytes = create_test_png_bytes();
let _mock = mock_server.mock_image_endpoint("/image.png", "image/png", &png_bytes);
let tool = create_image_tool(&mock_server, "/image.png").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, png_bytes,
"Decoded data should match original PNG bytes"
);
assert_eq!(&img.mime_type, "image/png");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_jpeg_image_response() {
let mut mock_server = MockImageServer::new_with_port(9202).await;
let jpeg_bytes = create_test_jpeg_bytes();
let _mock = mock_server.mock_image_endpoint("/image.jpg", "image/jpeg", &jpeg_bytes);
let tool = create_image_tool(&mock_server, "/image.jpg").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, jpeg_bytes,
"Decoded data should match original JPEG bytes"
);
assert_eq!(&img.mime_type, "image/jpeg");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_gif_image_response() {
let mut mock_server = MockImageServer::new_with_port(9203).await;
let gif_bytes = create_test_gif_bytes();
let _mock = mock_server.mock_image_endpoint("/image.gif", "image/gif", &gif_bytes);
let tool = create_image_tool(&mock_server, "/image.gif").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, gif_bytes,
"Decoded data should match original GIF bytes"
);
assert_eq!(&img.mime_type, "image/gif");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_webp_image_response() {
let mut mock_server = MockImageServer::new_with_port(9204).await;
let webp_bytes = vec![
b'R', b'I', b'F', b'F', 0x1A, 0x00, 0x00, 0x00, b'W', b'E', b'B', b'P', b'V', b'P', b'8', b' ', 0x0E, 0x00, 0x00, 0x00, 0x9D, 0x01, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let _mock = mock_server.mock_image_endpoint("/image.webp", "image/webp", &webp_bytes);
let tool = create_image_tool(&mock_server, "/image.webp").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, webp_bytes,
"Decoded data should match original WebP bytes"
);
assert_eq!(&img.mime_type, "image/webp");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_svg_xml_image_response() {
let mut mock_server = MockImageServer::new_with_port(9205).await;
let svg_content = r#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1"/></svg>"#;
let svg_bytes = svg_content.as_bytes();
let _mock = mock_server.mock_image_endpoint("/image.svg", "image/svg+xml", svg_bytes);
let tool = create_image_tool(&mock_server, "/image.svg").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, svg_bytes,
"Decoded data should match original SVG bytes"
);
assert_eq!(&img.mime_type, "image/svg+xml");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_bmp_image_response() {
let mut mock_server = MockImageServer::new_with_port(9206).await;
let bmp_bytes = vec![
b'B', b'M', 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00,
];
let _mock = mock_server.mock_image_endpoint("/image.bmp", "image/bmp", &bmp_bytes);
let tool = create_image_tool(&mock_server, "/image.bmp").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, bmp_bytes,
"Decoded data should match original BMP bytes"
);
assert_eq!(&img.mime_type, "image/bmp");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_image_with_charset_parameter() {
let mut mock_server = MockImageServer::new_with_port(9207).await;
let png_bytes = create_test_png_bytes();
let _mock =
mock_server.mock_image_endpoint("/image.png", "image/png; charset=utf-8", &png_bytes);
let tool = create_image_tool(&mock_server, "/image.png").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(decoded, png_bytes);
assert_eq!(&img.mime_type, "image/png; charset=utf-8");
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_text_response_not_converted() {
let mut mock_server = MockImageServer::new_with_port(9208).await;
let text_content = "This is plain text, not an image";
let _mock = mock_server.mock_text_endpoint("/text.txt", text_content);
let tool = create_text_tool(&mock_server, "/text.txt").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Text(txt) => {
assert!(
txt.text.contains(text_content),
"Should contain the text content"
);
}
_ => panic!("Expected Text content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_json_response_not_converted() {
let mut mock_server = MockImageServer::new_with_port(9209).await;
let json_data = json!({"message": "This is JSON data", "type": "test"});
let _mock = mock_server.mock_json_endpoint("/data.json", &json_data);
let tool = create_json_tool(&mock_server, "/data.json").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Text(txt) => {
assert!(
txt.text.contains("This is JSON data"),
"Should contain the JSON content"
);
}
_ => panic!("Expected Text content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_error_image_response_404() {
let mut mock_server = MockImageServer::new_with_port(9210).await;
let _mock = mock_server.mock_image_not_found("/missing.png");
let tool = create_image_tool(&mock_server, "/missing.png").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.is_error, Some(true));
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Text(txt) => {
assert!(
txt.text.contains("404") || txt.text.contains("Image not found"),
"Error message should mention 404 or error details"
);
}
_ => panic!(
"Expected Text content for error, got: {:?}",
call_result.content[0]
),
}
}
#[actix_web::test]
async fn test_base64_encoding_correctness() {
let mut mock_server = MockImageServer::new_with_port(9211).await;
let test_bytes = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
let _mock = mock_server.mock_image_endpoint("/test.png", "image/png", &test_bytes);
let tool = create_image_tool(&mock_server, "/test.png").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
assert_eq!(&img.mime_type, "image/png");
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(
decoded, test_bytes,
"Decoded bytes should exactly match original"
);
let expected_base64 = STANDARD.encode(&test_bytes);
assert_eq!(
&img.data, &expected_base64,
"Base64 encoding should match expected"
);
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}
#[actix_web::test]
async fn test_empty_image_response() {
let mut mock_server = MockImageServer::new_with_port(9212).await;
let empty_bytes: Vec<u8> = vec![];
let _mock = mock_server.mock_image_endpoint("/empty.png", "image/png", &empty_bytes);
let tool = create_image_tool(&mock_server, "/empty.png").expect("Failed to create tool");
let result = tool.call(&json!({}), Authorization::None, None).await;
assert!(result.is_ok(), "Tool call should succeed");
let call_result = result.unwrap();
assert_eq!(call_result.content.len(), 1);
use rmcp::model::RawContent;
match &call_result.content[0].raw {
RawContent::Image(img) => {
assert_eq!(&img.mime_type, "image/png");
use base64::{Engine as _, engine::general_purpose::STANDARD};
let decoded = STANDARD.decode(&img.data).expect("Should be valid base64");
assert_eq!(decoded.len(), 0, "Decoded empty image should have 0 bytes");
assert_eq!(
&img.data, "",
"Base64 encoding of empty bytes should be empty string"
);
}
_ => panic!("Expected Image content, got: {:?}", call_result.content[0]),
}
assert!(call_result.structured_content.is_none());
assert_eq!(call_result.is_error, Some(false));
}