#[cfg(test)]
mod test_helpers {
use serde_json::json;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tokio::time::timeout;
pub const TEST_PORT_INTEGRATION: u16 = 19880; pub const TEST_PORT_PROTOCOL: u16 = 19881; pub const TEST_PORT_RECONNECT: u16 = 19876;
pub fn get_test_mcp_server_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir)
.parent()
.and_then(|p| p.parent())
.expect(
"无法定位 workspace 根目录(mcp-proxy 应在 crates/ 下,target/ 在 workspace 根)",
);
workspace_root
.join("target/debug/examples/test_mcp_server")
.to_string_lossy()
.to_string()
}
pub fn get_mcp_proxy_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir)
.parent()
.and_then(|p| p.parent())
.expect(
"无法定位 workspace 根目录(mcp-proxy 应在 crates/ 下,target/ 在 workspace 根)",
);
workspace_root
.join("target/debug/mcp-proxy")
.to_string_lossy()
.to_string()
}
pub fn create_test_config() -> String {
let binary_path = get_test_mcp_server_path();
json!({
"mcpServers": {
"test-server": {
"command": binary_path,
"args": []
}
}
})
.to_string()
}
pub async fn spawn_proxy_server(port: u16) -> anyhow::Result<Child> {
let config = create_test_config();
let mcp_proxy_path = get_mcp_proxy_path();
let child = Command::new(&mcp_proxy_path)
.args([
"proxy",
"--port",
&port.to_string(),
"--host",
"127.0.0.1",
"--config",
&config,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(child)
}
pub async fn wait_for_server_ready(addr: &str, max_retries: u32) -> anyhow::Result<()> {
for i in 0..max_retries {
match tokio::net::TcpStream::connect(addr).await {
Ok(_) => {
println!("✅ Server ready (try #{})", i + 1);
return Ok(());
}
Err(_) => {
if i < max_retries - 1 {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
}
anyhow::bail!("服务器在 {} 次尝试后未就绪", max_retries)
}
pub async fn spawn_convert_client(
url: &str,
ping_interval: u64,
ping_timeout: u64,
) -> anyhow::Result<Child> {
let mcp_proxy_path = get_mcp_proxy_path();
let child = Command::new(&mcp_proxy_path)
.args([
"convert",
url,
"--ping-interval",
&ping_interval.to_string(),
"--ping-timeout",
&ping_timeout.to_string(),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(child)
}
pub async fn wait_for_stderr_patterns(
stderr: &mut BufReader<tokio::process::ChildStderr>,
patterns: &[&str],
timeout_duration: Duration,
) -> anyhow::Result<bool> {
let result = timeout(timeout_duration, async {
let mut line = String::new();
loop {
line.clear();
match stderr.read_line(&mut line).await {
Ok(0) => return false, Ok(_) => {
print!("[stderr] {}", line);
if patterns.iter().any(|pattern| line.contains(pattern)) {
return true;
}
}
Err(_) => return false,
}
}
})
.await;
match result {
Ok(found) => Ok(found),
Err(_) => Ok(false), }
}
pub async fn send_jsonrpc_and_receive(
stdin: &mut tokio::process::ChildStdin,
stdout: &mut BufReader<tokio::process::ChildStdout>,
request: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let msg = format!("{}\n", serde_json::to_string(&request)?);
stdin.write_all(msg.as_bytes()).await?;
stdin.flush().await?;
let mut response = String::new();
stdout.read_line(&mut response).await?;
let parsed: serde_json::Value = serde_json::from_str(&response)?;
Ok(parsed)
}
pub async fn initialize_mcp_client(
stdin: &mut tokio::process::ChildStdin,
stdout: &mut BufReader<tokio::process::ChildStdout>,
) -> anyhow::Result<()> {
let init_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {"listChanged": true},
"sampling": {}
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
});
let init_response = send_jsonrpc_and_receive(stdin, stdout, init_request).await?;
if init_response["error"].is_object() {
anyhow::bail!("Initialize failed: {:?}", init_response["error"]);
}
let initialized_notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized"
});
let msg = format!("{}\n", serde_json::to_string(&initialized_notification)?);
stdin.write_all(msg.as_bytes()).await?;
stdin.flush().await?;
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(())
}
}
#[cfg(test)]
mod integration_tests {
use super::test_helpers::*;
use serde_json::json;
use std::time::Duration;
use tokio::io::BufReader;
#[tokio::test]
async fn test_real_mcp_service_communication() {
println!("\\n========== Test: MCP service connection and communication ==========");
println!("🚀 Start local proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_INTEGRATION)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_INTEGRATION);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
tokio::time::sleep(Duration::from_secs(3)).await;
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 30, 10)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
println!("⏳ Waiting for client to connect...");
let connected = wait_for_stderr_patterns(
&mut stderr_reader,
&["开始代理转换", "proxying traffic", "Stdio server started"],
Duration::from_secs(15),
)
.await
.expect("监控 stderr 失败");
if !connected {
println!("⚠️ No connection success log detected, trying to communicate directly...");
tokio::time::sleep(Duration::from_secs(2)).await;
}
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
println!("📋 Get tool list...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
assert_eq!(tools_response["jsonrpc"], "2.0");
assert_eq!(tools_response["id"], 2);
assert!(tools_response["result"]["tools"].is_array());
let tools = tools_response["result"]["tools"].as_array().unwrap();
println!("✅ Obtained {} tools", tools.len());
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
println!("Tool list: {:?}", tool_names);
assert!(tool_names.contains(&"echo"), "应该包含 echo 工具");
assert!(tool_names.contains(&"increment"), "应该包含 increment 工具");
println!("🔧 Call the echo tool...");
let call_tool_request = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {
"message": "Hello from integration test!"
}
}
});
let call_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, call_tool_request)
.await
.expect("tools/call 请求失败");
assert_eq!(call_response["jsonrpc"], "2.0");
assert_eq!(call_response["id"], 3);
if call_response["error"].is_object() {
panic!("Tool call failed with error: {:?}", call_response["error"]);
}
let result = &call_response["result"];
assert!(
!result["isError"].as_bool().unwrap_or(true),
"echo 调用不应该出错"
);
assert!(result["content"].is_array(), "Content should be an array");
let content = result["content"].as_array().unwrap();
assert!(!content.is_empty(), "Content should not be empty");
let first_content = &content[0];
assert_eq!(first_content["type"], "text");
let text = first_content["text"]
.as_str()
.expect("Should have text field");
assert!(
text.contains("Hello from integration test!"),
"Should echo our message"
);
println!("✅ Tool call successful! Response: {}", text);
println!("🔧 Call the increment tool...");
let increment_request = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "increment",
"arguments": {}
}
});
let increment_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, increment_request)
.await
.expect("increment 请求失败");
assert!(
!increment_response["result"]["isError"]
.as_bool()
.unwrap_or(true),
"increment 调用不应该出错"
);
println!("✅ increment call successful");
println!("🧹 Cleaning process...");
drop(stdin);
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test completed ==========\\n");
}
#[tokio::test]
async fn test_protocol_detection() {
println!("\\n========== Test: Protocol Detection ==========");
println!("🚀 Start local proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_PROTOCOL)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_PROTOCOL);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
println!("🔍Detect protocol type...");
let url = format!("http://{}", addr);
let protocol = crate::client::protocol::detect_mcp_protocol_with_headers(&url, None).await;
assert!(protocol.is_ok(), "协议检测应该成功");
let protocol = protocol.unwrap();
use crate::client::protocol::McpProtocol;
assert_eq!(
protocol,
McpProtocol::Stream,
"应该检测到 Streamable HTTP 协议"
);
println!("✅ Protocol detected: {:?}", protocol);
println!("🧹 Cleaning process...");
let _ = proxy.kill().await;
println!("========== Test completed ==========\\n");
}
}
#[cfg(test)]
mod reconnection_tests {
use super::test_helpers::*;
use serde_json::json;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::timeout;
const CLIENT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const RECONNECT_DETECT_TIMEOUT: Duration = Duration::from_secs(30);
#[tokio::test]
async fn test_reconnection_normal_connection() {
println!("\\n========== Test 1: Normal connection and communication ==========");
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 5, 3)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
println!("⏳ Waiting for client to connect...");
let connected = wait_for_stderr_patterns(
&mut stderr_reader,
&["连接成功", "Backend connected successfully"],
CLIENT_CONNECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
if !connected {
println!("⚠️ No connection success log detected, trying to communicate directly...");
}
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
println!("📋 Get tool list...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
assert!(
tools_response["result"]["tools"].is_array(),
"应该返回工具列表"
);
let tools = tools_response["result"]["tools"].as_array().unwrap();
println!("✅ Obtained {} tools", tools.len());
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(tool_names.contains(&"echo"), "应该包含 echo 工具");
assert!(tool_names.contains(&"increment"), "应该包含 increment 工具");
println!("🔧 Call the increment tool...");
let call_request = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "increment",
"arguments": {}
}
});
let call_response = send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, call_request)
.await
.expect("tools/call 请求失败");
assert!(
!call_response["result"]["isError"].as_bool().unwrap_or(true),
"increment 调用不应该出错"
);
println!("✅ increment call successful");
println!("🧹 Cleaning process...");
drop(stdin);
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test 1 Complete ==========\\n");
}
#[tokio::test]
async fn test_reconnection_on_server_restart() {
println!("\\n========== Test 2: Automatically reconnect after server restart ==========");
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 1)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT + 1);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 2, 2)
.await
.expect("启动 convert 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stderr_reader = BufReader::new(stderr);
println!("⏳ Waiting for client to connect...");
tokio::time::sleep(Duration::from_secs(5)).await;
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
println!("⏳ Waiting for the client to detect the disconnect...");
let disconnected = wait_for_stderr_patterns(
&mut stderr_reader,
&["连接断开", "EVENT_DISCONNECTED", "Connection disconnected"],
RECONNECT_DETECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
if !disconnected {
println!(
"⚠️ No clear disconnection log detected, check if there are any reconnection attempts..."
);
}
println!("🔄 Restart proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 1)
.await
.expect("重启 proxy 失败");
wait_for_server_ready(&addr, 20)
.await
.expect("服务器重启超时");
println!("⏳ Waiting for the client to reconnect...");
let reconnected = wait_for_stderr_patterns(
&mut stderr_reader,
&[
"重连成功",
"EVENT_RECONNECTED",
"Reconnected, proxy service resumed",
],
RECONNECT_DETECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
if reconnected {
println!("✅ The client has been reconnected successfully");
} else {
println!(
"⚠️ No reconnection success log detected (may have reconnected before timeout)"
);
}
println!("🧹 Cleaning process...");
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test 2 Complete ==========\\n");
}
#[tokio::test]
async fn test_reconnection_exponential_backoff() {
println!("\\n========== Test 3: Exponential Backoff Verification ==========");
println!("🔗 Start convert client (serverless)...");
let url = format!("http://127.0.0.1:{}", TEST_PORT_RECONNECT + 2);
let mut client = spawn_convert_client(&url, 30, 10)
.await
.expect("启动 convert 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stderr_reader = BufReader::new(stderr);
println!("⏳ Monitor the backoff log (wait about 15 seconds)...");
let mut backoff_times: Vec<String> = Vec::new();
let result = timeout(Duration::from_secs(15), async {
let mut line = String::new();
loop {
line.clear();
match stderr_reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
print!("[stderr] {}", line);
if line.contains("秒后重连")
|| line.contains("retrying in")
|| line.contains("EVENT_RETRY_BACKOFF")
{
backoff_times.push(line.clone());
if backoff_times.len() >= 3 {
break;
}
}
}
Err(_) => break,
}
}
})
.await;
if result.is_err() {
println!("⚠️ Monitoring timeout");
}
println!("📊 Detected backoff log: {:?}", backoff_times);
if backoff_times.len() >= 2 {
println!("✅ It has been detected that the backoff mechanism is working");
} else {
println!("⚠️ Not enough backoff logs detected");
}
println!("🧹 Cleaning process...");
let _ = client.kill().await;
println!("========== Test 3 Complete ==========\\n");
}
#[tokio::test]
async fn test_request_returns_error_when_connection_closed() {
println!(
"\\n========== Test 4: The request returns an error immediately after the channel is disconnected =========="
);
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 3)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT + 3);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 2, 2)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
let stderr_monitor = tokio::spawn(async move {
let mut line = String::new();
loop {
line.clear();
match stderr_reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
print!("[stderr] {}", line);
}
Err(_) => break,
}
}
});
tokio::time::sleep(Duration::from_secs(3)).await;
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
println!("📋 Send first tools/list request (should succeed)...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let start = std::time::Instant::now();
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
let elapsed = start.elapsed();
assert!(
tools_response["result"]["tools"].is_array(),
"第一个请求应该成功返回工具列表"
);
println!(
"✅ The first request was successful and took time: {:?}",
elapsed
);
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
println!("⏳ Wait for the ping to detect the disconnection (about 5 seconds)...");
tokio::time::sleep(Duration::from_secs(5)).await;
println!("📋 Send a second tools/list request (should return an error quickly)...");
let tools_request2 = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/list"
});
let start = std::time::Instant::now();
let result = timeout(
Duration::from_secs(5),
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request2),
)
.await;
let elapsed = start.elapsed();
match result {
Ok(Ok(response)) => {
if response["error"].is_object() {
println!("✅ Error response received, time taken: {:?}", elapsed);
println!("Error message: {:?}", response["error"]);
assert!(
elapsed < Duration::from_secs(3),
"错误响应应该在 3 秒内返回,实际耗时: {:?}",
elapsed
);
} else {
println!(
"⚠️ Successful response received (may have been reconnected), time taken: {:?}",
elapsed
);
}
}
Ok(Err(e)) => {
println!(
"✅ The request failed (as expected), time taken: {:?}",
elapsed
);
println!("Error: {}", e);
assert!(
elapsed < Duration::from_secs(3),
"错误应该在 3 秒内返回,实际耗时: {:?}",
elapsed
);
}
Err(_) => {
println!(
"❌ The request times out (5 seconds), indicating that the client is waiting!"
);
panic!("请求应该快速返回错误,而不是超时空等");
}
}
println!("🧹 Cleaning process...");
drop(stdin);
stderr_monitor.abort();
let _ = client.kill().await;
println!("========== Test 4 Complete ==========\\n");
}
}