use recursive::mcp::{McpClient, McpServer};
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use tempfile::TempDir;
static SERVER: OnceLock<McpServer> = OnceLock::new();
fn canonicalize(p: &Path) -> PathBuf {
p.canonicalize().unwrap_or_else(|_| p.to_path_buf())
}
fn allowed_dir() -> PathBuf {
canonicalize(Path::new("/tmp"))
}
fn server() -> &'static McpServer {
SERVER.get_or_init(|| {
let dir = allowed_dir();
McpServer {
name: "filesystem-test".into(),
command: "npx".into(),
args: vec![
"-y".into(),
"@modelcontextprotocol/server-filesystem".into(),
dir.to_string_lossy().into(),
],
url: None,
}
})
}
fn tmp_dir() -> (TempDir, PathBuf) {
let tmp = TempDir::new_in(allowed_dir()).unwrap();
let path = canonicalize(tmp.path());
(tmp, path)
}
#[tokio::test]
#[ignore]
async fn test_initialize_and_list_tools() {
let mut client = McpClient::spawn(server()).await.unwrap();
let tools = client.list_tools().await.unwrap();
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(
tool_names.contains(&"read_file"),
"expected read_file tool, got: {tool_names:?}"
);
assert!(
tool_names.contains(&"write_file"),
"expected write_file tool, got: {tool_names:?}"
);
assert!(
tool_names.contains(&"list_directory"),
"expected list_directory tool, got: {tool_names:?}"
);
for tool in &tools {
assert!(
!tool.description.is_empty(),
"tool '{}' has empty description",
tool.name
);
assert!(
tool.input_schema.is_object(),
"tool '{}' has non-object input_schema",
tool.name
);
}
}
#[tokio::test]
#[ignore]
async fn test_read_file() {
let (_tmp, dir) = tmp_dir();
let file_path = dir.join("hello.txt");
let expected_content = "Hello, MCP integration test!";
std::fs::write(&file_path, expected_content).unwrap();
let mut client = McpClient::spawn(server()).await.unwrap();
let content = client
.call_tool("read_file", json!({"path": file_path.to_string_lossy()}))
.await
.unwrap();
assert_eq!(
content, expected_content,
"read_file returned unexpected content"
);
}
#[tokio::test]
#[ignore]
async fn test_write_file() {
let (_tmp, dir) = tmp_dir();
let file_path = dir.join("written-by-mcp.txt");
let content_to_write = "Written via MCP tool call!";
let mut client = McpClient::spawn(server()).await.unwrap();
let result = client
.call_tool(
"write_file",
json!({
"path": file_path.to_string_lossy(),
"content": content_to_write,
}),
)
.await
.unwrap();
assert!(
result.contains(file_path.to_string_lossy().as_ref()),
"write_file response should contain the file path, got: {result}"
);
let on_disk = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(on_disk, content_to_write, "file content mismatch");
}
#[tokio::test]
#[ignore]
async fn test_read_nonexistent_file() {
let (_tmp, dir) = tmp_dir();
let nonexistent = dir.join("does-not-exist.txt");
let mut client = McpClient::spawn(server()).await.unwrap();
let err = client
.call_tool("read_file", json!({"path": nonexistent.to_string_lossy()}))
.await
.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("ENOENT")
|| err_str.contains("No such file")
|| err_str.contains("not found")
|| err_str.contains("does not exist"),
"expected filesystem error for nonexistent file, got: {err_str}"
);
}