#![allow(dead_code)]
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::Mutex;
use crate::protocol::mcp::{
CallToolResult, Content, GetPromptResult, InitializeResult, ListResourcesResult,
ListToolsResult, Prompt, PromptMessage, ReadResourceResult, Resource, ResourceContent, Role,
Tool,
};
use crate::protocol::{ClientCapabilities, Implementation, ServerCapabilities};
use serde_json::json;
#[async_trait]
pub trait McpClientTrait: Send + Sync {
async fn initialize(&mut self) -> Result<InitializeResult>;
fn is_ready(&self) -> bool;
fn server_capabilities(&self) -> Option<&ServerCapabilities>;
fn transport_type(&self) -> &'static str;
async fn list_tools(&mut self) -> Result<Vec<Tool>>;
async fn list_tools_paginated(&mut self, cursor: Option<String>) -> Result<ListToolsResult>;
async fn call_tool(&mut self, name: &str, arguments: Option<Value>) -> Result<CallToolResult>;
async fn list_resources(&mut self) -> Result<Vec<Resource>>;
async fn list_resources_paginated(
&mut self,
cursor: Option<String>,
) -> Result<ListResourcesResult>;
async fn read_resource(&mut self, uri: &str) -> Result<ReadResourceResult>;
async fn list_prompts(&mut self) -> Result<Vec<Prompt>>;
async fn get_prompt(&mut self, name: &str, arguments: Option<Value>)
-> Result<GetPromptResult>;
async fn ping(&mut self) -> Result<()>;
async fn close(&mut self) -> Result<()>;
}
#[derive(Clone)]
pub struct MockToolResponse {
pub name: String,
pub response: CallToolResult,
}
#[derive(Clone)]
pub struct MockResourceResponse {
pub uri: String,
pub response: ReadResourceResult,
}
#[derive(Clone)]
pub struct MockPromptResponse {
pub name: String,
pub response: GetPromptResult,
}
pub struct MockMcpClient {
initialized: bool,
capabilities: ServerCapabilities,
server_info: Implementation,
tools: Arc<Mutex<Vec<Tool>>>,
tool_responses: Arc<Mutex<Vec<MockToolResponse>>>,
resources: Arc<Mutex<Vec<Resource>>>,
resource_responses: Arc<Mutex<Vec<MockResourceResponse>>>,
prompts: Arc<Mutex<Vec<Prompt>>>,
prompt_responses: Arc<Mutex<Vec<MockPromptResponse>>>,
closed: bool,
next_error: Arc<Mutex<Option<String>>>,
}
impl MockMcpClient {
pub fn new() -> Self {
Self {
initialized: false,
capabilities: ServerCapabilities::default(),
server_info: Implementation::new("mock-server", "1.0.0"),
tools: Arc::new(Mutex::new(Vec::new())),
tool_responses: Arc::new(Mutex::new(Vec::new())),
resources: Arc::new(Mutex::new(Vec::new())),
resource_responses: Arc::new(Mutex::new(Vec::new())),
prompts: Arc::new(Mutex::new(Vec::new())),
prompt_responses: Arc::new(Mutex::new(Vec::new())),
closed: false,
next_error: Arc::new(Mutex::new(None)),
}
}
pub fn with_capabilities(capabilities: ServerCapabilities) -> Self {
let mut client = Self::new();
client.capabilities = capabilities;
client
}
pub async fn set_tools(&self, tools: Vec<Tool>) {
let mut t = self.tools.lock().await;
*t = tools;
}
pub async fn add_tool(&self, tool: Tool) {
let mut tools = self.tools.lock().await;
tools.push(tool);
}
pub async fn set_tool_response(&self, name: &str, response: CallToolResult) {
let mut responses = self.tool_responses.lock().await;
responses.push(MockToolResponse {
name: name.to_string(),
response,
});
}
pub async fn set_resources(&self, resources: Vec<Resource>) {
let mut r = self.resources.lock().await;
*r = resources;
}
pub async fn add_resource(&self, resource: Resource) {
let mut resources = self.resources.lock().await;
resources.push(resource);
}
pub async fn set_resource_response(&self, uri: &str, response: ReadResourceResult) {
let mut responses = self.resource_responses.lock().await;
responses.push(MockResourceResponse {
uri: uri.to_string(),
response,
});
}
pub async fn set_prompts(&self, prompts: Vec<Prompt>) {
let mut p = self.prompts.lock().await;
*p = prompts;
}
pub async fn add_prompt(&self, prompt: Prompt) {
let mut prompts = self.prompts.lock().await;
prompts.push(prompt);
}
pub async fn set_prompt_response(&self, name: &str, response: GetPromptResult) {
let mut responses = self.prompt_responses.lock().await;
responses.push(MockPromptResponse {
name: name.to_string(),
response,
});
}
pub async fn set_next_error(&self, error: &str) {
let mut err = self.next_error.lock().await;
*err = Some(error.to_string());
}
pub fn is_closed(&self) -> bool {
self.closed
}
pub fn create_test_tool(name: &str, description: &str) -> Tool {
Tool {
name: name.to_string(),
description: Some(description.to_string()),
input_schema: json!({
"type": "object",
"properties": {}
}),
}
}
pub fn create_test_resource(uri: &str, name: &str) -> Resource {
Resource {
uri: uri.to_string(),
name: name.to_string(),
description: None,
mime_type: Some("text/plain".to_string()),
}
}
pub fn create_test_prompt(name: &str, description: &str) -> Prompt {
Prompt {
name: name.to_string(),
description: Some(description.to_string()),
arguments: None,
}
}
pub fn success_tool_result(text: &str) -> CallToolResult {
CallToolResult {
content: vec![Content::Text {
text: text.to_string(),
}],
is_error: None,
}
}
pub fn error_tool_result(text: &str) -> CallToolResult {
CallToolResult {
content: vec![Content::Text {
text: text.to_string(),
}],
is_error: Some(true),
}
}
pub fn text_resource_result(uri: &str, text: &str) -> ReadResourceResult {
ReadResourceResult {
contents: vec![ResourceContent {
uri: uri.to_string(),
mime_type: Some("text/plain".to_string()),
text: Some(text.to_string()),
blob: None,
}],
}
}
pub fn prompt_result(description: &str, messages: Vec<(&str, &str)>) -> GetPromptResult {
GetPromptResult {
description: Some(description.to_string()),
messages: messages
.into_iter()
.map(|(role, content)| PromptMessage {
role: match role {
"assistant" => Role::Assistant,
_ => Role::User,
},
content: Content::Text {
text: content.to_string(),
},
})
.collect(),
}
}
async fn check_error(&self) -> Result<()> {
let mut err = self.next_error.lock().await;
if let Some(msg) = err.take() {
anyhow::bail!("{}", msg);
}
Ok(())
}
}
impl Default for MockMcpClient {
fn default() -> Self {
Self::new()
}
}
impl Clone for MockMcpClient {
fn clone(&self) -> Self {
Self {
initialized: self.initialized,
capabilities: self.capabilities.clone(),
server_info: self.server_info.clone(),
tools: Arc::clone(&self.tools),
tool_responses: Arc::clone(&self.tool_responses),
resources: Arc::clone(&self.resources),
resource_responses: Arc::clone(&self.resource_responses),
prompts: Arc::clone(&self.prompts),
prompt_responses: Arc::clone(&self.prompt_responses),
closed: self.closed,
next_error: Arc::clone(&self.next_error),
}
}
}
#[async_trait]
impl McpClientTrait for MockMcpClient {
async fn initialize(&mut self) -> Result<InitializeResult> {
self.check_error().await?;
self.initialized = true;
Ok(InitializeResult {
protocol_version: "2024-11-05".to_string(),
capabilities: self.capabilities.clone(),
server_info: self.server_info.clone(),
instructions: None,
})
}
fn is_ready(&self) -> bool {
self.initialized && !self.closed
}
fn server_capabilities(&self) -> Option<&ServerCapabilities> {
if self.initialized {
Some(&self.capabilities)
} else {
None
}
}
fn transport_type(&self) -> &'static str {
"mock"
}
async fn list_tools(&mut self) -> Result<Vec<Tool>> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let tools = self.tools.lock().await;
Ok(tools.clone())
}
async fn list_tools_paginated(&mut self, _cursor: Option<String>) -> Result<ListToolsResult> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let tools = self.tools.lock().await;
Ok(ListToolsResult {
tools: tools.clone(),
next_cursor: None, })
}
async fn call_tool(&mut self, name: &str, _arguments: Option<Value>) -> Result<CallToolResult> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let responses = self.tool_responses.lock().await;
for resp in responses.iter() {
if resp.name == name {
return Ok(resp.response.clone());
}
}
Ok(Self::success_tool_result(&format!(
"Mock response for tool: {}",
name
)))
}
async fn list_resources(&mut self) -> Result<Vec<Resource>> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let resources = self.resources.lock().await;
Ok(resources.clone())
}
async fn list_resources_paginated(
&mut self,
_cursor: Option<String>,
) -> Result<ListResourcesResult> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let resources = self.resources.lock().await;
Ok(ListResourcesResult {
resources: resources.clone(),
next_cursor: None, })
}
async fn read_resource(&mut self, uri: &str) -> Result<ReadResourceResult> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let responses = self.resource_responses.lock().await;
for resp in responses.iter() {
if resp.uri == uri {
return Ok(resp.response.clone());
}
}
Ok(Self::text_resource_result(
uri,
&format!("Mock content for resource: {}", uri),
))
}
async fn list_prompts(&mut self) -> Result<Vec<Prompt>> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let prompts = self.prompts.lock().await;
Ok(prompts.clone())
}
async fn get_prompt(
&mut self,
name: &str,
_arguments: Option<Value>,
) -> Result<GetPromptResult> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
let responses = self.prompt_responses.lock().await;
for resp in responses.iter() {
if resp.name == name {
return Ok(resp.response.clone());
}
}
Ok(Self::prompt_result(
&format!("Mock prompt: {}", name),
vec![("user", &format!("Mock message for prompt: {}", name))],
))
}
async fn ping(&mut self) -> Result<()> {
self.check_error().await?;
if !self.initialized {
anyhow::bail!("Client not initialized");
}
Ok(())
}
async fn close(&mut self) -> Result<()> {
self.closed = true;
Ok(())
}
}
#[async_trait]
pub trait McpClientFactory: Send + Sync {
async fn create(
&self,
target: &str,
args: &[String],
client_name: &str,
client_version: &str,
capabilities: Option<ClientCapabilities>,
) -> Result<Box<dyn McpClientTrait>>;
}
pub struct MockClientFactory {
client: Arc<Mutex<Option<MockMcpClient>>>,
}
impl MockClientFactory {
pub fn new(client: MockMcpClient) -> Self {
Self {
client: Arc::new(Mutex::new(Some(client))),
}
}
pub fn with_new_client() -> (Self, MockMcpClient) {
let client = MockMcpClient::new();
let factory = Self::new(client.clone());
(factory, client)
}
}
#[async_trait]
impl McpClientFactory for MockClientFactory {
async fn create(
&self,
_target: &str,
_args: &[String],
_client_name: &str,
_client_version: &str,
_capabilities: Option<ClientCapabilities>,
) -> Result<Box<dyn McpClientTrait>> {
let mut client = self.client.lock().await;
match client.take() {
Some(c) => Ok(Box::new(c)),
None => anyhow::bail!("MockClientFactory: client already consumed"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::mcp::{PromptsCapability, ResourcesCapability, ToolsCapability};
#[tokio::test]
async fn mock_client_initialize() {
let mut client = MockMcpClient::new();
assert!(!client.is_ready());
let result = client.initialize().await.unwrap();
assert!(client.is_ready());
assert_eq!(result.protocol_version, "2024-11-05");
assert_eq!(result.server_info.name, "mock-server");
}
#[tokio::test]
async fn mock_client_with_capabilities() {
let caps = ServerCapabilities {
tools: Some(ToolsCapability::default()),
..ServerCapabilities::default()
};
let mut client = MockMcpClient::with_capabilities(caps);
client.initialize().await.unwrap();
let server_caps = client.server_capabilities().unwrap();
assert!(server_caps.tools.is_some());
}
#[tokio::test]
async fn mock_client_list_tools() {
let mut client = MockMcpClient::new();
client
.set_tools(vec![
MockMcpClient::create_test_tool("tool1", "First tool"),
MockMcpClient::create_test_tool("tool2", "Second tool"),
])
.await;
client.initialize().await.unwrap();
let tools = client.list_tools().await.unwrap();
assert_eq!(tools.len(), 2);
assert_eq!(tools[0].name, "tool1");
assert_eq!(tools[1].name, "tool2");
}
#[tokio::test]
async fn mock_client_add_tool() {
let mut client = MockMcpClient::new();
client
.add_tool(MockMcpClient::create_test_tool("added_tool", "Added"))
.await;
client.initialize().await.unwrap();
let tools = client.list_tools().await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "added_tool");
}
#[tokio::test]
async fn mock_client_call_tool_default_response() {
let mut client = MockMcpClient::new();
client.initialize().await.unwrap();
let result = client.call_tool("any_tool", None).await.unwrap();
assert!(result.is_error.is_none());
assert!(!result.content.is_empty());
}
#[tokio::test]
async fn mock_client_call_tool_custom_response() {
let mut client = MockMcpClient::new();
client
.set_tool_response(
"my_tool",
MockMcpClient::success_tool_result("custom result"),
)
.await;
client.initialize().await.unwrap();
let result = client.call_tool("my_tool", None).await.unwrap();
if let Content::Text { text } = &result.content[0] {
assert_eq!(text, "custom result");
} else {
panic!("Expected text content");
}
}
#[tokio::test]
async fn mock_client_call_tool_error_response() {
let mut client = MockMcpClient::new();
client
.set_tool_response(
"error_tool",
MockMcpClient::error_tool_result("error message"),
)
.await;
client.initialize().await.unwrap();
let result = client.call_tool("error_tool", None).await.unwrap();
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn mock_client_list_resources() {
let mut client = MockMcpClient::new();
client
.set_resources(vec![MockMcpClient::create_test_resource(
"file://test.txt",
"test.txt",
)])
.await;
client.initialize().await.unwrap();
let resources = client.list_resources().await.unwrap();
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].uri, "file://test.txt");
}
#[tokio::test]
async fn mock_client_read_resource() {
let mut client = MockMcpClient::new();
client
.set_resource_response(
"file://test.txt",
MockMcpClient::text_resource_result("file://test.txt", "file contents"),
)
.await;
client.initialize().await.unwrap();
let result = client.read_resource("file://test.txt").await.unwrap();
assert_eq!(result.contents.len(), 1);
let content = &result.contents[0];
assert_eq!(content.text.as_deref(), Some("file contents"));
}
#[tokio::test]
async fn mock_client_list_prompts() {
let mut client = MockMcpClient::new();
client
.set_prompts(vec![MockMcpClient::create_test_prompt(
"prompt1",
"Test prompt",
)])
.await;
client.initialize().await.unwrap();
let prompts = client.list_prompts().await.unwrap();
assert_eq!(prompts.len(), 1);
assert_eq!(prompts[0].name, "prompt1");
}
#[tokio::test]
async fn mock_client_get_prompt() {
let mut client = MockMcpClient::new();
client
.set_prompt_response(
"my_prompt",
MockMcpClient::prompt_result("Test", vec![("user", "Hello")]),
)
.await;
client.initialize().await.unwrap();
let result = client.get_prompt("my_prompt", None).await.unwrap();
assert_eq!(result.description, Some("Test".to_string()));
assert_eq!(result.messages.len(), 1);
}
#[tokio::test]
async fn mock_client_ping() {
let mut client = MockMcpClient::new();
client.initialize().await.unwrap();
let result = client.ping().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn mock_client_close() {
let mut client = MockMcpClient::new();
client.initialize().await.unwrap();
assert!(client.is_ready());
client.close().await.unwrap();
assert!(!client.is_ready());
assert!(client.is_closed());
}
#[tokio::test]
async fn mock_client_error_injection() {
let mut client = MockMcpClient::new();
client.set_next_error("Simulated error").await;
let result = client.initialize().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Simulated error"));
}
#[tokio::test]
async fn mock_client_not_initialized_errors() {
let mut client = MockMcpClient::new();
let tools_result = client.list_tools().await;
assert!(tools_result.is_err());
let call_result = client.call_tool("test", None).await;
assert!(call_result.is_err());
let resources_result = client.list_resources().await;
assert!(resources_result.is_err());
let read_result = client.read_resource("uri").await;
assert!(read_result.is_err());
let prompts_result = client.list_prompts().await;
assert!(prompts_result.is_err());
let get_prompt_result = client.get_prompt("name", None).await;
assert!(get_prompt_result.is_err());
let ping_result = client.ping().await;
assert!(ping_result.is_err());
}
#[tokio::test]
async fn mock_client_transport_type() {
let client = MockMcpClient::new();
assert_eq!(client.transport_type(), "mock");
}
#[tokio::test]
async fn mock_client_clone() {
let client = MockMcpClient::new();
client
.add_tool(MockMcpClient::create_test_tool("shared", "Shared tool"))
.await;
let mut cloned = client.clone();
cloned.initialize().await.unwrap();
let tools = cloned.list_tools().await.unwrap();
assert_eq!(tools.len(), 1);
}
#[tokio::test]
async fn mock_client_default() {
let client = MockMcpClient::default();
assert!(!client.is_ready());
}
#[tokio::test]
async fn mock_factory_returns_client() {
let (factory, mock) = MockClientFactory::with_new_client();
mock.add_tool(MockMcpClient::create_test_tool("test", "Test"))
.await;
let mut client = factory
.create("target", &[], "client", "1.0", None)
.await
.unwrap();
client.initialize().await.unwrap();
let tools = client.list_tools().await.unwrap();
assert_eq!(tools.len(), 1);
}
#[tokio::test]
async fn mock_factory_consumes_client() {
let client = MockMcpClient::new();
let factory = MockClientFactory::new(client);
let _ = factory
.create("target", &[], "client", "1.0", None)
.await
.unwrap();
let result = factory.create("target", &[], "client", "1.0", None).await;
assert!(result.is_err());
}
#[test]
fn create_test_tool_helper() {
let tool = MockMcpClient::create_test_tool("test", "description");
assert_eq!(tool.name, "test");
assert_eq!(tool.description, Some("description".to_string()));
}
#[test]
fn create_test_resource_helper() {
let resource = MockMcpClient::create_test_resource("file://test", "test");
assert_eq!(resource.uri, "file://test");
assert_eq!(resource.name, "test");
}
#[test]
fn create_test_prompt_helper() {
let prompt = MockMcpClient::create_test_prompt("test", "description");
assert_eq!(prompt.name, "test");
assert_eq!(prompt.description, Some("description".to_string()));
}
#[test]
fn success_tool_result_helper() {
let result = MockMcpClient::success_tool_result("output");
assert!(result.is_error.is_none());
assert_eq!(result.content.len(), 1);
}
#[test]
fn error_tool_result_helper() {
let result = MockMcpClient::error_tool_result("error");
assert_eq!(result.is_error, Some(true));
}
#[test]
fn text_resource_result_helper() {
let result = MockMcpClient::text_resource_result("uri", "content");
assert_eq!(result.contents.len(), 1);
}
#[test]
fn prompt_result_helper() {
let result = MockMcpClient::prompt_result("desc", vec![("user", "msg")]);
assert_eq!(result.description, Some("desc".to_string()));
assert_eq!(result.messages.len(), 1);
}
#[tokio::test]
async fn mock_client_capabilities_access() {
let caps = ServerCapabilities {
tools: Some(ToolsCapability::default()),
resources: Some(ResourcesCapability::default()),
prompts: Some(PromptsCapability::default()),
..ServerCapabilities::default()
};
let mut client = MockMcpClient::with_capabilities(caps);
assert!(client.server_capabilities().is_none());
client.initialize().await.unwrap();
let server_caps = client.server_capabilities().unwrap();
assert!(server_caps.tools.is_some());
assert!(server_caps.resources.is_some());
assert!(server_caps.prompts.is_some());
}
}