use crate::error::{A2aMcpError, Result};
use a2a_rs::domain::AgentSkill;
use rmcp::model::Tool;
use serde_json::json;
pub struct SkillToolConverter;
impl SkillToolConverter {
pub fn skill_to_tool(skill: &AgentSkill, agent_url: &str) -> Tool {
let tool_name = Self::create_tool_name(agent_url, &skill.id);
let mut description_parts = vec![skill.description.clone()];
if !skill.examples.is_empty() {
description_parts.push(format!("\n\nExamples:\n- {}", skill.examples.join("\n- ")));
}
if !skill.input_modes.is_empty() {
description_parts.push(format!(
"\nSupported input modes: {}",
skill.input_modes.join(", ")
));
}
if !skill.output_modes.is_empty() {
description_parts.push(format!(
"\nSupported output modes: {}",
skill.output_modes.join(", ")
));
}
let full_description = description_parts.join("");
let input_schema = serde_json::from_value(json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message or query to send to the agent skill"
},
"task_id": {
"type": "string",
"description": "Optional. The ID of an existing task to continue. Omit when starting a new task."
}
},
"required": ["message"]
}))
.expect("Failed to parse schema JSON");
Tool::new(
tool_name,
full_description,
std::sync::Arc::new(input_schema),
)
}
pub fn create_tool_name(agent_url: &str, skill_id: &str) -> String {
let sanitized_url = agent_url
.replace("https://", "")
.replace("http://", "")
.replace(['/', ':', '.'], "_");
format!("{}_{}", sanitized_url, skill_id)
}
pub fn parse_tool_name(tool_name: &str) -> Result<(String, String)> {
let parts: Vec<&str> = tool_name.rsplitn(2, '_').collect();
if parts.len() != 2 {
return Err(A2aMcpError::InvalidToolCall(format!(
"Invalid tool name format: {}",
tool_name
)));
}
Ok((parts[1].to_string(), parts[0].to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skill_to_tool_conversion() {
let skill = AgentSkill::new(
"test_skill".to_string(),
"Test Skill".to_string(),
"A test skill for demonstration".to_string(),
vec!["test".to_string()],
)
.with_examples(vec!["Example 1".to_string(), "Example 2".to_string()])
.with_input_modes(vec!["text".to_string()])
.with_output_modes(vec!["text".to_string()]);
let tool = SkillToolConverter::skill_to_tool(&skill, "https://example.com/agent");
assert!(tool.name.contains("test_skill"));
assert!(tool.description.is_some());
assert!(tool.description.as_ref().unwrap().contains("A test skill"));
assert!(tool.description.as_ref().unwrap().contains("Example 1"));
}
#[test]
fn test_tool_name_creation() {
let name =
SkillToolConverter::create_tool_name("https://example.com:8080/api/agent", "my_skill");
assert_eq!(name, "example_com_8080_api_agent_my_skill");
}
#[test]
fn test_parse_tool_name() {
let (agent_id, skill_id) =
SkillToolConverter::parse_tool_name("example_com_8080_api_agent_my_skill").unwrap();
assert_eq!(agent_id, "example_com_8080_api_agent_my");
assert_eq!(skill_id, "skill");
}
}