提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
// MCP-Proxy CLI 实现
//
// CLI 主入口,负责命令分发到各个实现模块
use anyhow::{Result, bail};
// 导出 CLI 特定类型
// 注意: ConvertArgs 等通用参数类型已由 client/mod.rs 统一导出
pub use crate::client::support::args::{Cli, Commands};
/// 运行 CLI 主逻辑
///
/// 根据命令类型分发到对应的实现模块:
/// - Convert -> cli_impl/convert_cmd.rs
/// - Check/Detect -> cli_impl/check.rs
/// - Health -> cli_impl/health.rs
/// - Proxy -> proxy_server.rs
pub async fn run_cli(cli: Cli) -> Result<()> {
match cli.command {
Some(Commands::Convert(args)) => {
crate::client::cli_impl::run_convert_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Check(args)) => {
crate::client::cli_impl::run_check_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Detect(args)) => {
crate::client::cli_impl::run_detect_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Health(args)) => {
crate::client::cli_impl::run_health_command(args, cli.quiet).await
}
Some(Commands::Proxy(args)) => {
super::proxy_server::run_proxy_command(args, cli.verbose, cli.quiet).await
}
None => {
// 直接 URL 模式(向后兼容)
if let Some(url) = cli.url {
let args = crate::client::support::args::ConvertArgs {
url: Some(url),
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0, // 无限重试
allow_tools: None,
deny_tools: None,
ping_interval: 30, // 默认 30 秒 ping 一次
ping_timeout: 10, // 默认 10 秒超时
logging: crate::client::support::LoggingArgs {
diagnostic: true, // 默认启用诊断模式
log_dir: None, // 默认无日志目录(将在 init_logging 中自动设置)
log_file: None, // 默认无日志文件
otlp_endpoint: None, // 默认不启用 OTLP 追踪
service_name: "mcp-proxy".to_string(),
},
};
crate::client::cli_impl::run_convert_command(args, cli.verbose, cli.quiet).await
} else {
bail!("请提供 URL 或使用子命令")
}
}
}
}

View File

@@ -0,0 +1,42 @@
//! 检查和检测命令
//!
//! 实现服务状态检查和协议检测功能
use anyhow::Result;
use crate::client::support::{CheckArgs, DetectArgs};
/// 运行检查命令
pub async fn run_check_command(args: CheckArgs, _verbose: bool, quiet: bool) -> Result<()> {
if !quiet {
eprintln!("Checking service health: {}", &args.url);
}
match crate::client::protocol::detect_mcp_protocol(&args.url).await {
Ok(protocol) => {
if !quiet {
eprintln!("Service is healthy (protocol: {protocol})");
}
Ok(())
}
Err(e) => {
if !quiet {
eprintln!("Service check failed: {e}");
}
Err(e)
}
}
}
/// 运行协议检测命令
pub async fn run_detect_command(args: DetectArgs, _verbose: bool, quiet: bool) -> Result<()> {
let protocol = crate::client::protocol::detect_mcp_protocol(&args.url).await?;
if quiet {
println!("{}", protocol);
} else {
eprintln!("{}", protocol);
}
Ok(())
}

View File

@@ -0,0 +1,130 @@
//! Convert 命令的 CLI 实现
//!
//! 处理 convert 命令的参数解析和路由到核心逻辑
use anyhow::Result;
use crate::client::core::{run_command_mode, run_url_mode_with_retry};
use crate::client::support::{ConvertArgs, init_logging, parse_convert_config};
use crate::proxy::ToolFilter;
/// 运行转换命令 - 核心功能
pub async fn run_convert_command(args: ConvertArgs, verbose: bool, quiet: bool) -> Result<()> {
// 检查 --allow-tools 和 --deny-tools 互斥
if args.allow_tools.is_some() && args.deny_tools.is_some() {
anyhow::bail!(
"--allow-tools and --deny-tools cannot be used together, please choose only one"
);
}
// 创建工具过滤器
let tool_filter = if let Some(allow_tools) = args.allow_tools.clone() {
tracing::info!("Tool allowlist enabled: {:?}", allow_tools);
ToolFilter::allow(allow_tools)
} else if let Some(deny_tools) = args.deny_tools.clone() {
tracing::info!("Tool denylist enabled: {:?}", deny_tools);
ToolFilter::deny(deny_tools)
} else {
tracing::debug!("Tool filter disabled");
ToolFilter::default()
};
// 解析配置
tracing::debug!("Parsing convert configuration...");
let config_source = parse_convert_config(&args)?;
tracing::info!("Configuration parsed");
// 提取 MCP 名称用于日志文件命名
let mcp_name = match &config_source {
crate::client::support::McpConfigSource::RemoteService { name, .. } => {
tracing::info!("Service name: {}", name);
Some(name.as_str())
}
crate::client::support::McpConfigSource::LocalCommand { name, .. } => {
tracing::info!("Service name: {}", name);
Some(name.as_str())
}
_ => {
tracing::info!("Service name not specified");
None
}
};
// 初始化日志系统
init_logging(&args, mcp_name, quiet, verbose)?;
tracing::debug!("Logging initialized");
// 记录命令启动(必须在日志系统初始化之后)
tracing::info!("========================================");
tracing::info!("Starting convert command");
tracing::info!("Command: convert");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Diagnostic mode: {}", args.logging.diagnostic);
tracing::info!("========================================");
// 根据配置源执行不同逻辑
match config_source {
crate::client::support::McpConfigSource::DirectUrl { url } => {
tracing::info!("Mode: direct URL");
tracing::info!("Target URL: {}", url);
// 直接 URL 模式(带自动重连)
run_url_mode_with_retry(
&args,
&url,
std::collections::HashMap::new(),
None,
tool_filter,
verbose,
quiet,
)
.await
}
crate::client::support::McpConfigSource::RemoteService {
name,
url,
protocol,
headers,
timeout,
} => {
// 远程服务配置模式
tracing::info!("Mode: remote service config");
tracing::info!("Service name: {}", name);
tracing::info!("Service URL: {}", url);
if let Some(proto) = &protocol {
tracing::info!("Configured protocol: {:?}", proto);
}
if !headers.is_empty() {
tracing::debug!("Custom headers: {:?}", headers);
}
if let Some(timeout) = timeout {
tracing::debug!("Configured timeout: {}s", timeout);
}
if !quiet {
eprintln!("🚀 MCP-Stdio-Proxy: {} ({}) → stdio", name, url);
}
// 合并 headers配置 + 命令行
let merged_headers =
crate::client::support::merge_headers(headers, &args.header, args.auth.as_ref());
run_url_mode_with_retry(
&args,
&url,
merged_headers,
protocol.or(timeout.map(|_| crate::client::protocol::McpProtocol::Stream)),
tool_filter,
verbose,
quiet,
)
.await
}
crate::client::support::McpConfigSource::LocalCommand {
name,
command,
args: cmd_args,
env,
} => {
// 本地命令模式子进程继承父进程环境变量MCP JSON 的 env 会覆盖同名变量)
run_command_mode(&name, &command, cmd_args, env, tool_filter, quiet).await
}
}
}

View File

@@ -0,0 +1,207 @@
//! 健康检查命令
//!
//! 通过建立 MCP 连接验证服务是否健康
use std::time::{Duration, Instant};
use anyhow::{Result, bail};
use mcp_common::McpClientConfig;
use mcp_sse_proxy::SseClientConnection;
use mcp_streamable_proxy::StreamClientConnection;
use crate::client::protocol::detect_mcp_protocol;
use crate::client::proxy_server::ProxyProtocol;
use crate::client::support::HealthArgs;
use crate::model::McpProtocol;
/// 健康检查结果
struct HealthCheckResult {
/// 工具数量
tool_count: usize,
/// 服务器名称
server_name: Option<String>,
/// 服务器版本
server_version: Option<String>,
}
/// 运行健康检查命令
///
/// 通过真正建立 MCP 连接来验证服务是否健康。
/// 成功返回 Ok(()),失败返回 Err。
pub async fn run_health_command(args: HealthArgs, quiet: bool) -> Result<()> {
if !quiet {
eprintln!("Checking health for: {}", &args.url);
}
// 1. 确定协议类型
let protocol = match &args.protocol {
Some(p) => {
let proto = proxy_protocol_to_mcp_protocol(p.clone());
if !quiet {
eprintln!("Using protocol: {}", protocol_display_name(&proto));
}
proto
}
None => {
if !quiet {
eprintln!("Detecting protocol...");
}
let proto = detect_mcp_protocol(&args.url).await?;
if !quiet {
eprintln!("Detected protocol: {}", protocol_display_name(&proto));
}
proto
}
};
// 2. 检查协议类型是否支持
if protocol == McpProtocol::Stdio {
bail!("Stdio protocol is not supported for health checks");
}
// 3. 构建配置
let config = build_config(&args);
// 4. 尝试连接(带超时)
let start = Instant::now();
let result = tokio::time::timeout(
Duration::from_secs(args.timeout),
connect_and_verify(&config, protocol.clone()),
)
.await;
let elapsed = start.elapsed();
// 5. 输出结果
match result {
Ok(Ok(health_result)) => {
if !quiet {
eprintln!("Health check passed");
eprintln!(" Protocol: {}", protocol_display_name(&protocol));
eprintln!(" Tools: {}", health_result.tool_count);
eprintln!(" Response time: {}ms", elapsed.as_millis());
if let (Some(name), Some(version)) =
(&health_result.server_name, &health_result.server_version)
{
eprintln!(" Server: {} v{}", name, version);
} else if let Some(name) = &health_result.server_name {
eprintln!(" Server: {}", name);
}
}
Ok(())
}
Ok(Err(e)) => {
if !quiet {
eprintln!("Health check failed");
eprintln!(" Error: {}", e);
eprintln!(" Response time: {}ms", elapsed.as_millis());
}
Err(anyhow::anyhow!("health check failed: {}", e))
}
Err(_) => {
if !quiet {
eprintln!("Health check failed");
eprintln!(" Error: connection timed out ({}s)", args.timeout);
}
Err(anyhow::anyhow!("health check timeout"))
}
}
}
/// 获取协议的显示名称
fn protocol_display_name(protocol: &McpProtocol) -> &'static str {
match protocol {
McpProtocol::Sse => "SSE",
McpProtocol::Stream => "Streamable HTTP",
McpProtocol::Stdio => "Stdio",
}
}
/// 将 ProxyProtocol 转换为 McpProtocol
fn proxy_protocol_to_mcp_protocol(p: ProxyProtocol) -> McpProtocol {
match p {
ProxyProtocol::Sse => McpProtocol::Sse,
ProxyProtocol::Stream => McpProtocol::Stream,
}
}
/// 构建 MCP 客户端配置
fn build_config(args: &HealthArgs) -> McpClientConfig {
let mut config = McpClientConfig::new(&args.url);
// 添加认证 header
if let Some(auth) = &args.auth {
config = config.with_header("Authorization", auth);
}
// 添加自定义 headers
for (key, value) in &args.header {
config = config.with_header(key, value);
}
// 设置超时
config = config.with_connect_timeout(Duration::from_secs(args.timeout));
config = config.with_read_timeout(Duration::from_secs(args.timeout));
config
}
/// 尝试连接并验证服务
async fn connect_and_verify(
config: &McpClientConfig,
protocol: McpProtocol,
) -> Result<HealthCheckResult> {
match protocol {
McpProtocol::Sse => {
let conn = SseClientConnection::connect(config.clone()).await?;
// 获取工具列表
let tools = conn.list_tools().await?;
let tool_count = tools.len();
// 获取服务器信息
let (server_name, server_version) = conn
.peer_info()
.map(|info| {
(
Some(info.server_info.name.clone()),
Some(info.server_info.version.clone()),
)
})
.unwrap_or((None, None));
Ok(HealthCheckResult {
tool_count,
server_name,
server_version,
})
}
McpProtocol::Stream => {
let conn = StreamClientConnection::connect(config.clone()).await?;
// 获取工具列表
let tools = conn.list_tools().await?;
let tool_count = tools.len();
// 获取服务器信息
let (server_name, server_version) = conn
.peer_info()
.map(|info| {
(
Some(info.server_info.name.clone()),
Some(info.server_info.version.clone()),
)
})
.unwrap_or((None, None));
Ok(HealthCheckResult {
tool_count,
server_name,
server_version,
})
}
McpProtocol::Stdio => {
// 不应该到达这里,因为前面已经检查过了
bail!("stdio protocol is not supported for health check")
}
}
}

View File

@@ -0,0 +1,12 @@
//! CLI 实现模块
//!
//! 处理 CLI 命令的实现和参数解析
pub mod check;
pub mod convert_cmd;
pub mod health;
// 导出 CLI 命令实现
pub use check::{run_check_command, run_detect_command};
pub use convert_cmd::run_convert_command;
pub use health::run_health_command;

View File

@@ -0,0 +1,165 @@
//! 本地命令模式处理
//!
//! 处理本地命令形式的 MCP 服务(通过子进程)
use anyhow::Result;
use std::collections::HashMap;
use std::process::Stdio;
use crate::proxy::{StreamProxyHandler, ToolFilter};
// 使用 mcp-streamable-proxy 的类型rmcp 0.12process-wrap 9.0
use mcp_streamable_proxy::{
ClientCapabilities, ClientInfo, Implementation, ServiceExt, TokioChildProcess, stdio,
};
use crate::client::support::utils::truncate_str;
// 进程组管理(跨平台子进程清理)
use process_wrap::tokio::{CommandWrap, KillOnDrop};
#[cfg(unix)]
use process_wrap::tokio::ProcessGroup;
#[cfg(windows)]
use process_wrap::tokio::JobObject;
/// 命令模式执行(本地子进程)
/// 使用 mcp-streamable-proxyrmcp 0.12)实现 stdio CLI 模式
pub async fn run_command_mode(
name: &str,
command: &str,
cmd_args: Vec<String>,
env: HashMap<String, String>,
tool_filter: ToolFilter,
quiet: bool,
) -> Result<()> {
tracing::info!("Running in local command mode");
tracing::info!("Command: {} {:?}", command, cmd_args);
if !env.is_empty() {
tracing::debug!("Env var count: {}", env.len());
}
if !quiet {
eprintln!("🚀 MCP-Stdio-Proxy: {} (command) → stdio", name);
eprintln!(" Command: {} {:?}", command, cmd_args);
if !env.is_empty() {
eprintln!(" Env vars: {:?}", env);
}
}
// 显示过滤器配置
if !quiet && tool_filter.is_enabled() {
eprintln!("🔧 Tool filtering enabled");
}
// 诊断日志:记录子进程启动信息
tracing::debug!("[child-process] {} {:?}", command, cmd_args);
// 使用 process-wrap 创建子进程命令(跨平台进程清理)
// process-wrap 会自动处理进程组Unix或 Job ObjectWindows
// 并且在 Drop 时自动清理子进程树
// 子进程默认继承父进程的所有环境变量
let mut wrapped_cmd = CommandWrap::with_new(command, |cmd| {
cmd.args(&cmd_args);
// 设置 MCP JSON 配置中的环境变量(会覆盖继承的同名变量)
for (k, v) in &env {
cmd.env(k, v);
}
});
// Unix: 创建进程组,支持 killpg 清理整个进程树
#[cfg(unix)]
wrapped_cmd.wrap(ProcessGroup::leader());
// Windows: 使用 Job Object 管理进程树,并隐藏控制台窗口
// 使用 CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP 确保孙进程也不弹出窗口
#[cfg(windows)]
{
use process_wrap::tokio::CreationFlags;
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
wrapped_cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
wrapped_cmd.wrap(JobObject);
}
// 所有平台: Drop 时自动清理进程
wrapped_cmd.wrap(KillOnDrop);
// 启动子进程
// 使用 builder 模式捕获 stderr便于诊断子 MCP 服务初始化失败
tracing::debug!("Starting child process...");
let (tokio_process, child_stderr) = TokioChildProcess::builder(wrapped_cmd)
.stderr(Stdio::piped())
.spawn()?;
// 启动 stderr 日志读取任务
if let Some(stderr_pipe) = child_stderr {
mcp_common::spawn_stderr_reader(stderr_pipe, name.to_string());
}
if !quiet {
eprintln!("🔗 Starting child process...");
}
// 创建 ClientInfo使用 rmcp 0.12 类型)
let client_info = create_client_info();
// 连接到子进程
let running = client_info.serve(tokio_process).await?;
if !quiet {
eprintln!("✅ Child process started, proxying to stdio...");
// 打印工具列表
match running.list_tools(None).await {
Ok(tools_result) => {
let tools = &tools_result.tools;
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
eprintln!("💡 You can now send JSON-RPC requests through stdin");
}
// 使用 StreamProxyHandler + stdio 将本地 MCP 服务透明暴露为 stdio
let proxy_handler =
StreamProxyHandler::with_tool_filter(running, name.to_string(), tool_filter);
let server = proxy_handler.serve(stdio()).await?;
// 设置 Ctrl+C 信号处理
tokio::select! {
result = server.waiting() => {
result?;
}
_ = tokio::signal::ctrl_c() => {
tracing::info!("Ctrl+C received, shutting down");
// tokio runtime 会清理资源,包括子进程
}
}
Ok(())
}
/// 创建 ClientInfo使用 rmcp 1.1.0 类型)
fn create_client_info() -> ClientInfo {
let capabilities = ClientCapabilities::builder()
.enable_experimental()
.enable_roots()
.enable_roots_list_changed()
.enable_sampling()
.build();
ClientInfo::new(
capabilities,
Implementation::new("mcp-proxy-cli", env!("CARGO_PKG_VERSION")),
)
}

View File

@@ -0,0 +1,168 @@
//! 公共模块 - 提取 SSE 和 Stream 模式的共享逻辑
//!
//! 减少代码重复,统一健康检查行为
use std::future::Future;
use std::time::Duration;
/// 健康检查能力 trait
///
/// 抽象 SSE 和 Stream handler 的共同行为
pub trait HealthChecker: Send + Sync {
/// 检查后端是否可用(同步检查)
fn is_backend_available(&self) -> bool;
/// 检查是否已终止(异步,会调用 list_tools 验证后端服务)
fn is_terminated_async(&self) -> impl Future<Output = bool> + Send;
}
/// 监控连接健康状态
///
/// 提取自 monitor_sse_connection 和 monitor_stream_connection 的公共逻辑
///
/// # 参数
/// - `handler`: 实现 HealthChecker trait 的 handler
/// - `ping_interval`: ping 间隔0 表示禁用 ping
/// - `ping_timeout`: ping 超时(秒)
/// - `quiet`: 是否静默模式
/// - `protocol_name`: 协议名称,用于日志输出(如 "SSE" 或 "Stream"
///
/// # 返回值
/// 返回断开连接的原因描述
pub async fn monitor_connection_health<H: HealthChecker>(
handler: &H,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
protocol_name: &str,
) -> String {
let connection_start = std::time::Instant::now();
if !quiet {
tracing::info!("[{}] Connection health monitoring started", protocol_name);
}
// 健康检查日志间隔:与 ping 间隔一致,但至少 30 秒
let health_log_interval_secs = if ping_interval > 0 {
ping_interval.max(30)
} else {
30
};
if ping_interval == 0 {
// 没有 ping 检测,仅检查连接状态
let mut health_log_interval =
tokio::time::interval(Duration::from_secs(health_log_interval_secs));
health_log_interval.tick().await; // 跳过第一次
let mut check_count = 0u64;
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(1)) => {
if !handler.is_backend_available() {
let alive_duration = connection_start.elapsed();
let disconnect_reason =
format!(
"[{}] Backend connection closed (alive: {}s)",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
}
_ = health_log_interval.tick() => {
check_count += 1;
let alive_duration = connection_start.elapsed();
let backend_available = handler.is_backend_available();
tracing::info!(
"[{}] Health check #{} status={}, alive={}s",
protocol_name,
check_count,
if backend_available { "ok" } else { "error" },
alive_duration.as_secs()
);
}
}
}
}
let mut interval = tokio::time::interval(Duration::from_secs(ping_interval));
interval.tick().await;
let mut ping_count = 0u64;
let mut last_health_log = std::time::Instant::now();
let mut first_ping = true; // 标记是否是第一次 ping
loop {
interval.tick().await;
ping_count += 1;
let alive_duration = connection_start.elapsed();
if !handler.is_backend_available() {
let disconnect_reason = format!(
"[{}] Backend connection closed (alive: {}s)",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
let check_result = tokio::time::timeout(
Duration::from_secs(ping_timeout),
handler.is_terminated_async(),
)
.await;
match check_result {
Ok(true) => {
let disconnect_reason = format!(
"[{}] Ping check failed (service error), alive: {}s",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
Ok(false) => {
// 第一次 ping 成功后打印,之后每隔 health_log_interval_secs 秒打印一次
let since_last_log = last_health_log.elapsed().as_secs();
if first_ping || since_last_log >= health_log_interval_secs {
tracing::info!(
"[{}] Ping check #{} passed, alive={}s",
protocol_name,
ping_count,
alive_duration.as_secs()
);
last_health_log = std::time::Instant::now();
first_ping = false;
} else {
tracing::debug!(
"[{}] list_tools ping #{} passed, alive={}s",
protocol_name,
ping_count,
alive_duration.as_secs()
);
}
}
Err(_) => {
let disconnect_reason = format!(
"[{}] Ping check timeout ({}s), alive: {}s",
protocol_name,
ping_timeout,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
}
}
}

View File

@@ -0,0 +1,157 @@
//! 协议转换核心逻辑
//!
//! 处理协议转换的主要流程,包括 URL 模式、协议检测等
use anyhow::Result;
use std::collections::HashMap;
use crate::client::support::{ConvertArgs, protocol_name};
use crate::proxy::{McpClientConfig, ToolFilter};
use super::sse::run_sse_mode;
use super::stream::run_stream_mode;
/// URL 模式执行(带自动重连)
/// 使用分支逻辑:根据协议类型调用不同的处理函数
pub async fn run_url_mode_with_retry(
args: &ConvertArgs,
url: &str,
merged_headers: HashMap<String, String>,
config_protocol: Option<crate::client::protocol::McpProtocol>,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("Starting protocol conversion");
tracing::info!("Target URL: {url}");
tracing::debug!("Header count: {}", merged_headers.len());
tracing::debug!(
"Ping interval: {}s, ping timeout: {}s",
args.ping_interval,
args.ping_timeout
);
tracing::debug!("Retry count: {} (0 = unlimited)", args.retries);
if !quiet && merged_headers.is_empty() {
eprintln!("🚀 MCP-Stdio-Proxy: {} → stdio", url);
}
// 显示过滤器配置
if !quiet {
if let Some(ref allow_tools) = args.allow_tools {
tracing::info!("Tool allowlist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
tracing::info!("Tool denylist: {:?}", deny_tools);
}
}
// 确定协议类型:命令行参数 > 配置文件 > 自动检测
let protocol = if let Some(ref proto) = args.protocol {
let detected = match proto {
crate::client::proxy_server::ProxyProtocol::Sse => {
crate::client::protocol::McpProtocol::Sse
}
crate::client::proxy_server::ProxyProtocol::Stream => {
crate::client::protocol::McpProtocol::Stream
}
};
tracing::info!(
"Using protocol from CLI argument: {}",
protocol_name(&detected)
);
if !quiet {
eprintln!("🔧 Using protocol from CLI: {}", protocol_name(&detected));
}
detected
} else if let Some(proto) = config_protocol {
tracing::info!("Using protocol from config: {}", protocol_name(&proto));
if !quiet {
eprintln!("🔧 Using protocol from config: {}", protocol_name(&proto));
}
proto
} else {
tracing::info!("Detecting protocol...");
if !quiet {
eprintln!("🔍 Detecting protocol...");
}
let detection_start = std::time::Instant::now();
let detected = crate::client::protocol::detect_mcp_protocol(url)
.await
.map_err(|e| {
tracing::error!("Protocol detection failed: {}", e);
e
})?;
let detection_duration = detection_start.elapsed();
tracing::info!(
"Protocol detection completed: protocol={}, duration={:?}",
protocol_name(&detected),
detection_duration
);
if !quiet {
eprintln!("🔍 Detected protocol: {}", protocol_name(&detected));
}
detected
};
// 构建 McpClientConfig
tracing::debug!("Building MCP client config...");
let config = build_mcp_config(url, &merged_headers, args.auth.as_ref());
tracing::debug!("MCP client config ready");
// 根据协议类型分支处理
tracing::info!("Using protocol: {}", protocol_name(&protocol));
match protocol {
crate::client::protocol::McpProtocol::Sse => {
run_sse_mode(config, args.clone(), tool_filter, verbose, quiet)
.await
.map_err(|e| {
tracing::error!("SSE mode failed: {:?}", e);
eprintln!("❌ SSE mode failed: {}", e);
e
})
}
crate::client::protocol::McpProtocol::Stream => {
run_stream_mode(config, args.clone(), tool_filter, verbose, quiet)
.await
.map_err(|e| {
tracing::error!("Stream mode failed: {:?}", e);
eprintln!("❌ Stream mode failed: {}", e);
e
})
}
crate::client::protocol::McpProtocol::Stdio => {
tracing::error!("Stdio protocol does not support URL conversion");
anyhow::bail!(
"Stdio protocol does not support URL conversion, please use --config for local commands"
)
}
}
}
/// 构建 McpClientConfig
pub fn build_mcp_config(
url: &str,
headers: &HashMap<String, String>,
auth: Option<&String>,
) -> McpClientConfig {
let mut config = McpClientConfig::new(url);
for (k, v) in headers {
// Authorization header: 确保有 "Bearer " 前缀,与 Server 模式行为一致
if k.eq_ignore_ascii_case("Authorization") {
let value = if v.starts_with("Bearer ") {
v.clone()
} else {
format!("Bearer {}", v)
};
config = config.with_header(k, value);
} else {
config = config.with_header(k, v);
}
}
if let Some(auth_value) = auth {
// 命令行 --auth 参数不带 "Bearer " 前缀,直接添加
config = config.with_header("Authorization", auth_value);
}
config
}

View File

@@ -0,0 +1,15 @@
//! 核心业务逻辑模块
//!
//! 包含协议转换的核心实现,与 CLI 接口解耦
pub mod command;
pub mod common;
pub mod convert;
pub mod sse;
pub mod stream;
// 导出公共 API
// 注意: run_sse_mode 和 run_stream_mode 是内部实现细节,
// 只被 convert 模块使用,不需要对外暴露
pub use command::run_command_mode;
pub use convert::run_url_mode_with_retry;

View File

@@ -0,0 +1,436 @@
//! SSE 模式处理
//!
//! Server-Sent Events 协议的实现和连接管理
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use tracing::error;
use super::common::HealthChecker;
use crate::client::support::{
ConvertArgs, classify_error, print_diagnostic_report, summarize_error, truncate_str,
};
use crate::proxy::{McpClientConfig, ProxyHandler, SseClientConnection, ToolFilter};
use mcp_sse_proxy::{ServiceExt, stdio as sse_stdio};
/// 为 ProxyHandler 实现 HealthChecker trait
impl HealthChecker for ProxyHandler {
fn is_backend_available(&self) -> bool {
self.is_backend_available()
}
async fn is_terminated_async(&self) -> bool {
self.is_terminated_async().await
}
}
/// SSE 模式处理(使用 mcp-sse-proxyrmcp 0.10
pub async fn run_sse_mode(
config: McpClientConfig,
args: ConvertArgs,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("========================================");
tracing::info!("Starting SSE mode");
tracing::info!("Target URL: {}", config.url);
tracing::info!(
"Ping config: interval={}s, timeout={}s",
args.ping_interval,
args.ping_timeout
);
tracing::info!("========================================");
if !quiet {
eprintln!("🔗 Connecting to backend service (SSE)...");
}
// 1. 使用高层 API 连接(带重试,防止初始连接因时序问题失败)
let connect_timeout = Duration::from_secs(15);
const MAX_INITIAL_RETRIES: u32 = 3;
const INITIAL_BACKOFF_SECS: u64 = 2;
const MAX_BACKOFF_SECS: u64 = 4;
tracing::info!(
"Connecting to backend (per-attempt timeout: {}s, max retries: {})",
connect_timeout.as_secs(),
MAX_INITIAL_RETRIES
);
let connect_start = std::time::Instant::now();
let mut last_error = None;
let mut conn = None;
let mut backoff_secs = INITIAL_BACKOFF_SECS;
for attempt in 1..=MAX_INITIAL_RETRIES {
match tokio::time::timeout(
connect_timeout,
SseClientConnection::connect(config.clone()),
)
.await
{
Ok(Ok(c)) => {
if attempt > 1 {
tracing::info!(
"Backend connection succeeded on attempt {}/{}",
attempt,
MAX_INITIAL_RETRIES
);
}
conn = Some(c);
break;
}
Ok(Err(e)) => {
tracing::warn!(
"Backend connection attempt {}/{} failed: {}",
attempt,
MAX_INITIAL_RETRIES,
e
);
last_error = Some(format!("Backend connection failed: {}", e));
}
Err(_) => {
tracing::warn!(
"Backend connection attempt {}/{} timed out ({}s)",
attempt,
MAX_INITIAL_RETRIES,
connect_timeout.as_secs()
);
last_error = Some(format!(
"Backend connection timeout ({}s)",
connect_timeout.as_secs()
));
}
}
if attempt < MAX_INITIAL_RETRIES {
tracing::info!("Retrying in {}s... (elapsed: {:?})", backoff_secs, connect_start.elapsed());
if !quiet {
eprintln!(
"⚠️ Connection attempt {}/{} failed, retrying in {}s...",
attempt, MAX_INITIAL_RETRIES, backoff_secs
);
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
let conn = conn.ok_or_else(|| {
let elapsed = connect_start.elapsed();
let msg = last_error.unwrap_or_else(|| "Unknown connection error".to_string());
tracing::error!(
"All {} connection attempts failed after {:?}: {}",
MAX_INITIAL_RETRIES, elapsed, msg
);
eprintln!(
"❌ All {} connection attempts failed after {:.1}s: {}",
MAX_INITIAL_RETRIES, elapsed.as_secs_f64(), msg
);
anyhow::anyhow!(msg)
})?;
let connect_duration = connect_start.elapsed();
tracing::info!(
"Backend connected successfully (duration: {:?})",
connect_duration
);
if !quiet {
eprintln!("✅ Backend connected successfully");
// 打印工具列表
print_sse_tools(&conn, quiet).await;
if args.ping_interval > 0 {
eprintln!(
"💓 Health ping: every {}s (timeout {}s)",
args.ping_interval, args.ping_timeout
);
}
}
// 2. 创建 handler消耗 conn
tracing::debug!("Creating ProxyHandler...");
let handler = Arc::new(conn.into_handler("cli".to_string(), tool_filter.clone()));
tracing::debug!("ProxyHandler created");
// 3. 启动 stdio server
tracing::info!("Starting stdio server...");
let server = (*handler).clone().serve(sse_stdio()).await.map_err(|e| {
tracing::error!("Failed to start stdio server: {:?}", e);
eprintln!("❌ Failed to start stdio server: {}", e);
e
})?;
tracing::info!("Stdio server started");
if !quiet {
eprintln!("💡 Stdio server started, proxying traffic...");
}
// 4. 启动 watchdog 任务
let handler_for_watchdog = handler.clone();
let mut watchdog_handle = tokio::spawn(run_sse_watchdog(
handler_for_watchdog,
args,
config,
tool_filter,
verbose,
quiet,
));
tracing::debug!("Watchdog task started");
// 5. 等待 stdio server 退出
tracing::info!("Waiting for stdio/watchdog events...");
tokio::select! {
result = server.waiting() => {
tracing::info!("========================================");
tracing::info!("Stdio server exited (EOF)");
tracing::info!("========================================");
watchdog_handle.abort();
result?;
}
watchdog_result = &mut watchdog_handle => {
tracing::info!("========================================");
tracing::info!("Watchdog task exited");
tracing::info!("========================================");
if let Err(e) = watchdog_result
&& !e.is_cancelled()
{
error!("SSE Watchdog task failed: {:?}", e);
}
}
}
tracing::info!("SSE mode exited normally");
Ok(())
}
/// 打印 SSE 连接的工具列表
async fn print_sse_tools(conn: &SseClientConnection, quiet: bool) {
if quiet {
return;
}
match conn.list_tools().await {
Ok(tools) => {
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
}
/// SSE 模式的 watchdog负责监控连接健康、断开时重连
async fn run_sse_watchdog(
handler: Arc<ProxyHandler>,
args: ConvertArgs,
config: McpClientConfig,
_tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) {
tracing::info!("========================================");
tracing::info!("Starting SSE watchdog");
tracing::info!("Max retries: {}", args.retries);
tracing::info!("========================================");
let max_retries = args.retries;
let mut attempt = 0u32;
let mut backoff_secs = 1u64;
const MAX_BACKOFF_SECS: u64 = 30;
const EVENT_DISCONNECTED: &str = "EVENT_DISCONNECTED";
const EVENT_RECONNECTED: &str = "EVENT_RECONNECTED";
const EVENT_RETRY_BACKOFF: &str = "EVENT_RETRY_BACKOFF";
let initial_connection_start = std::time::Instant::now();
// 首先监控现有连接的健康状态
tracing::info!("Monitoring initial connection health");
let disconnect_reason =
monitor_sse_connection(&handler, args.ping_interval, args.ping_timeout, quiet).await;
// 连接断开,标记后端不可用
tracing::warn!("Initial connection disconnected: {}", disconnect_reason);
handler.swap_backend(None);
let alive_duration = initial_connection_start.elapsed();
tracing::info!(
"Initial connection alive duration: {}s",
alive_duration.as_secs()
);
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(首次断开)
print_diagnostic_report(
"SSE",
&config.url,
alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
// 进入重连循环
loop {
attempt += 1;
tracing::info!("========================================");
if max_retries == 0 {
tracing::info!("Reconnect attempt #{} (unlimited mode)", attempt);
} else {
tracing::info!("Reconnect attempt {}/{}", attempt, max_retries);
}
tracing::info!("Backoff: {}s", backoff_secs);
if !quiet {
eprintln!("🔗 Reconnecting (attempt #{})...", attempt);
}
// 尝试建立连接
tracing::debug!("Attempting to establish connection...");
let connect_start = std::time::Instant::now();
let connect_result = SseClientConnection::connect(config.clone()).await;
let connect_duration = connect_start.elapsed();
match connect_result {
Ok(conn) => {
tracing::info!("Reconnect succeeded (duration: {:?})", connect_duration);
// 连接成功,获取 RunningService 并热替换后端
let running = conn.into_running_service();
handler.swap_backend(Some(running));
backoff_secs = 1;
if !quiet {
eprintln!(
"✅ [{}] Reconnected, proxy service resumed",
EVENT_RECONNECTED
);
}
// 监控连接健康
tracing::info!("Monitoring connection after reconnect");
let reconnect_start = std::time::Instant::now();
let disconnect_reason =
monitor_sse_connection(&handler, args.ping_interval, args.ping_timeout, quiet)
.await;
// 连接断开,标记后端不可用
tracing::warn!("Reconnected session disconnected: {}", disconnect_reason);
handler.swap_backend(None);
let reconnect_alive_duration = reconnect_start.elapsed();
tracing::info!(
"Reconnected session alive duration: {}s",
reconnect_alive_duration.as_secs()
);
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(重连后断开)
print_diagnostic_report(
"SSE",
&config.url,
reconnect_alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
}
Err(e) => {
let error_type = classify_error(&e);
tracing::error!(
"Connection failed [{}]: {} (duration: {:?})",
error_type,
summarize_error(&e),
connect_duration
);
if max_retries > 0 && attempt >= max_retries {
tracing::error!("Max retries reached: {}", max_retries);
if !quiet {
eprintln!(
"❌ Connection failed, max retries reached ({})",
max_retries
);
eprintln!(" Error type: {}", error_type);
eprintln!(" Error detail: {}", e);
}
// 生成最终诊断报告
print_diagnostic_report(
"SSE",
&config.url,
0,
"Connection failed: max retries reached",
Some(&error_type),
args.logging.diagnostic,
);
break;
}
if !quiet {
if max_retries == 0 {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s (attempt #{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt
);
} else {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s ({}/{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt,
max_retries
);
}
}
if verbose && !quiet {
eprintln!(" Full error: {}", e);
}
}
}
tracing::debug!("Waiting {}s before next reconnect attempt", backoff_secs);
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
tracing::info!("SSE watchdog exited");
}
/// 监控 SSE 连接健康状态
///
/// 委托给 common::monitor_connection_health 公共函数
async fn monitor_sse_connection(
handler: &ProxyHandler,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
) -> String {
super::common::monitor_connection_health(handler, ping_interval, ping_timeout, quiet, "SSE")
.await
}

View File

@@ -0,0 +1,405 @@
//! Stream 模式处理
//!
//! Streamable HTTP 协议的实现和连接管理
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use super::common::HealthChecker;
use crate::client::support::{
ConvertArgs, classify_error, print_diagnostic_report, summarize_error, truncate_str,
};
use crate::proxy::{McpClientConfig, StreamClientConnection, StreamProxyHandler, ToolFilter};
use mcp_streamable_proxy::{ServiceExt, stdio as stream_stdio};
/// 为 StreamProxyHandler 实现 HealthChecker trait
impl HealthChecker for StreamProxyHandler {
fn is_backend_available(&self) -> bool {
self.is_backend_available()
}
async fn is_terminated_async(&self) -> bool {
self.is_terminated_async().await
}
}
/// Stream 模式处理(使用 mcp-streamable-proxyrmcp 0.12
pub async fn run_stream_mode(
config: McpClientConfig,
args: ConvertArgs,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("========================================");
tracing::info!("Starting Stream mode");
tracing::info!("Target URL: {}", config.url);
tracing::info!(
"Ping config: interval={}s, timeout={}s",
args.ping_interval,
args.ping_timeout
);
tracing::info!("========================================");
if !quiet {
eprintln!("🔗 Connecting to backend service (Stream)...");
}
// 1. 使用高层 API 连接(带重试,防止初始连接因时序问题失败)
//
// 重试策略:最多 3 次,每次给 15s 连接超时,间隔 2s/4s
// 第1次最多 15s→ 失败 → 等 2s → 第2次最多 15s→ 失败 → 等 4s → 第3次最多 15s→ 退出
// 最坏总耗时15 + 2 + 15 + 4 + 15 = 51s但大多数失败会比 15s 更快返回)
// 通常总耗时:连接快速失败(~1s) + 2 + 快速失败(~1s) + 4 + 快速失败(~1s) = ~9s
let connect_timeout = Duration::from_secs(15);
const MAX_INITIAL_RETRIES: u32 = 3;
const INITIAL_BACKOFF_SECS: u64 = 2;
const MAX_BACKOFF_SECS: u64 = 4;
tracing::info!(
"Connecting to backend (per-attempt timeout: {}s, max retries: {})",
connect_timeout.as_secs(),
MAX_INITIAL_RETRIES
);
let connect_start = std::time::Instant::now();
let mut last_error = None;
let mut conn = None;
let mut backoff_secs = INITIAL_BACKOFF_SECS;
for attempt in 1..=MAX_INITIAL_RETRIES {
match tokio::time::timeout(
connect_timeout,
StreamClientConnection::connect(config.clone()),
)
.await
{
Ok(Ok(c)) => {
if attempt > 1 {
tracing::info!(
"Backend connection succeeded on attempt {}/{}",
attempt,
MAX_INITIAL_RETRIES
);
}
conn = Some(c);
break;
}
Ok(Err(e)) => {
tracing::warn!(
"Backend connection attempt {}/{} failed: {}",
attempt,
MAX_INITIAL_RETRIES,
e
);
last_error = Some(format!("Backend connection failed: {}", e));
}
Err(_) => {
tracing::warn!(
"Backend connection attempt {}/{} timed out ({}s)",
attempt,
MAX_INITIAL_RETRIES,
connect_timeout.as_secs()
);
last_error = Some(format!(
"Backend connection timeout ({}s)",
connect_timeout.as_secs()
));
}
}
if attempt < MAX_INITIAL_RETRIES {
tracing::info!("Retrying in {}s... (elapsed: {:?})", backoff_secs, connect_start.elapsed());
if !quiet {
eprintln!(
"⚠️ Connection attempt {}/{} failed, retrying in {}s...",
attempt, MAX_INITIAL_RETRIES, backoff_secs
);
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
let conn = conn.ok_or_else(|| {
let elapsed = connect_start.elapsed();
let msg = last_error.unwrap_or_else(|| "Unknown connection error".to_string());
tracing::error!(
"All {} connection attempts failed after {:?}: {}",
MAX_INITIAL_RETRIES, elapsed, msg
);
eprintln!(
"❌ All {} connection attempts failed after {:.1}s: {}",
MAX_INITIAL_RETRIES, elapsed.as_secs_f64(), msg
);
anyhow::anyhow!(msg)
})?;
let connect_duration = connect_start.elapsed();
tracing::info!(
"Backend connected successfully (duration: {:?})",
connect_duration
);
if !quiet {
eprintln!("✅ Backend connected successfully");
// 打印工具列表
print_stream_tools(&conn, quiet).await;
if args.ping_interval > 0 {
eprintln!(
"💓 Health ping: every {}s (timeout {}s)",
args.ping_interval, args.ping_timeout
);
}
}
// 2. 创建 handler消耗 conn
tracing::debug!("Creating ProxyHandler...");
let handler = Arc::new(conn.into_handler("cli".to_string(), tool_filter.clone()));
tracing::debug!("ProxyHandler created");
// 3. 启动 stdio server使用 stream_stdio即 rmcp 0.12 的 stdio
tracing::info!("Starting stdio server...");
let server = (*handler).clone().serve(stream_stdio()).await.map_err(|e| {
tracing::error!("Failed to start stdio server: {:?}", e);
eprintln!("❌ Failed to start stdio server: {}", e);
e
})?;
tracing::info!("Stdio server started");
if !quiet {
eprintln!("💡 Stdio server started, proxying traffic...");
}
// 4. 启动 watchdog 任务
let handler_for_watchdog = handler.clone();
let mut watchdog_handle = tokio::spawn(run_stream_watchdog(
handler_for_watchdog,
args,
config,
tool_filter,
verbose,
quiet,
));
tracing::debug!("Watchdog task started");
// 5. 等待 stdio server 退出
tracing::info!("Waiting for stdio/watchdog events...");
tokio::select! {
result = server.waiting() => {
tracing::info!("========================================");
tracing::info!("Stdio server exited (EOF)");
tracing::info!("========================================");
watchdog_handle.abort();
result?;
}
watchdog_result = &mut watchdog_handle => {
tracing::info!("========================================");
tracing::info!("Watchdog task exited");
tracing::info!("========================================");
if let Err(e) = watchdog_result
&& !e.is_cancelled()
{
tracing::error!("Stream Watchdog task failed: {:?}", e);
}
}
}
tracing::info!("Stream mode exited normally");
Ok(())
}
/// 打印 Stream 连接的工具列表
async fn print_stream_tools(conn: &StreamClientConnection, quiet: bool) {
if quiet {
return;
}
match conn.list_tools().await {
Ok(tools) => {
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
}
/// Stream 模式的 watchdog负责监控连接健康、断开时重连
async fn run_stream_watchdog(
handler: Arc<StreamProxyHandler>,
args: ConvertArgs,
config: McpClientConfig,
_tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) {
let max_retries = args.retries;
let mut attempt = 0u32;
let mut backoff_secs = 1u64;
const MAX_BACKOFF_SECS: u64 = 30;
const EVENT_DISCONNECTED: &str = "EVENT_DISCONNECTED";
const EVENT_RECONNECTED: &str = "EVENT_RECONNECTED";
const EVENT_RETRY_BACKOFF: &str = "EVENT_RETRY_BACKOFF";
let initial_connection_start = std::time::Instant::now();
// 首先监控现有连接的健康状态
let disconnect_reason =
monitor_stream_connection(&handler, args.ping_interval, args.ping_timeout, quiet).await;
// 连接断开,标记后端不可用
handler.swap_backend(None);
let alive_duration = initial_connection_start.elapsed();
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(首次断开)
print_diagnostic_report(
"Streamable HTTP",
&config.url,
alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
// 进入重连循环
loop {
attempt += 1;
if !quiet {
eprintln!("🔗 Reconnecting (attempt #{})...", attempt);
}
// 尝试建立连接
let connect_result = StreamClientConnection::connect(config.clone()).await;
match connect_result {
Ok(conn) => {
// 连接成功,获取 RunningService 并热替换后端
let running = conn.into_running_service();
handler.swap_backend(Some(running));
backoff_secs = 1;
if !quiet {
eprintln!(
"✅ [{}] Reconnected, proxy service resumed",
EVENT_RECONNECTED
);
}
// 监控连接健康
let reconnect_start = std::time::Instant::now();
let disconnect_reason = monitor_stream_connection(
&handler,
args.ping_interval,
args.ping_timeout,
quiet,
)
.await;
// 连接断开,标记后端不可用
handler.swap_backend(None);
let reconnect_alive_duration = reconnect_start.elapsed();
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(重连后断开)
print_diagnostic_report(
"Streamable HTTP",
&config.url,
reconnect_alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
}
Err(e) => {
let error_type = classify_error(&e);
if max_retries > 0 && attempt >= max_retries {
if !quiet {
eprintln!(
"❌ Connection failed, max retries reached ({})",
max_retries
);
eprintln!(" Error type: {}", error_type);
eprintln!(" Error detail: {}", e);
}
// 生成最终诊断报告
print_diagnostic_report(
"Streamable HTTP",
&config.url,
0,
"Connection failed: max retries reached",
Some(&error_type),
args.logging.diagnostic,
);
break;
}
if !quiet {
if max_retries == 0 {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s (attempt #{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt
);
} else {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s ({}/{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt,
max_retries
);
}
}
if verbose && !quiet {
eprintln!(" Full error: {}", e);
}
}
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
/// 监控 Stream 连接健康状态
///
/// 委托给 common::monitor_connection_health 公共函数
async fn monitor_stream_connection(
handler: &StreamProxyHandler,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
) -> String {
super::common::monitor_connection_health(handler, ping_interval, ping_timeout, quiet, "Stream")
.await
}

View File

@@ -0,0 +1,20 @@
// MCP 客户端模块
// 提供各种 MCP 协议的客户端实现和 CLI 工具
mod cli;
mod protocol;
pub(crate) mod proxy_server;
// 新的模块化架构 (按功能层次分组)
pub mod cli_impl; // CLI 命令实现
pub mod core; // 核心业务逻辑
pub mod support; // 支持功能
#[cfg(test)]
mod tests;
// 导出 CLI 功能(公共 API
pub use cli::{Cli, Commands, run_cli};
// 注意ConvertArgs, CheckArgs, DetectArgs 等类型只在内部使用,
// 不需要在这里重新导出。如需使用,请通过 support 模块导入。

View File

@@ -0,0 +1,15 @@
// MCP 协议检测模块
// 用于自动检测远程 MCP 服务的协议类型
// 复用 server 模块的协议检测逻辑
use anyhow::Result;
// 复用 model 中的协议类型定义
pub use crate::model::McpProtocol;
/// 自动检测 MCP 协议类型
///
/// 直接复用 server 模块的协议检测逻辑
pub async fn detect_mcp_protocol(url: &str) -> Result<McpProtocol> {
crate::server::detect_mcp_protocol(url).await
}

View File

@@ -0,0 +1,378 @@
//! MCP Proxy Server - 将 stdio MCP 服务代理为 HTTP/SSE 或 Streamable HTTP 服务
//!
//! 支持多个 agent 复用同一个 MCP 服务
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Once;
use std::time::Duration;
use anyhow::{Result, bail};
use clap::Parser;
use serde::Deserialize;
use tracing::{error, info, warn};
use crate::client::support::{LoggingArgs, init_logging_with_config};
use crate::proxy::ToolFilter;
/// Panic hook 初始化标志(确保只设置一次)
static INIT_PANIC_HOOK: Once = Once::new();
/// 最大重试次数 (0 = 无限重试)
const MAX_RETRIES: u32 = 0;
/// 初始重试间隔(秒)
const INITIAL_RETRY_DELAY_SECS: u64 = 3;
/// 最大重试间隔(秒)
const MAX_RETRY_DELAY_SECS: u64 = 30;
/// 输出协议类型
#[derive(clap::ValueEnum, Clone, Debug, Default)]
pub enum ProxyProtocol {
/// SSE 协议
Sse,
/// Streamable HTTP 协议
#[default]
Stream,
}
/// 代理模式参数 - 将 stdio MCP 服务代理为 HTTP 服务
#[derive(Parser, Debug, Clone)]
pub struct ProxyArgs {
/// 监听端口
#[arg(short, long, default_value = "8080", help = "监听端口")]
pub port: u16,
/// 监听地址
#[arg(long, default_value = "127.0.0.1", help = "监听地址")]
pub host: String,
/// MCP 服务名称(当配置包含多个服务时必需)
#[arg(short, long, help = "MCP 服务名称(多服务配置时必需)")]
pub name: Option<String>,
/// MCP 服务配置 JSON
#[arg(long, conflicts_with = "config_file", help = "MCP 服务配置 JSON")]
pub config: Option<String>,
/// MCP 服务配置文件路径
#[arg(long, conflicts_with = "config", help = "MCP 服务配置文件路径")]
pub config_file: Option<PathBuf>,
/// 输出协议类型
#[arg(long, value_enum, default_value = "stream", help = "输出协议类型")]
pub protocol: ProxyProtocol,
/// SSE 端点路径(仅 SSE 协议)
#[arg(long, default_value = "/sse", help = "SSE 端点路径")]
pub sse_path: String,
/// 消息端点路径(仅 SSE 协议)
#[arg(long, default_value = "/message", help = "消息端点路径")]
pub message_path: String,
/// 工具白名单(逗号分隔),只允许指定的工具
#[arg(long, value_delimiter = ',', help = "工具白名单(逗号分隔)")]
pub allow_tools: Option<Vec<String>>,
/// 工具黑名单(逗号分隔),排除指定的工具
#[arg(long, value_delimiter = ',', help = "工具黑名单(逗号分隔)")]
pub deny_tools: Option<Vec<String>>,
/// 日志配置(使用通用结构)
#[command(flatten)]
pub logging: LoggingArgs,
}
/// MCP 配置格式
#[derive(Deserialize, Debug)]
struct McpConfig {
#[serde(rename = "mcpServers")]
mcp_servers: HashMap<String, StdioConfig>,
}
/// stdio 配置
#[derive(Deserialize, Debug, Clone)]
struct StdioConfig {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
}
/// 解析后的服务配置(包含服务名)
struct ParsedConfig {
name: String,
config: StdioConfig,
}
/// 运行代理命令
pub async fn run_proxy_command(args: ProxyArgs, verbose: bool, quiet: bool) -> Result<()> {
// 设置 panic hook 以记录详细的 panic 信息(只设置一次)
INIT_PANIC_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|panic_info| {
let backtrace = std::backtrace::Backtrace::capture();
error!(
"[PANIC] Program panic - Location: {}:{}, Message: {:?}",
panic_info.location().map(|l| l.file()).unwrap_or("unknown"),
panic_info.location().map(|l| l.line()).unwrap_or(0),
panic_info.payload().downcast_ref::<String>()
);
error!("[PANIC] Stack trace:\\n{:?}", backtrace);
}));
});
// 1. 验证互斥参数
if args.allow_tools.is_some() && args.deny_tools.is_some() {
bail!("--allow-tools 和 --deny-tools 不能同时使用,请只选择其中一个");
}
// 2. 解析配置
let parsed = parse_config(&args)?;
// 2.5 初始化镜像源环境变量MCP_PROXY_NPM_REGISTRY → npm_config_registry 等)
// 必须在日志初始化之前、单线程阶段调用
{
let mirror = mcp_common::mirror::MirrorConfig::from_env();
if !mirror.is_empty() {
mirror.apply_to_process_env();
}
}
// 3. 初始化日志系统(在启动服务之前)
init_logging_with_config(&args.logging, Some(&parsed.name), quiet, verbose)?;
// 4. 创建工具过滤器
let tool_filter = if let Some(allow_tools) = args.allow_tools.clone() {
ToolFilter::allow(allow_tools)
} else if let Some(deny_tools) = args.deny_tools.clone() {
ToolFilter::deny(deny_tools)
} else {
ToolFilter::default()
};
let protocol_name = match args.protocol {
ProxyProtocol::Sse => "SSE",
ProxyProtocol::Stream => "Streamable HTTP",
};
// 记录服务启动信息到日志文件
info!(
"[Service startup] MCP Proxy service startup - protocol: {}, service name: {}, command: {} {:?}",
protocol_name,
parsed.name,
parsed.config.command,
parsed.config.args.as_ref().unwrap_or(&vec![])
);
if let Some(ref allow_tools) = args.allow_tools {
info!("[Service startup] Tool whitelist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
info!("[Service startup] Tool blacklist: {:?}", deny_tools);
}
if !quiet {
eprintln!("🚀 MCP Proxy Service");
eprintln!("Protocol type: {}", protocol_name);
eprintln!("Service name: {}", parsed.name);
eprintln!(
"Command: {} {:?}",
parsed.config.command,
parsed.config.args.as_ref().unwrap_or(&vec![])
);
if verbose && let Some(ref env) = parsed.config.env {
eprintln!("Environment variable: {:?}", env);
}
// 显示过滤器配置
if let Some(ref allow_tools) = args.allow_tools {
eprintln!("Tool whitelist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
eprintln!("Tool blacklist: {:?}", deny_tools);
}
}
// 5. 端口提前绑定(在重试循环之前),确保 ServiceManager 的 TCP 健康检查能检测到进程存活
let bind_addr = format!("{}:{}", args.host, args.port);
let std_listener = std::net::TcpListener::bind(&bind_addr)
.map_err(|e| anyhow::anyhow!("端口绑定失败 {}: {}", bind_addr, e))?;
std_listener
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("设置非阻塞失败: {}", e))?;
info!("[Port Binding] Binded {}", bind_addr);
// 6. 主循环 - 支持子进程崩溃后自动重启(指数退避,有上限)
let mut retry_count: u32 = 0;
let mut retry_delay = Duration::from_secs(INITIAL_RETRY_DELAY_SECS);
loop {
let result = run_proxy_server(
&args,
&parsed,
&std_listener,
tool_filter.clone(),
verbose,
quiet,
)
.await;
match result {
Ok(_) => {
// 正常退出(如 Ctrl+C
info!(
"[Service stopped] MCP Proxy service stopped normally - service name: {}",
parsed.name
);
if !quiet {
eprintln!("🛑 Service has been stopped");
}
break;
}
Err(e) => {
retry_count += 1;
// MAX_RETRIES = 0 表示无限重试,非零值表示最大重试次数
#[allow(clippy::absurd_extreme_comparisons)]
if MAX_RETRIES > 0 && retry_count >= MAX_RETRIES {
error!(
"[Service Termination] Maximum number of retries reached {}, service name: {}, last error: {}",
MAX_RETRIES, parsed.name, e
);
return Err(e);
}
let retry_info = if MAX_RETRIES == 0 {
format!("{}", retry_count)
} else {
format!("{}/{}", retry_count, MAX_RETRIES)
};
error!(
"[Service exception] MCP Proxy service exited abnormally - service name: {}, error: {}, {} and restarts after seconds ({})",
parsed.name,
e,
retry_delay.as_secs(),
retry_info
);
eprintln!(
"⚠️ Service exception: {}, {}, restart after seconds ({})...",
e,
retry_delay.as_secs(),
retry_info
);
tokio::time::sleep(retry_delay).await;
retry_delay =
std::cmp::min(retry_delay * 2, Duration::from_secs(MAX_RETRY_DELAY_SECS));
warn!(
"[Service Restart] Restarting MCP Proxy service - Service name: {}",
parsed.name
);
if !quiet {
eprintln!("🔄 Restarting service...");
}
continue;
}
}
}
Ok(())
}
/// 运行代理服务器(单次运行)
async fn run_proxy_server(
args: &ProxyArgs,
parsed: &ParsedConfig,
std_listener: &std::net::TcpListener,
tool_filter: ToolFilter,
_verbose: bool,
quiet: bool,
) -> Result<()> {
// Windows 上解析命令扩展名(如 npx -> npx.cmd
let resolved_command = mcp_common::resolve_windows_command(&parsed.config.command);
if resolved_command != parsed.config.command {
info!(
"[Command parsing] Windows command parsed: {} -> {}",
parsed.config.command, resolved_command
);
}
// 根据协议类型选择对应的库并启动服务器
// 每个库使用自己的 rmcp 版本创建完整的生命周期
match args.protocol {
ProxyProtocol::Sse => {
// 使用 mcp-sse-proxy 库rmcp 0.10
let config = mcp_sse_proxy::McpServiceConfig {
name: parsed.name.clone(),
command: resolved_command,
args: parsed.config.args.clone(),
env: parsed.config.env.clone(),
tool_filter: Some(tool_filter),
};
mcp_sse_proxy::run_sse_server_from_config(config, std_listener, quiet).await
}
ProxyProtocol::Stream => {
// 使用 mcp-streamable-proxy 库rmcp 0.12
let config = mcp_streamable_proxy::McpServiceConfig {
name: parsed.name.clone(),
command: resolved_command,
args: parsed.config.args.clone(),
env: parsed.config.env.clone(),
tool_filter: Some(tool_filter),
};
mcp_streamable_proxy::run_stream_server_from_config(config, std_listener, quiet).await
}
}
}
// Note: 两个库现在都使用 mcp-common::ToolFilter所以直接传递即可
/// 解析配置
fn parse_config(args: &ProxyArgs) -> Result<ParsedConfig> {
// 1. 读取配置内容
let json_str = if let Some(ref config) = args.config {
config.clone()
} else if let Some(ref path) = args.config_file {
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("读取配置文件失败: {}", e))?
} else {
bail!("必须提供 --config 或 --config-file 参数");
};
// 2. 解析配置
let mcp_config: McpConfig = serde_json::from_str(&json_str).map_err(|e| {
anyhow::anyhow!(
"配置解析失败: {}。配置必须是标准 MCP 格式,包含 mcpServers 字段",
e
)
})?;
let servers = mcp_config.mcp_servers;
if servers.is_empty() {
bail!("配置中没有找到任何 MCP 服务");
}
// 3. 根据服务数量和 --name 参数选择服务
if servers.len() == 1 {
// 单服务:自动使用,无需 --name
let (name, config) = servers.into_iter().next().unwrap();
Ok(ParsedConfig { name, config })
} else if let Some(ref name) = args.name {
// 多服务:根据 --name 选择
servers
.get(name)
.cloned()
.map(|config| ParsedConfig {
name: name.clone(),
config,
})
.ok_or_else(|| {
anyhow::anyhow!(
"服务 '{}' 不存在。可用服务: {:?}",
name,
servers.keys().collect::<Vec<_>>()
)
})
} else {
// 多服务但未指定 --name
bail!(
"配置包含多个服务 {:?},请使用 --name 指定要启动的服务",
servers.keys().collect::<Vec<_>>()
);
}
}

View File

@@ -0,0 +1,244 @@
//! CLI 参数定义
//!
//! 定义所有命令行参数结构体
use clap::Parser;
use std::path::PathBuf;
/// 通用日志配置参数
///
/// 用于多个命令之间共享日志配置
#[derive(Parser, Debug, Clone)]
pub struct LoggingArgs {
/// 启用详细诊断模式,输出连接和工具调用的详细时间信息(默认关闭
#[arg(
long,
default_value = "false",
help = "启用详细诊断模式,追踪连接生命周期和超时问题(默认关闭)"
)]
pub diagnostic: bool,
/// 日志输出目录(自动生成文件名)
#[arg(long, help = "日志输出目录,将自动生成日志文件名")]
pub log_dir: Option<PathBuf>,
/// 日志文件完整路径(手动指定)
#[arg(long, conflicts_with = "log_dir", help = "日志文件完整路径")]
pub log_file: Option<PathBuf>,
/// OTLP 追踪端点(如 http://localhost:4317
///
/// 启用 diagnostic 模式时,可配置此参数将追踪数据发送到 Jaeger 等 OTLP 兼容的后端。
/// 支持 gRPC (端口 4317) 和 HTTP (端口 4318) 协议。
#[arg(
long,
env = "OTEL_EXPORTER_OTLP_ENDPOINT",
help = "OTLP 追踪端点 (如 http://localhost:4317)"
)]
pub otlp_endpoint: Option<String>,
/// 追踪服务名称(用于 Jaeger 等追踪后端标识)
#[arg(
long,
default_value = "mcp-proxy",
help = "追踪服务名称(用于 Jaeger 等追踪后端标识)"
)]
pub service_name: String,
}
/// MCP-Proxy CLI 主命令结构
#[derive(Parser, Debug)]
#[command(name = "mcp-proxy")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "MCP 协议转换代理工具", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// 直接URL模式向后兼容
#[arg(value_name = "URL", help = "MCP 服务的 URL 地址(直接模式)")]
pub url: Option<String>,
/// 全局详细输出
#[arg(short, long, global = true)]
pub verbose: bool,
/// 全局静默模式
#[arg(short, long, global = true)]
pub quiet: bool,
}
#[derive(clap::Subcommand, Debug)]
pub enum Commands {
/// 协议转换模式 - 将 URL 转换为 stdio
Convert(ConvertArgs),
/// 检查服务状态
Check(CheckArgs),
/// 协议检测
Detect(DetectArgs),
/// 代理模式 - 将 stdio MCP 服务代理为 HTTP/SSE 服务
Proxy(crate::client::proxy_server::ProxyArgs),
/// 健康检查 - 验证 MCP 服务是否可用
Health(HealthArgs),
}
/// 协议转换参数
#[derive(Parser, Debug, Clone)]
pub struct ConvertArgs {
/// MCP 服务的 URL 地址(可选,与 --config/--config-file 二选一)
#[arg(value_name = "URL", help = "MCP 服务的 URL 地址")]
pub url: Option<String>,
/// MCP 服务配置 JSON
#[arg(long, conflicts_with = "config_file", help = "MCP 服务配置 JSON")]
pub config: Option<String>,
/// MCP 服务配置文件路径
#[arg(long, conflicts_with = "config", help = "MCP 服务配置文件路径")]
pub config_file: Option<PathBuf>,
/// MCP 服务名称(多服务配置时必需)
#[arg(short, long, help = "MCP 服务名称(多服务配置时必需)")]
pub name: Option<String>,
/// 指定远程服务协议类型(不指定则自动检测)
#[arg(long, value_enum, help = "指定远程服务协议类型(不指定则自动检测)")]
pub protocol: Option<crate::client::proxy_server::ProxyProtocol>,
/// 认证 header (如: "Bearer token")
#[arg(short, long, help = "认证 header")]
pub auth: Option<String>,
/// 自定义 HTTP headers
#[arg(short = 'H', long, value_parser = parse_key_val, help = "自定义 HTTP headers (KEY=VALUE 格式)")]
pub header: Vec<(String, String)>,
/// 重试次数
#[arg(long, default_value = "0", help = "重试次数0 表示无限重试")]
pub retries: u32,
/// 工具白名单(逗号分隔),只允许指定的工具
#[arg(
long,
value_delimiter = ',',
help = "工具白名单(逗号分隔),只允许指定的工具"
)]
pub allow_tools: Option<Vec<String>>,
/// 工具黑名单(逗号分隔),排除指定的工具
#[arg(
long,
value_delimiter = ',',
help = "工具黑名单(逗号分隔),排除指定的工具"
)]
pub deny_tools: Option<Vec<String>>,
/// 客户端 ping 间隔0 表示禁用
#[arg(
long,
default_value = "30",
help = "客户端 ping 间隔0 表示禁用"
)]
pub ping_interval: u64,
/// 客户端 ping 超时(秒)
#[arg(
long,
default_value = "10",
help = "客户端 ping 超时(秒),超时则认为连接断开"
)]
pub ping_timeout: u64,
/// 日志配置(使用通用结构)
#[command(flatten)]
pub logging: LoggingArgs,
}
/// 检查参数
#[derive(Parser, Debug)]
pub struct CheckArgs {
/// 要检查的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header
#[arg(short, long)]
pub auth: Option<String>,
/// 超时时间
#[arg(long, default_value = "10")]
pub timeout: u64,
}
/// 协议检测参数
#[derive(Parser, Debug)]
pub struct DetectArgs {
/// 要检测的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header
#[arg(short, long)]
pub auth: Option<String>,
}
/// 健康检查参数
#[derive(Parser, Debug)]
#[command(after_help = "\
退出码:
0 服务健康 - MCP 连接握手成功
1 服务不健康 - 连接失败、超时或握手失败
示例:
# 基本用法
mcp-proxy health http://localhost:8080/mcp
# 带认证
mcp-proxy health http://localhost:8080/mcp -a \"Bearer token123\"
# 指定协议和超时
mcp-proxy health http://localhost:8080/mcp --protocol sse --timeout 5
# 静默模式(仅返回退出码,适合脚本使用)
mcp-proxy health http://localhost:8080/mcp -q
# 在 shell 脚本中使用
if mcp-proxy health http://localhost:8080/mcp -q; then
echo \"MCP 服务正常\"
else
echo \"MCP 服务不可用\"
fi
")]
pub struct HealthArgs {
/// 要检查的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header (如: "Bearer token")
#[arg(short, long, help = "认证 header")]
pub auth: Option<String>,
/// 自定义 HTTP headers
#[arg(short = 'H', long, value_parser = parse_key_val, help = "自定义 HTTP headers (KEY=VALUE 格式)")]
pub header: Vec<(String, String)>,
/// 超时时间(秒)
#[arg(long, default_value = "10")]
pub timeout: u64,
/// 指定远程服务协议类型(不指定则自动检测)
#[arg(long, value_enum, help = "指定远程服务协议类型(不指定则自动检测)")]
pub protocol: Option<crate::client::proxy_server::ProxyProtocol>,
}
/// 解析 KEY=VALUE 格式的辅助函数
pub fn parse_key_val(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("无效的 KEY=VALUE 格式: {}", s))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}

View File

@@ -0,0 +1,197 @@
//! MCP 配置解析
//!
//! 解析 JSON 配置文件,支持多种服务配置格式
use anyhow::{Result, bail};
use serde::Deserialize;
use std::collections::HashMap;
use super::args::ConvertArgs;
/// 解析后的配置源
#[derive(Debug, Clone)]
pub enum McpConfigSource {
/// 直接 URL 模式(命令行参数)
DirectUrl { url: String },
/// 远程服务配置JSON 配置)
RemoteService {
name: String,
url: String,
protocol: Option<crate::client::protocol::McpProtocol>,
headers: HashMap<String, String>,
timeout: Option<u64>,
},
/// 本地命令配置JSON 配置)
LocalCommand {
name: String,
command: String,
args: Vec<String>,
env: HashMap<String, String>,
},
}
/// MCP 配置格式
#[derive(Deserialize, Debug)]
struct McpConfig {
#[serde(rename = "mcpServers")]
mcp_servers: HashMap<String, McpServerInnerConfig>,
}
/// MCP 服务配置(支持 Command 和 Url 两种类型)
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum McpServerInnerConfig {
Command(StdioConfig),
Url(UrlConfig),
}
/// stdio 配置(本地命令)
#[derive(Deserialize, Debug, Clone)]
struct StdioConfig {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
}
/// URL 配置(远程服务)
#[derive(Deserialize, Debug, Clone)]
struct UrlConfig {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
rename = "baseUrl",
alias = "baseurl",
alias = "base_url"
)]
base_url: Option<String>,
#[serde(default, rename = "type", alias = "Type")]
r#type: Option<String>,
pub headers: Option<HashMap<String, String>>,
#[serde(default, alias = "authToken", alias = "auth_token")]
pub auth_token: Option<String>,
pub timeout: Option<u64>,
}
impl UrlConfig {
fn get_url(&self) -> Option<&str> {
self.url.as_deref().or(self.base_url.as_deref())
}
}
/// 解析 convert 命令的配置
pub fn parse_convert_config(args: &ConvertArgs) -> Result<McpConfigSource> {
// 优先级url > config > config_file
if let Some(ref url) = args.url {
return Ok(McpConfigSource::DirectUrl { url: url.clone() });
}
// 读取 JSON 配置
let json_str = if let Some(ref config) = args.config {
config.clone()
} else if let Some(ref path) = args.config_file {
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("读取配置文件失败: {}", e))?
} else {
bail!("必须提供 URL、--config 或 --config-file 参数之一");
};
// 解析 JSON 配置
let mcp_config: McpConfig = serde_json::from_str(&json_str).map_err(|e| {
anyhow::anyhow!(
"配置解析失败: {}。配置必须是标准 MCP 格式,包含 mcpServers 字段",
e
)
})?;
let servers = mcp_config.mcp_servers;
if servers.is_empty() {
bail!("配置中没有找到任何 MCP 服务");
}
// 选择服务
let (name, inner_config) = if let Some(ref name) = args.name {
// 用户指定了服务名称,必须严格匹配
let config = servers.get(name).cloned().ok_or_else(|| {
anyhow::anyhow!(
"服务 '{}' 不存在。可用服务: {:?}",
name,
servers.keys().collect::<Vec<_>>()
)
})?;
(name.clone(), config)
} else if servers.len() == 1 {
// 单服务且未指定名称,自动使用
servers.into_iter().next().unwrap()
} else {
// 多服务且未指定名称
bail!(
"配置包含多个服务 {:?},请使用 --name 指定要使用的服务",
servers.keys().collect::<Vec<_>>()
);
};
// 根据配置类型返回
match inner_config {
McpServerInnerConfig::Command(stdio) => Ok(McpConfigSource::LocalCommand {
name,
command: stdio.command,
args: stdio.args.unwrap_or_default(),
env: stdio.env.unwrap_or_default(),
}),
McpServerInnerConfig::Url(url_config) => {
let url = url_config
.get_url()
.ok_or_else(|| anyhow::anyhow!("URL 配置缺少 url 或 baseUrl 字段"))?
.to_string();
// 解析协议类型
let protocol =
url_config
.r#type
.as_ref()
.and_then(|t| match t.to_ascii_lowercase().as_str() {
"sse" => Some(crate::client::protocol::McpProtocol::Sse),
"http" | "stream" | "streamablehttp" | "streamable-http"
| "streamable_http" => Some(crate::client::protocol::McpProtocol::Stream),
_ => None,
});
// 合并 headersJSON 配置中的 auth_token -> Authorization
let mut headers = url_config.headers.clone().unwrap_or_default();
if let Some(auth_token) = &url_config.auth_token {
headers.insert("Authorization".to_string(), auth_token.clone());
}
Ok(McpConfigSource::RemoteService {
name,
url,
protocol,
headers,
timeout: url_config.timeout,
})
}
}
}
/// 合并 headersJSON 配置 + 命令行参数(命令行优先)
pub fn merge_headers(
config_headers: HashMap<String, String>,
cli_headers: &[(String, String)],
cli_auth: Option<&String>,
) -> HashMap<String, String> {
let mut merged = config_headers;
// 命令行 -H 参数覆盖配置
for (key, value) in cli_headers {
merged.insert(key.clone(), value.clone());
}
// 命令行 --auth 参数优先级最高
if let Some(auth_value) = cli_auth {
merged.insert("Authorization".to_string(), auth_value.clone());
}
merged
}

View File

@@ -0,0 +1,628 @@
//! 配置解析单元测试
//!
//! 测试各种 MCP JSON 配置格式的解析
use super::args::{ConvertArgs, LoggingArgs};
use super::config::{McpConfigSource, parse_convert_config};
#[cfg(test)]
mod config_parsing_tests {
use super::*;
/// 测试 1: 解析简单的本地命令配置command 类型)
#[test]
fn test_parse_simple_command_config() {
let config_json = r#"{
"mcpServers": {
"test-service": {
"command": "node",
"args": ["server.js"]
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok(), "解析应该成功");
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand {
name,
command,
args,
..
} => {
assert_eq!(name, "test-service");
assert_eq!(command, "node");
assert_eq!(args, vec!["server.js"]);
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 2: 解析带环境变量的本地命令配置
#[test]
fn test_parse_command_config_with_env() {
let config_json = r#"{
"mcpServers": {
"my-service": {
"command": "python",
"args": ["-m", "mcp_server"],
"env": {
"API_KEY": "test-key",
"DEBUG": "true"
}
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand {
name,
command,
args,
env,
} => {
assert_eq!(name, "my-service");
assert_eq!(command, "python");
assert_eq!(args, vec!["-m", "mcp_server"]);
assert_eq!(env.get("API_KEY"), Some(&"test-key".to_string()));
assert_eq!(env.get("DEBUG"), Some(&"true".to_string()));
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 3: 解析远程 URL 配置(使用 baseUrl
#[test]
fn test_parse_remote_service_config_baseurl() {
let config_json = r#"{
"mcpServers": {
"remote-service": {
"baseUrl": "https://api.example.com/mcp",
"headers": {
"X-Custom-Header": "custom-value"
},
"authToken": "Bearer token123",
"timeout": 30
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService {
name,
url,
headers,
timeout,
..
} => {
assert_eq!(name, "remote-service");
assert_eq!(url, "https://api.example.com/mcp");
assert_eq!(
headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
assert_eq!(
headers.get("Authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(timeout, Some(30));
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
/// 测试 4: 解析远程 URL 配置(使用 url 字段)
#[test]
fn test_parse_remote_service_config_url() {
let config_json = r#"{
"mcpServers": {
"sse-service": {
"url": "https://api.example.com/sse",
"type": "sse"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService {
name,
url,
protocol,
..
} => {
assert_eq!(name, "sse-service");
assert_eq!(url, "https://api.example.com/sse");
assert_eq!(format!("{:?}", protocol), "Some(Sse)"); // SSE 协议
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
/// 测试 5: 多服务配置,使用 --name 参数选择
#[test]
fn test_parse_multi_server_config_with_name() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node",
"args": ["a.js"]
},
"service-b": {
"command": "python",
"args": ["b.py"]
},
"service-c": {
"baseUrl": "https://api.example.com/mcp"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: Some("service-b".to_string()),
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand { name, command, .. } => {
assert_eq!(name, "service-b");
assert_eq!(command, "python");
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 6: 多服务配置,未指定 --name 参数应该失败
#[test]
fn test_parse_multi_server_config_without_name_should_fail() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node"
},
"service-b": {
"command": "python"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None, // 未指定 name
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "多服务配置未指定 --name 应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("请使用 --name 指定"),
"错误消息应该提示使用 --name"
);
}
/// 测试 7: 指定的服务不存在应该失败
#[test]
fn test_parse_nonexistent_service_should_fail() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: Some("nonexistent".to_string()), // 不存在的服务
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "不存在的服务应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("'nonexistent' 不存在"),
"错误消息应该提示服务不存在"
);
}
/// 测试 8: 空配置应该失败
#[test]
fn test_parse_empty_config_should_fail() {
let config_json = r#"{
"mcpServers": {}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "空配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("没有找到任何 MCP 服务"),
"错误消息应该提示没有服务"
);
}
/// 测试 9: URL 配置缺少 url 和 baseUrl 应该失败
#[test]
fn test_parse_url_config_without_url_should_fail() {
let config_json = r#"{
"mcpServers": {
"invalid-service": {
"type": "sse"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "缺少 URL 的配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("缺少 url 或 baseUrl"),
"错误消息应该提示缺少 URL"
);
}
/// 测试 10: 直接 URL 模式(不使用 JSON 配置)
#[test]
fn test_parse_direct_url_mode() {
let args = ConvertArgs {
url: Some("https://api.example.com/mcp".to_string()),
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::DirectUrl { url } => {
assert_eq!(url, "https://api.example.com/mcp");
}
_ => panic!("应该解析为 DirectUrl 类型"),
}
}
/// 测试 11: 既没有 URL 也没有配置应该失败
#[test]
fn test_parse_no_url_no_config_should_fail() {
let args = ConvertArgs {
url: None,
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "既没有 URL 也没有配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("必须提供 URL"),
"错误消息应该提示需要提供配置"
);
}
/// 测试 12: 无效的 JSON 应该失败
#[test]
fn test_parse_invalid_json_should_fail() {
let invalid_json = r#"{
"mcpServers": {
"test": {
"command": "node"
}
// 缺少闭合括号
}"#;
let args = ConvertArgs {
url: None,
config: Some(invalid_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "无效的 JSON 应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("配置解析失败"),
"错误消息应该提示解析失败"
);
}
/// 测试 13: Stream 协议类型解析
#[test]
fn test_parse_stream_protocol_type() {
let config_json = r#"{
"mcpServers": {
"stream-service": {
"url": "https://api.example.com/mcp",
"type": "stream"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService { protocol, .. } => {
assert_eq!(format!("{:?}", protocol), "Some(Stream)");
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
}

View File

@@ -0,0 +1,211 @@
//! 诊断和错误处理
//!
//! 提供错误分类、诊断报告生成等功能
/// 错误分类
pub fn classify_error(e: &anyhow::Error) -> String {
let err_str = e.to_string().to_lowercase();
// 特殊识别 30 秒超时(可能是服务器限制)
if (err_str.contains("30") || err_str.contains("thirty"))
&& (err_str.contains("timeout") || err_str.contains("second") || err_str.contains(""))
{
"30-second timeout".to_string()
}
// 识别 503 服务不可用
else if err_str.contains("503") || err_str.contains("service unavailable") {
"503 Service Unavailable".to_string()
}
// 识别其他 HTTP 5xx 错误
else if err_str.contains("500") || err_str.contains("internal server error") {
"500 Internal Server Error".to_string()
} else if err_str.contains("502") || err_str.contains("bad gateway") {
"502 Bad Gateway".to_string()
} else if err_str.contains("504") || err_str.contains("gateway timeout") {
"504 Gateway Timeout".to_string()
}
// 识别 HTTP 4xx 错误
else if err_str.contains("401") || err_str.contains("unauthorized") {
"401 Unauthorized".to_string()
} else if err_str.contains("403") || err_str.contains("forbidden") {
"403 Forbidden".to_string()
} else if err_str.contains("404") || err_str.contains("not found") {
"404 Not Found".to_string()
} else if err_str.contains("408") || err_str.contains("request timeout") {
"408 Request Timeout".to_string()
}
// 通用超时
else if err_str.contains("timeout") || err_str.contains("timed out") {
"Timeout".to_string()
}
// 连接相关错误
else if err_str.contains("connection refused") {
"Connection Refused".to_string()
} else if err_str.contains("connection reset") {
"Connection Reset".to_string()
} else if err_str.contains("eof") || err_str.contains("closed") || err_str.contains("shutdown")
{
"Connection Closed".to_string()
}
// 网络相关错误
else if err_str.contains("dns") || err_str.contains("resolve") {
"DNS Resolution Failed".to_string()
} else if err_str.contains("certificate") || err_str.contains("ssl") || err_str.contains("tls")
{
"SSL/TLS Error".to_string()
} else if err_str.contains("sending request") || err_str.contains("network") {
"Network Error".to_string()
}
// 会话相关
else if err_str.contains("session") {
"Session Error".to_string()
} else {
"Unknown Error".to_string()
}
}
/// 简化错误信息(用于单行日志)
pub fn summarize_error(e: &anyhow::Error) -> String {
let full = e.to_string();
// 截取第一行或前80个字符
let first_line = full.lines().next().unwrap_or(&full);
// 使用 chars() 安全处理 UTF-8 字符,避免在多字节字符中间截断
if first_line.chars().count() > 80 {
format!("{}...", first_line.chars().take(77).collect::<String>())
} else {
first_line.to_string()
}
}
/// 生成诊断报告
pub fn print_diagnostic_report(
protocol: &str,
url: &str,
alive_duration_secs: u64,
disconnect_reason: &str,
error_type: Option<&str>,
diagnostic: bool,
) {
if !diagnostic {
return;
}
eprintln!("\n=== Connection Diagnostic Report ===");
eprintln!("Protocol: {protocol}");
// 隐藏 URL 中的敏感信息(如 token/ak/key/secret 参数)
let masked_url = if url.contains("?") {
let parts: Vec<&str> = url.split('?').collect();
if parts.len() == 2 {
let base = parts[0];
let params: Vec<&str> = parts[1].split('&').collect();
let masked_params: Vec<String> = params
.iter()
.map(|p| {
let lower = p.to_lowercase();
let key_part = lower.split('=').next().unwrap_or("");
if key_part.contains("key")
|| key_part.contains("token")
|| key_part.contains("secret")
|| key_part.contains("auth")
|| key_part.contains("password")
|| key_part.contains("passwd")
|| key_part.contains("credential")
|| key_part == "ak"
|| key_part == "sk"
{
let original_key = p.split('=').next().unwrap_or("");
format!("{}=***", original_key)
} else {
p.to_string()
}
})
.collect();
format!("{}?{}", base, masked_params.join("&"))
} else {
url.to_string()
}
} else {
url.to_string()
};
eprintln!("Service URL: {masked_url}");
eprintln!("Connection duration: {}s", alive_duration_secs);
eprintln!("Disconnect reason: {disconnect_reason}");
if let Some(err_type) = error_type {
eprintln!("Error type: {err_type}");
}
// 分析可能的原因
eprintln!("\nPossible causes:");
if (28..=32).contains(&alive_duration_secs) {
eprintln!(" Connection dropped around 30 seconds:");
eprintln!(" 1. The backend may enforce a fixed session timeout");
eprintln!(" 2. A load balancer or gateway may be closing idle connections");
eprintln!(" 3. Keepalive/ping settings may be too weak for this environment");
} else if alive_duration_secs < 10 {
eprintln!(" Quick disconnect ({}s):", alive_duration_secs);
eprintln!(" 1. Service may be unavailable or misconfigured");
eprintln!(" 2. Authentication/headers may be invalid");
eprintln!(" 3. URL/path may point to a non-MCP endpoint");
} else if alive_duration_secs >= 60 {
eprintln!(" Long-lived connection ({}s):", alive_duration_secs);
eprintln!(" 1. Disconnect may be caused by transient network instability");
eprintln!(" 2. Backend restarts or rolling deployments may interrupt sessions");
}
let timeout_30s = "30-second timeout";
let service_unavailable = "503 Service Unavailable";
if error_type == Some(timeout_30s) || error_type == Some(service_unavailable) {
eprintln!("\nSuggestions:");
eprintln!(" 1. Increase backend/read timeout settings");
eprintln!(" 2. Increase client ping timeout if the backend is slow");
eprintln!(" 3. Consider async/task-based invocation patterns");
eprintln!(" 4. Increase ping interval to {}s to reduce pressure", 120);
} else if disconnect_reason.contains("Ping") || disconnect_reason.contains("ping") {
eprintln!("\nSuggestions:");
eprintln!(" 1. Increase ping timeout to {}s", 30);
eprintln!(" 2. Increase ping interval to {}s", 60);
eprintln!(" 3. Disable ping if the backend does not support stable probes");
}
eprintln!("==============================\n");
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
#[test]
fn test_classify_error() {
assert_eq!(classify_error(&anyhow!("connection timeout")), "Timeout");
assert_eq!(
classify_error(&anyhow!("connection refused")),
"Connection Refused"
);
assert_eq!(
classify_error(&anyhow!("503 Service Unavailable")),
"503 Service Unavailable"
);
assert_eq!(
classify_error(&anyhow!("401 Unauthorized")),
"401 Unauthorized"
);
}
#[test]
fn test_summarize_error() {
let short_err = anyhow!("short error");
assert_eq!(summarize_error(&short_err), "short error");
let long_err = anyhow!(
"this is a very long error message that exceeds eighty characters and should be truncated"
);
let summary = summarize_error(&long_err);
assert!(summary.len() <= 80);
assert!(summary.ends_with("..."));
}
}

View File

@@ -0,0 +1,268 @@
//! 日志系统初始化
//!
//! 处理日志文件的创建、日志级别配置和 OpenTelemetry 追踪初始化
use anyhow::Result;
use mcp_common::{TracingConfig, TracingGuard};
use once_cell::sync::OnceCell;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use super::args::{ConvertArgs, LoggingArgs};
/// 全局追踪守卫,保持 OTLP exporter 存活
static TRACING_GUARD: OnceCell<TracingGuard> = OnceCell::new();
/// 初始化日志系统
/// 只有在 diagnostic=true 时才会创建日志文件并输出详细日志
pub fn init_logging(
args: &ConvertArgs,
mcp_name: Option<&str>,
quiet: bool,
verbose: bool,
) -> Result<()> {
init_logging_with_config(&args.logging, mcp_name, quiet, verbose)
}
/// 使用日志配置初始化日志系统(更通用的接口)
///
/// 这个函数可以被任何需要日志初始化的地方调用,不依赖完整的 ConvertArgs
///
/// # 功能
///
/// - 根据 `diagnostic` 参数控制日志级别
/// - 支持日志文件输出
/// - 支持 OTLP 追踪(当配置了 `otlp_endpoint` 时)
pub fn init_logging_with_config(
logging: &LoggingArgs,
mcp_name: Option<&str>,
quiet: bool,
verbose: bool,
) -> Result<()> {
// 检查是否需要启用 OTLP 追踪
let enable_otlp = logging.diagnostic && logging.otlp_endpoint.is_some();
// 如果启用 OTLP先初始化 tracer provider
if enable_otlp {
init_otlp_tracing(logging, mcp_name, quiet)?;
}
// 只有在 diagnostic=true 或用户明确指定日志文件时才创建日志文件
let log_file_path = determine_log_file_path(logging, mcp_name)?;
// 根据不同的配置组合初始化 subscriber
match (log_file_path, enable_otlp) {
(Some(file_path), true) => {
init_with_file_and_otlp(logging, &file_path, quiet, verbose)?;
}
(Some(file_path), false) => {
init_with_file_only(logging, &file_path, quiet, verbose)?;
}
(None, true) => {
init_stderr_with_otlp(logging, quiet, verbose)?;
}
(None, false) => {
init_stderr_only(logging, quiet, verbose)?;
}
}
Ok(())
}
/// 确定日志文件路径
fn determine_log_file_path(
logging: &LoggingArgs,
mcp_name: Option<&str>,
) -> Result<Option<std::path::PathBuf>> {
if let Some(log_file) = &logging.log_file {
// 手动指定文件
Ok(Some(log_file.clone()))
} else if let Some(log_dir) = &logging.log_dir {
// 指定日志目录
let session_id = generate_session_id();
let date = chrono::Local::now().format("%Y%m%d");
let name_part = mcp_name.unwrap_or("unknown");
let filename = format!("mcp-proxy-{}-{}-{}.log", name_part, date, session_id);
std::fs::create_dir_all(log_dir)
.map_err(|e| anyhow::anyhow!("Failed to create log directory: {}", e))?;
Ok(Some(log_dir.join(filename)))
} else if logging.diagnostic {
// diagnostic=true 时,使用系统临时目录
let session_id = generate_session_id();
let date = chrono::Local::now().format("%Y%m%d");
let name_part = mcp_name.unwrap_or("unknown");
let filename = format!("mcp-proxy-{}-{}-{}.log", name_part, date, session_id);
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join(filename);
// 尝试创建文件,验证目录可写
std::fs::File::create(&file_path).map_err(|e| {
anyhow::anyhow!(
"Failed to create log file: {} (path: {})",
e,
file_path.display()
)
})?;
Ok(Some(file_path))
} else {
Ok(None)
}
}
/// 创建日志过滤器
fn create_filter(logging: &LoggingArgs, verbose: bool) -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
if logging.diagnostic {
EnvFilter::new("debug")
} else if verbose {
EnvFilter::new("info")
} else {
EnvFilter::new("warn")
}
})
}
/// 初始化:文件 + OTLP
fn init_with_file_and_otlp(
logging: &LoggingArgs,
file_path: &std::path::Path,
quiet: bool,
verbose: bool,
) -> Result<()> {
let file = std::fs::File::create(file_path)
.map_err(|e| anyhow::anyhow!("Failed to create log file: {}", e))?;
if !quiet {
eprintln!("📝 Log file: {}", file_path.display());
eprintln!(
"📋 Diagnostic mode: {} (log level: {})",
if logging.diagnostic {
"enabled"
} else {
"disabled"
},
if logging.diagnostic { "DEBUG" } else { "WARN" }
);
}
let file_shared = std::sync::Arc::new(file);
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(
fmt::layer()
.with_writer(std::sync::Mutex::new(file_shared.clone()))
.with_ansi(false),
)
.with(fmt::layer().with_writer(std::io::stderr).with_ansi(true))
.with(tracing_opentelemetry::layer())
.init();
Ok(())
}
/// 初始化:仅文件
fn init_with_file_only(
logging: &LoggingArgs,
file_path: &std::path::Path,
quiet: bool,
verbose: bool,
) -> Result<()> {
let file = std::fs::File::create(file_path)
.map_err(|e| anyhow::anyhow!("Failed to create log file: {}", e))?;
if !quiet {
eprintln!("📝 Log file: {}", file_path.display());
eprintln!(
"📋 Diagnostic mode: {} (log level: {})",
if logging.diagnostic {
"enabled"
} else {
"disabled"
},
if logging.diagnostic { "DEBUG" } else { "WARN" }
);
}
let file_shared = std::sync::Arc::new(file);
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(
fmt::layer()
.with_writer(std::sync::Mutex::new(file_shared.clone()))
.with_ansi(false),
)
.with(fmt::layer().with_writer(std::io::stderr).with_ansi(true))
.init();
Ok(())
}
/// 初始化stderr + OTLP
fn init_stderr_with_otlp(logging: &LoggingArgs, quiet: bool, verbose: bool) -> Result<()> {
if !quiet && !logging.diagnostic {
eprintln!("📋 Diagnostic mode: disabled (no log file will be created)");
}
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_writer(std::io::stderr))
.with(tracing_opentelemetry::layer())
.init();
Ok(())
}
/// 初始化:仅 stderr
fn init_stderr_only(logging: &LoggingArgs, quiet: bool, verbose: bool) -> Result<()> {
if !quiet && !logging.diagnostic {
eprintln!("📋 Diagnostic mode: disabled (no log file will be created)");
}
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_writer(std::io::stderr))
.init();
Ok(())
}
/// 初始化 OTLP 追踪Jaeger 等)
fn init_otlp_tracing(logging: &LoggingArgs, mcp_name: Option<&str>, quiet: bool) -> Result<()> {
if let Some(endpoint) = &logging.otlp_endpoint {
let service_name = mcp_name.unwrap_or(&logging.service_name);
let config = TracingConfig::new(service_name)
.with_otlp(endpoint)
.with_version(env!("CARGO_PKG_VERSION"));
let guard = mcp_common::init_tracing(&config)?;
// 保存到全局静态变量,确保 guard 在程序运行期间保持存活
let _ = TRACING_GUARD.set(guard);
if !quiet {
eprintln!("🔭 OTLP tracing: enabled");
eprintln!(" Endpoint: {}", endpoint);
eprintln!(" Service: {}", service_name);
}
}
Ok(())
}
/// 生成随机会话 ID8 位十六进制)
pub fn generate_session_id() -> String {
use rand::Rng;
let mut rng = rand::rng();
format!("{:08x}", rng.random::<u32>())
}

View File

@@ -0,0 +1,19 @@
//! 支持功能模块
//!
//! 提供配置、日志、工具函数等基础设施支持
pub mod args;
pub mod config;
pub mod diagnostic;
pub mod logging;
pub mod utils;
#[cfg(test)]
mod config_tests;
// 导出常用类型
pub use args::{CheckArgs, ConvertArgs, DetectArgs, HealthArgs, LoggingArgs};
pub use config::{McpConfigSource, merge_headers, parse_convert_config};
pub use diagnostic::{classify_error, print_diagnostic_report, summarize_error};
pub use logging::{init_logging, init_logging_with_config};
pub use utils::{protocol_name, truncate_str};

View File

@@ -0,0 +1,40 @@
//! 通用工具函数
/// 获取协议名称
pub fn protocol_name(protocol: &crate::client::protocol::McpProtocol) -> &'static str {
match protocol {
crate::client::protocol::McpProtocol::Sse => "SSE",
crate::client::protocol::McpProtocol::Stream => "Streamable HTTP",
crate::client::protocol::McpProtocol::Stdio => "Stdio",
}
}
/// 截断字符串UTF-8 安全)
pub fn truncate_str(s: &str, max_len: usize) -> String {
if s.chars().count() > max_len {
format!("{}...", s.chars().take(max_len - 3).collect::<String>())
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::protocol::McpProtocol;
#[test]
fn test_protocol_name() {
assert_eq!(protocol_name(&McpProtocol::Sse), "SSE");
assert_eq!(protocol_name(&McpProtocol::Stream), "Streamable HTTP");
assert_eq!(protocol_name(&McpProtocol::Stdio), "Stdio");
}
#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("你好世界", 3), "...");
assert_eq!(truncate_str("你好世界", 6), "你好世界");
}
}

View File

@@ -0,0 +1,874 @@
// MCP 客户端模块测试 - 集成测试
// ============== 共享测试工具 ==============
#[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; // integration_tests 使用
pub const TEST_PORT_PROTOCOL: u16 = 19881; // protocol detection 使用
pub const TEST_PORT_RECONNECT: u16 = 19876; // reconnection_tests 使用
/// 获取预编译的 test_mcp_server 二进制路径
///
/// 注意test_mcp_server 现在是 mcp-sse-proxy 的 example
/// 编译输出在 target/debug/examples/test_mcp_server
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().unwrap();
workspace_root
.join("target/debug/examples/test_mcp_server")
.to_string_lossy()
.to_string()
}
/// 获取预编译的 mcp-proxy 二进制路径
pub fn get_mcp_proxy_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir).parent().unwrap();
workspace_root
.join("target/debug/mcp-proxy")
.to_string_lossy()
.to_string()
}
/// 创建 MCP 服务配置 JSON使用预编译二进制
pub fn create_test_config() -> String {
let binary_path = get_test_mcp_server_path();
json!({
"mcpServers": {
"test-server": {
"command": binary_path,
"args": []
}
}
})
.to_string()
}
/// 启动 proxy 服务器
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)
}
/// 等待服务器就绪TCP 轮询)
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)
}
/// 启动 convert 客户端进程
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)
}
/// 监控 stderr 输出,查找任一日志模式
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, // EOF
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), // timeout
}
}
/// 发送 JSON-RPC 请求并获取响应
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)
}
/// 初始化 MCP 客户端(发送 initialize + initialized
pub async fn initialize_mcp_client(
stdin: &mut tokio::process::ChildStdin,
stdout: &mut BufReader<tokio::process::ChildStdout>,
) -> anyhow::Result<()> {
// 发送 initialize 请求
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"]);
}
// 发送 initialized 通知
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;
/// 测试本地 MCP 服务连接和通信
///
/// 使用本地 test-mcp-server + mcp-proxy proxy 进行测试
/// 验证完整的 MCP 通信流程initialize -> tools/list -> tools/call
#[tokio::test]
async fn test_real_mcp_service_communication() {
println!("\\n========== Test: MCP service connection and communication ==========");
// 1. 启动本地 proxy 服务器
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("服务器启动超时");
// 等待 proxy 完全初始化后端连接
tokio::time::sleep(Duration::from_secs(3)).await;
// 2. 启动 convert 客户端连接到本地 proxy
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;
}
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送 tools/list 请求
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 工具");
// 5. 测试调用 echo 工具
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);
// 6. 测试调用 increment 工具
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");
}
/// 测试协议检测功能
///
/// 使用本地 mcp-proxy proxy 服务测试协议检测
/// 本地 proxy 默认使用 Streamable HTTP 协议
#[tokio::test]
async fn test_protocol_detection() {
println!("\\n========== Test: Protocol Detection ==========");
// 1. 启动本地 proxy 服务器Streamable HTTP 模式)
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("服务器启动超时");
// 2. 测试协议检测
println!("🔍Detect protocol type...");
let url = format!("http://{}", addr);
let protocol = crate::client::protocol::detect_mcp_protocol(&url).await;
assert!(protocol.is_ok(), "协议检测应该成功");
let protocol = protocol.unwrap();
use crate::client::protocol::McpProtocol;
// 本地 proxy 默认使用 Streamable HTTP
assert_eq!(
protocol,
McpProtocol::Stream,
"应该检测到 Streamable HTTP 协议"
);
println!("✅ Protocol detected: {:?}", protocol);
// 清理
println!("🧹 Cleaning process...");
let _ = proxy.kill().await;
println!("========== Test completed ==========\\n");
}
}
/// 本地重连测试模块
///
/// 使用 `mcp-proxy proxy` 启动本地服务,`mcp-proxy convert` 连接测试
/// 验证通道检查和自动重连逻辑
#[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);
/// 测试 1: 正常连接和通信
///
/// 验证:
/// - proxy 服务启动正常
/// - convert 客户端连接成功
/// - tools/list 请求正常响应
#[tokio::test]
async fn test_reconnection_normal_connection() {
println!("\\n========== Test 1: Normal connection and communication ==========");
// 1. 启动 proxy 服务器
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("服务器启动超时");
// 2. 启动 convert 客户端
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);
// 等待客户端连接成功(监控 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...");
}
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送 tools/list 请求
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 工具");
// 5. 测试调用 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");
}
/// 测试 2: 服务器重启后自动重连
///
/// 验证:
/// - 杀死 proxy 服务后convert 检测到断开
/// - 重启 proxy 后convert 自动重连
/// - 重连后功能正常
#[tokio::test]
async fn test_reconnection_on_server_restart() {
println!("\\n========== Test 2: Automatically reconnect after server restart ==========");
// 1. 启动 proxy 服务器
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("服务器启动超时");
// 2. 启动 convert 客户端(短 ping 间隔以快速检测断开)
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;
// 3. 杀死 proxy 服务器
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
// 4. 等待客户端检测到断开
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 失败");
// 也可能是 "Ping 检测" 或 "后端连接已关闭"
if !disconnected {
println!(
"⚠️ No clear disconnection log detected, check if there are any reconnection attempts..."
);
}
// 5. 重启 proxy 服务器
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("服务器重启超时");
// 6. 等待客户端重连成功
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");
}
/// 测试 3: 指数退避验证
///
/// 验证退避时间递增1s, 2s, 4s...
#[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");
}
/// 测试 4: 通道断开后请求是否立即返回错误
///
/// 验证:
/// - 服务器停止后,客户端发送请求能否立即返回错误
/// - 而不是空等超时
#[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 =========="
);
// 1. 启动 proxy 服务器
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("服务器启动超时");
// 2. 启动 convert 客户端(短 ping 间隔以快速检测断开)
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);
// 后台监控 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;
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送第一个 tools/list 请求确认通信正常
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
);
// 5. 杀死 proxy 服务器
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
// 等待 ping 检测发现连接断开ping 间隔是 2s超时也是 2s
println!("⏳ Wait for the ping to detect the disconnection (about 5 seconds)...");
tokio::time::sleep(Duration::from_secs(5)).await;
// 6. 发送第二个 tools/list 请求
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");
}
}