use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use tracing::warn;
use crate::tool_simulation::{FunctionDeclaration, FunctionResponse, TextToolHandler};
use crate::types::RunnerError;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct McpToolDefinition {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[async_trait]
pub trait McpToolExecutor: Send + Sync {
async fn execute(&self, tool_name: &str, arguments: &Value) -> Result<Value, RunnerError>;
}
pub fn mcp_tools_to_declarations(tools: &[McpToolDefinition]) -> Vec<FunctionDeclaration> {
tools
.iter()
.map(|tool| FunctionDeclaration {
name: tool.name.clone(),
description: tool.description.clone(),
parameters: Some(tool.input_schema.clone()),
})
.collect()
}
pub fn create_mcp_tool_handler(executor: Arc<dyn McpToolExecutor>) -> TextToolHandler {
Arc::new(move |tool_name: &str, arguments: &Value| {
let executor = Arc::clone(&executor);
let tool_name_owned = tool_name.to_owned();
let arguments_owned = arguments.clone();
let result = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(executor.execute(&tool_name_owned, &arguments_owned))
});
match result {
Ok(value) => FunctionResponse {
name: tool_name_owned,
response: value,
},
Err(err) => {
warn!(
tool_name = tool_name_owned,
error = %err,
"MCP tool execution failed"
);
FunctionResponse {
name: tool_name_owned,
response: serde_json::json!({"error": err.message}),
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn conversion_correctness() {
let tools = vec![
McpToolDefinition {
name: "read_file".to_owned(),
description: "Read a file from disk".to_owned(),
input_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}),
},
McpToolDefinition {
name: "list_dir".to_owned(),
description: "List directory contents".to_owned(),
input_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string"},
"recursive": {"type": "boolean"}
}
}),
},
];
let declarations = mcp_tools_to_declarations(&tools);
assert_eq!(declarations.len(), 2);
assert_eq!(declarations[0].name, "read_file");
assert_eq!(declarations[0].description, "Read a file from disk");
assert_eq!(
declarations[0].parameters,
Some(json!({
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}))
);
assert_eq!(declarations[1].name, "list_dir");
}
#[test]
fn empty_list_conversion() {
let declarations = mcp_tools_to_declarations(&[]);
assert!(declarations.is_empty());
}
struct MockExecutor {
result: Result<Value, RunnerError>,
}
#[async_trait]
impl McpToolExecutor for MockExecutor {
async fn execute(
&self,
_tool_name: &str,
_arguments: &Value,
) -> Result<Value, RunnerError> {
self.result.clone()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn handler_with_mock_executor() {
let executor = Arc::new(MockExecutor {
result: Ok(json!({"status": "ok", "data": [1, 2, 3]})),
});
let handler = create_mcp_tool_handler(executor);
let response = handler("test_tool", &json!({"key": "value"}));
assert_eq!(response.name, "test_tool");
assert_eq!(response.response["status"], "ok");
assert_eq!(response.response["data"], json!([1, 2, 3]));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn handler_error_path() {
let executor = Arc::new(MockExecutor {
result: Err(RunnerError::external_service("mcp", "connection refused")),
});
let handler = create_mcp_tool_handler(executor);
let response = handler("broken_tool", &json!({}));
assert_eq!(response.name, "broken_tool");
assert!(response.response["error"]
.as_str()
.expect("error field")
.contains("connection refused"));
}
}