use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::mcp::clients::MCPClient;
use crate::mcp::core::error::WorkflowError;
use crate::mcp::protocol::{CallToolResult, ToolDefinition};
#[derive(Debug, Clone)]
pub struct MockMCPClient {
connected: Arc<Mutex<bool>>,
tools: Arc<Mutex<Vec<ToolDefinition>>>,
tool_responses: Arc<Mutex<HashMap<String, CallToolResult>>>,
call_history: Arc<Mutex<Vec<ToolCall>>>,
should_fail: Arc<Mutex<Option<WorkflowError>>>,
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub name: String,
pub arguments: Option<HashMap<String, serde_json::Value>>,
pub timestamp: std::time::Instant,
}
impl MockMCPClient {
pub fn new() -> Self {
Self {
connected: Arc::new(Mutex::new(false)),
tools: Arc::new(Mutex::new(Vec::new())),
tool_responses: Arc::new(Mutex::new(HashMap::new())),
call_history: Arc::new(Mutex::new(Vec::new())),
should_fail: Arc::new(Mutex::new(None)),
}
}
pub async fn with_tools(&self, tools: Vec<ToolDefinition>) {
let mut mock_tools = self.tools.lock().await;
*mock_tools = tools;
}
pub async fn expect_tool_call(&self, name: &str, response: CallToolResult) {
let mut responses = self.tool_responses.lock().await;
responses.insert(name.to_string(), response);
}
pub async fn fail_with(&self, error: WorkflowError) {
let mut should_fail = self.should_fail.lock().await;
*should_fail = Some(error);
}
pub async fn get_call_history(&self) -> Vec<ToolCall> {
let history = self.call_history.lock().await;
history.clone()
}
pub async fn verify_tool_called(
&self,
name: &str,
expected_args: Option<HashMap<String, serde_json::Value>>,
) -> bool {
let history = self.call_history.lock().await;
history.iter().any(|call| {
call.name == name
&& match (&call.arguments, &expected_args) {
(None, None) => true,
(Some(actual), Some(expected)) => {
expected.iter().all(|(k, v)| actual.get(k) == Some(v))
}
_ => false,
}
})
}
pub async fn reset(&self) {
*self.connected.lock().await = false;
self.tools.lock().await.clear();
self.tool_responses.lock().await.clear();
self.call_history.lock().await.clear();
*self.should_fail.lock().await = None;
}
}
#[async_trait]
impl MCPClient for MockMCPClient {
async fn connect(&mut self) -> Result<(), WorkflowError> {
if let Some(ref error) = *self.should_fail.lock().await {
return Err(WorkflowError::ExecutionError(format!("{}", error)));
}
*self.connected.lock().await = true;
Ok(())
}
async fn initialize(
&mut self,
_client_name: &str,
_client_version: &str,
) -> Result<(), WorkflowError> {
if let Some(ref error) = *self.should_fail.lock().await {
return Err(WorkflowError::ExecutionError(format!("{}", error)));
}
if !*self.connected.lock().await {
return Err(WorkflowError::ExecutionError("Not connected".to_string()));
}
Ok(())
}
async fn list_tools(&mut self) -> Result<Vec<ToolDefinition>, WorkflowError> {
if let Some(ref error) = *self.should_fail.lock().await {
return Err(WorkflowError::ExecutionError(format!("{}", error)));
}
if !*self.connected.lock().await {
return Err(WorkflowError::ExecutionError("Not connected".to_string()));
}
let tools = self.tools.lock().await;
Ok(tools.clone())
}
async fn call_tool(
&mut self,
name: &str,
arguments: Option<HashMap<String, serde_json::Value>>,
) -> Result<CallToolResult, WorkflowError> {
if let Some(ref error) = *self.should_fail.lock().await {
return Err(WorkflowError::ExecutionError(format!("{}", error)));
}
if !*self.connected.lock().await {
return Err(WorkflowError::ExecutionError("Not connected".to_string()));
}
let mut history = self.call_history.lock().await;
history.push(ToolCall {
name: name.to_string(),
arguments: arguments.clone(),
timestamp: std::time::Instant::now(),
});
let responses = self.tool_responses.lock().await;
if let Some(response) = responses.get(name) {
Ok(response.clone())
} else {
Ok(CallToolResult {
content: vec![crate::mcp::protocol::ToolContent::Text {
text: format!("Mock response for tool: {}", name),
}],
is_error: Some(false),
})
}
}
async fn disconnect(&mut self) -> Result<(), WorkflowError> {
if let Some(ref error) = *self.should_fail.lock().await {
return Err(WorkflowError::ExecutionError(format!("{}", error)));
}
*self.connected.lock().await = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
.try_lock()
.map(|guard| *guard)
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_client_basic_flow() {
let mut client = MockMCPClient::new();
assert!(!client.is_connected());
client.connect().await.unwrap();
assert!(client.is_connected());
client.initialize("test", "1.0").await.unwrap();
let tools = vec![ToolDefinition {
name: "test_tool".to_string(),
description: Some("Test tool".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
}];
client.with_tools(tools.clone()).await;
let listed_tools = client.list_tools().await.unwrap();
assert_eq!(listed_tools.len(), 1);
assert_eq!(listed_tools[0].name, "test_tool");
let response = CallToolResult {
content: vec![crate::mcp::protocol::ToolContent::Text {
text: "Success".to_string(),
}],
is_error: Some(false),
};
client.expect_tool_call("test_tool", response.clone()).await;
let result = client.call_tool("test_tool", None).await.unwrap();
assert_eq!(result.content, response.content);
assert!(client.verify_tool_called("test_tool", None).await);
client.disconnect().await.unwrap();
assert!(!client.is_connected());
}
#[tokio::test]
async fn test_mock_client_with_failure() {
let mut client = MockMCPClient::new();
let error = WorkflowError::ExecutionError("Test error".to_string());
client.fail_with(error).await;
assert!(client.connect().await.is_err());
assert!(client.initialize("test", "1.0").await.is_err());
assert!(client.list_tools().await.is_err());
assert!(client.call_tool("test", None).await.is_err());
}
}