use std::sync::Arc;
use traitclaw_core::traits::tool::ErasedTool;
use traitclaw_core::traits::tool_registry::ToolRegistry;
use traitclaw_core::Result;
use crate::server::McpServer;
use crate::tool::McpTool;
pub struct McpToolRegistry {
server: McpServer,
tools: Vec<Arc<dyn ErasedTool>>,
}
impl McpToolRegistry {
pub async fn stdio(program: &str, args: &[&str]) -> Result<Self> {
let server = McpServer::stdio(program, args).await?;
let tools = server.erased_tools();
tracing::info!(
"McpToolRegistry: connected to '{}', discovered {} tool(s)",
program,
tools.len()
);
Ok(Self { server, tools })
}
#[must_use]
pub fn from_server(server: McpServer) -> Self {
let tools = server.erased_tools();
Self { server, tools }
}
#[must_use]
pub fn server(&self) -> &McpServer {
&self.server
}
#[must_use]
pub fn mcp_tools(&self) -> &[Arc<McpTool>] {
self.server.tools()
}
}
impl ToolRegistry for McpToolRegistry {
fn get_tools(&self) -> Vec<Arc<dyn ErasedTool>> {
self.tools.clone()
}
fn find_tool(&self, name: &str) -> Option<Arc<dyn ErasedTool>> {
self.tools.iter().find(|t| t.name() == name).cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use traitclaw_core::traits::tool::{ErasedTool, ToolSchema};
struct FakeRegistry {
tools: Vec<Arc<dyn ErasedTool>>,
}
impl FakeRegistry {
fn with_tools(tools: Vec<Arc<dyn ErasedTool>>) -> Self {
Self { tools }
}
}
impl ToolRegistry for FakeRegistry {
fn get_tools(&self) -> Vec<Arc<dyn ErasedTool>> {
self.tools.clone()
}
fn find_tool(&self, name: &str) -> Option<Arc<dyn ErasedTool>> {
self.tools.iter().find(|t| t.name() == name).cloned()
}
}
struct FakeTool {
name: String,
}
impl FakeTool {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
}
}
}
#[async_trait::async_trait]
impl ErasedTool for FakeTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"fake tool"
}
fn schema(&self) -> ToolSchema {
ToolSchema {
name: self.name.clone(),
description: "fake tool".into(),
parameters: serde_json::json!({ "type": "object", "properties": {} }),
}
}
async fn execute_json(&self, _input: Value) -> traitclaw_core::Result<Value> {
Ok(Value::String(format!("{} called", self.name)))
}
}
fn make_registry(names: &[&str]) -> FakeRegistry {
let tools: Vec<Arc<dyn ErasedTool>> = names
.iter()
.map(|n| Arc::new(FakeTool::new(n)) as Arc<dyn ErasedTool>)
.collect();
FakeRegistry::with_tools(tools)
}
#[test]
fn test_get_tools_returns_all() {
let reg = make_registry(&["read_file", "write_file", "list_dir", "search", "delete"]);
assert_eq!(reg.get_tools().len(), 5);
}
#[test]
fn test_find_tool_by_name() {
let reg = make_registry(&["read_file", "write_file", "search"]);
assert!(reg.find_tool("read_file").is_some());
assert!(reg.find_tool("write_file").is_some());
assert!(reg.find_tool("search").is_some());
}
#[test]
fn test_find_tool_not_found() {
let reg = make_registry(&["read_file"]);
assert!(reg.find_tool("nonexistent").is_none());
}
#[test]
fn test_len_and_is_empty() {
let reg = make_registry(&["a", "b", "c"]);
assert_eq!(reg.len(), 3);
assert!(!reg.is_empty());
let empty = make_registry(&[]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
}
#[test]
fn test_tool_schemas_populated() {
let reg = make_registry(&["read_file"]);
let tools = reg.get_tools();
assert_eq!(tools.len(), 1);
let schema = tools[0].schema();
assert_eq!(schema.name, "read_file");
assert!(!schema.parameters.is_null());
}
#[tokio::test]
async fn test_tool_execution_through_registry() {
let reg = make_registry(&["echo_tool"]);
let tool = reg.find_tool("echo_tool").expect("echo_tool should exist");
let result = tool
.execute_json(serde_json::json!({"text": "hello"}))
.await
.unwrap();
assert_eq!(result, Value::String("echo_tool called".into()));
}
#[test]
fn test_object_safe_as_trait_object() {
let reg = make_registry(&["a"]);
let _: Arc<dyn ToolRegistry> = Arc::new(reg);
}
}