提交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");
}
}

View File

@@ -0,0 +1,106 @@
use anyhow::Result;
use serde::Deserialize;
use std::env;
use std::fs::File;
use std::path::Path;
/// The default config file
const DEFAULT_CONFIG_YAML: &str = include_str!("../config.yml");
/// config.yml 中 mirror 段的结构
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone, Default)]
pub struct MirrorYamlConfig {
#[serde(default)]
pub npm_registry: String,
#[serde(default)]
pub pypi_index_url: String,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
pub server: ServerConfig,
pub log: LogConfig,
#[serde(default)]
pub mirror: MirrorYamlConfig,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
/// The port to listen on for incoming connections
pub port: u16,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct LogConfig {
/// The log level to use
pub level: String,
/// The path to the log file
pub path: String,
/// The number of log files to retain (default: 20)
#[serde(default = "default_retain_days")]
pub retain_days: u32,
}
/// Default log files to retain
fn default_retain_days() -> u32 {
5
}
#[allow(dead_code)]
impl AppConfig {
/// Load the config file from the following sources:
/// 1. /app/config.yml
/// 2. config.yml
/// 3. BOT_SERVER_CONFIG environment variable
///
/// Environment variables can override config values:
/// - MCP_PROXY_PORT: Override server port
/// - MCP_PROXY_LOG_DIR: Override log directory path
/// - MCP_PROXY_LOG_LEVEL: Override log level
pub fn load_config() -> Result<Self> {
let mut config = match (
File::open("/app/config.yml"),
File::open("config.yml"),
env::var("BOT_SERVER_CONFIG"),
) {
(Ok(file), _, _) => serde_yaml::from_reader(file),
(_, Ok(file), _) => serde_yaml::from_reader(file),
(_, _, Ok(file_path)) => serde_yaml::from_reader(File::open(file_path)?),
_ => {
// 如果都没有,则使用默认配置
serde_yaml::from_str::<AppConfig>(DEFAULT_CONFIG_YAML)
}
}?;
// 环境变量覆盖配置(优先级最高)
if let Ok(port) = env::var("MCP_PROXY_PORT")
&& let Ok(port_num) = port.parse::<u16>()
{
config.server.port = port_num;
}
if let Ok(log_dir) = env::var("MCP_PROXY_LOG_DIR") {
config.log.path = log_dir;
}
if let Ok(log_level) = env::var("MCP_PROXY_LOG_LEVEL") {
config.log.level = log_level;
}
Ok(config)
}
pub fn log_path_init(&self) -> Result<()> {
let log_path = &self.log.path;
// 获取日志文件的父目录
if let Some(parent) = Path::new(log_path).parent()
&& !parent.exists()
{
std::fs::create_dir_all(parent)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,44 @@
//! 进程环境初始化
//!
//! 在 mcp-proxy 启动早期调用,设置进程级环境变量:
//! - 镜像源npm_config_registry / UV_INDEX_URL
use crate::config::{AppConfig, MirrorYamlConfig};
/// 初始化进程环境(镜像源)
///
/// 在 main() 启动早期、日志初始化前调用。
/// 设置的环境变量会被所有子进程npx/uvx 等)自动继承。
pub fn init(app_config: &AppConfig) {
// 1. 镜像源配置
init_mirror(&app_config.mirror);
// 2. 汇总诊断日志
mcp_common::diagnostic::eprint_env_summary();
}
/// 从 config.yml + 环境变量合并镜像配置,设为进程级环境变量
fn init_mirror(yml: &MirrorYamlConfig) {
let mut config = mcp_common::mirror::MirrorConfig::from_env();
// config.yml 非空值作为默认(环境变量优先级更高)
if config.npm_registry.is_none() && !yml.npm_registry.is_empty() {
config.npm_registry = Some(yml.npm_registry.clone());
}
if config.pypi_index_url.is_none() && !yml.pypi_index_url.is_empty() {
config.pypi_index_url = Some(yml.pypi_index_url.clone());
}
if config.is_empty() {
eprintln!(" - Mirror: not configured");
return;
}
if let Some(ref npm) = config.npm_registry {
eprintln!(" - npm registry: {npm}");
}
if let Some(ref pypi) = config.pypi_index_url {
eprintln!(" - PyPI index: {pypi}");
}
config.apply_to_process_env();
}

View File

@@ -0,0 +1,40 @@
// 初始化 i18n使用 crate 内置翻译文件
#[macro_use]
extern crate rust_i18n;
// 初始化翻译文件,使用 crate 内置 locales支持独立发布
i18n!("locales", fallback = "en");
mod client;
mod config;
pub mod env_init;
mod mcp_error;
mod model;
mod proxy;
mod server;
#[cfg(test)]
mod tests;
// 导出基础功能
pub use config::AppConfig;
pub use mcp_error::AppError;
pub use model::{
AppState, DynamicRouterService, McpConfig, McpProtocol, McpType, ProxyHandlerManager,
get_proxy_manager,
};
pub use proxy::{McpHandler, ProxyHandler, StreamProxyHandler};
pub use proxy::{SseBackendConfig, SseServerBuilder, StreamBackendConfig, StreamServerBuilder};
pub use server::{
create_telemetry_layer, get_health, get_ready, get_router, init_tracer_provider,
log_service_info, mcp_start_task, schedule_check_mcp_live, set_layer, shutdown_telemetry,
start_schedule_task,
};
// 导出 CLI 功能
pub use client::{Cli, Commands, run_cli};
// 导出 i18n 功能
pub use mcp_common::{current_locale, init_locale_from_env, set_locale, t};
// 导出用于基准测试的组件
pub use server::handlers::run_code_handler::{RunCodeMessageRequest, run_code_handler};

View File

@@ -0,0 +1,355 @@
// Windows 平台配置:隐藏控制台窗口
// 当从 Tauri 等 GUI 应用启动时,不显示 CMD 窗口
// 注意:这会影响所有 Windows 平台的运行,独立运行时也不会有控制台输出
// 日志会写入文件(默认 ./logs/),可以通过日志文件查看运行状态
mod config;
use anyhow::Result;
use backtrace::Backtrace;
use clap::Parser;
use log::{error, info, warn};
use mcp_stdio_proxy::{
AppConfig, AppState, Cli, get_proxy_manager, get_router, init_locale_from_env,
init_tracer_provider, log_service_info, run_cli, start_schedule_task,
};
use run_code_rmcp::warm_up_all_envs;
use tokio::net::TcpListener;
use tokio::signal;
use tracing_appender::rolling::{Builder, Rotation};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::{EnvFilter, Layer as _};
#[tokio::main]
async fn main() -> Result<()> {
// 在最早期注册 panic hook确保 panic 信息一定输出到 stderr
// 当作为 stdio 子进程运行时(如 mcp-proxy convert父进程通过 stderr pipe 捕获此输出
std::panic::set_hook(Box::new(|info| {
let msg = if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else if let Some(s) = info.payload().downcast_ref::<&str>() {
s.to_string()
} else {
"unknown panic".to_string()
};
let location = info
.location()
.map(|l| format!(" at {}:{}", l.file(), l.line()))
.unwrap_or_default();
eprintln!("❌ PANIC{}: {}", location, msg);
}));
// 初始化 Rustls CryptoProvider必须在任何使用 TLS 的代码之前)
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
// 解析命令行参数
let cli = Cli::parse();
// 如果有子命令,运行 CLI 模式
if cli.command.is_some() || cli.url.is_some() {
return run_cli_mode(cli).await;
}
// 否则运行传统的服务器模式
run_server_mode().await
}
/// 运行 CLI 模式
async fn run_cli_mode(cli: Cli) -> Result<()> {
// 初始化语言设置
init_locale_from_env();
// 检查是否是需要自定义日志初始化的命令
// convert 和 proxy 命令会根据自己的参数(--log-dir、--log-file初始化日志所以这里跳过
let is_convert_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Convert(_)));
let is_proxy_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Proxy(_)));
let is_health_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Health(_)));
let has_custom_logging = is_convert_command || is_proxy_command;
// CLI 模式独立的日志配置
// 跳过会自己初始化日志的命令,避免重复初始化导致 panic
if !has_custom_logging && !cli.quiet {
// CLI 模式默认只显示错误,避免 info/debug 日志污染输出
let log_level = if cli.verbose {
"debug"
} else if is_health_command {
// health 命令使用更严格的过滤,屏蔽 rmcp 库的噪音日志
"off"
} else {
"error" // 默认只显示错误,屏蔽 info/warn/debug
};
// CLI 模式的日志配置:
// 1. 禁用 ANSI 颜色(避免污染 JSON
// 2. 输出到 stderrstdout 用于 JSON-RPC 通信)
// 3. 简化格式(无时间戳、无目标)
// 4. 优先使用 RUST_LOG 环境变量,否则使用默认日志级别
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(std::io::stderr)
.compact()
.init();
}
// 运行 CLI 命令
let result = run_cli(cli).await;
if let Err(ref e) = result {
// 确保错误信息在进程退出前写到 stderr方便排查 stdio bridge 启动失败问题
eprintln!("❌ CLI command failed: {:?}", e);
}
result
}
/// 运行传统的服务器模式
async fn run_server_mode() -> Result<()> {
// 初始化语言设置
init_locale_from_env();
// 配置日志(保持原有的完整日志配置)
let app_config = AppConfig::load_config()?;
// 打印配置信息到 stderr在日志系统初始化之前
eprintln!("========================================");
eprintln!("Starting MCP proxy service...");
eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
eprintln!("Configuration loaded:");
eprintln!(" - Port: {}", app_config.server.port);
eprintln!(" - Log directory: {}", &app_config.log.path);
eprintln!(" - Log level: {}", &app_config.log.level);
eprintln!(" - Log retention days: {}", app_config.log.retain_days);
mcp_stdio_proxy::env_init::init(&app_config);
eprintln!("========================================");
app_config.log_path_init()?;
let log_level = app_config.log.level.clone();
let log_path = app_config.log.path.clone();
let server_port = app_config.server.port;
let retain_days = app_config.log.retain_days;
// 解析 RUST_LOG 环境变量
let log_level_for_console = log_level.clone();
let mut console_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level_for_console));
// 修复 rmcp 库的 span clone panic 问题
// 过滤掉 rmcp 的 trace/debug 级别日志,只保留 warn/error避免 span 生命周期管理问题
// see: https://github.com/tokio-rs/tracing/issues/2778
console_filter = console_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
// 使用 tracing-subscriber 初始化日志记录器
let console_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_writer(std::io::stdout)
.with_filter(console_filter);
// 日志写入到文件,使用 Builder 模式配置日志轮转和保留策略
let log_path_for_file = log_path.clone();
let file_appender = Builder::new()
.rotation(Rotation::DAILY) // 按天滚动
.filename_prefix("log") // 文件名前缀
.max_log_files(retain_days as usize) // 保留最近 N 个日志文件
.build(&log_path_for_file)?;
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let mut log_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
// 修复 rmcp 库的 span clone panic 问题(同样应用于文件日志)
log_filter = log_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
// 配置文件日志层:使用 compact 格式,避免显示完整的 span 嵌套链,减少日志膨胀
let file_layer = tracing_subscriber::fmt::layer()
.compact()
.with_ansi(false)
.with_writer(non_blocking)
.with_filter(log_filter);
// 初始化 OpenTelemetry tracer provider
init_tracer_provider("mcp-proxy", "0.1.0")?;
// 修复 rmcp 库的 span clone panic 问题(同样应用于 OpenTelemetry 层)
// 只为 warn 和以上级别创建 span避免过多的 span 导致 clone 问题
let telemetry_filter = EnvFilter::new("warn")
.add_directive("mcp_proxy=debug".parse().unwrap())
.add_directive("mcp_stdio_proxy=debug".parse().unwrap())
.add_directive("rmcp=error".parse().unwrap())
.add_directive("run_code_rmcp=error".parse().unwrap());
// 配置 OpenTelemetry添加过滤器以避免 span clone panic
let telemetry_layer = tracing_opentelemetry::layer().with_filter(telemetry_filter);
// 初始化 tracing 订阅器
tracing_subscriber::registry()
.with(console_layer)
.with(file_layer)
.with(telemetry_layer)
.init();
// 记录服务信息
log_service_info("mcp-proxy", "0.1.0")?;
tracing::info!("========================================");
tracing::info!("Starting MCP proxy service...");
tracing::info!("Running in proxy server mode");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Configuration:");
tracing::info!(" - Listen port: {}", server_port);
tracing::info!(" - Log directory: {}", log_path);
tracing::info!(" - Log level: {}", &app_config.log.level);
tracing::info!(" - Log retention days: {}", retain_days);
tracing::info!("Environment overrides:");
if std::env::var("MCP_PROXY_PORT").is_ok() {
tracing::info!(" - MCP_PROXY_PORT override detected: {}", server_port);
}
if let Ok(log_dir) = std::env::var("MCP_PROXY_LOG_DIR") {
tracing::info!(" - MCP_PROXY_LOG_DIR override detected: {}", log_dir);
}
if let Ok(level) = std::env::var("MCP_PROXY_LOG_LEVEL") {
tracing::info!(" - MCP_PROXY_LOG_LEVEL override detected: {}", level);
}
tracing::info!("========================================");
// 监听地址
let addr = format!("0.0.0.0:{server_port}");
tracing::info!("Binding to {addr}...");
let listener = TcpListener::bind(&addr).await.map_err(|e| {
tracing::error!("Failed to bind to {addr}: {e}");
e
})?;
tracing::info!("Successfully bound to {addr}");
// 构建 axum 路由
tracing::info!("Initializing application state...");
let state = AppState::new(app_config.clone()).await;
tracing::info!("Application state initialized");
// 初始化 MCP 路由
tracing::info!("Initializing MCP router...");
let app = get_router(state.clone()).await?;
tracing::info!("MCP router initialized");
info!("MCP proxy started: {addr}");
info!("Health endpoint: http://{addr}/health");
info!("MCP list endpoint: http://{addr}/mcp/list");
// 启动定时任务定期检查MCP服务状态
tokio::spawn(start_schedule_task());
info!("Background schedule task started");
info!("Log rotation configured (keep last {retain_days} files)");
// 打印系统信息
tracing::info!("System info:");
tracing::info!(" - OS: {}", std::env::consts::OS);
tracing::info!(" - Arch: {}", std::env::consts::ARCH);
tracing::info!(
" - Working directory: {:?}",
std::env::current_dir().unwrap_or_default()
);
// 注册关闭处理函数,确保在程序退出前执行清理
tokio::spawn(async move {
// 确保在程序退出前执行清理
std::panic::set_hook(Box::new(move |panic_info| {
// 记录详细的 panic 信息
warn!("Panic hook triggered");
// 记录 panic 消息
if let Some(s) = panic_info.payload().downcast_ref::<String>() {
error!("Panic reason: {s}");
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
error!("Panic reason: {s}");
} else {
error!("Panic reason: <unknown>");
}
// 记录 panic 位置
if let Some(location) = panic_info.location() {
error!("Panic location: {}:{}", location.file(), location.line());
}
// 尝试获取堆栈跟踪
error!("Panic backtrace:");
let backtrace = Backtrace::new();
error!("{backtrace:?}");
}));
});
// 预热 uv/deno 环境依赖
tokio::spawn(async move {
info!("Warming up uv/deno runtime dependencies...");
match warm_up_all_envs(None, None, None, None).await {
Ok(_) => info!("Runtime dependency warm-up completed"),
Err(e) => error!("Runtime dependency warm-up failed: {e}"),
}
});
// 启动服务器,监听多种信号以实现优雅关闭
info!("Starting HTTP server...");
let server =
axum::serve(listener, app.into_make_service()).with_graceful_shutdown(shutdown_signal());
// 运行服务器
if let Err(e) = server.await {
error!("HTTP server exited with error: {e}");
}
// 服务器关闭后执行清理逻辑
warn!("Shutdown signal received, starting cleanup...");
// 清理所有SSE服务
match get_proxy_manager().cleanup_all_resources().await {
Ok(_) => info!("Resource cleanup completed"),
Err(e) => error!("Resource cleanup failed: {e}"),
}
// 等待一小段时间确保所有资源都被清理
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
info!("Shutdown complete");
Ok(())
}
// 监听多种终止信号
async fn shutdown_signal() {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
}
#[cfg(test)]
mod tests {
#[test]
fn test_crypto_provider_install() {
// 测试 CryptoProvider 可以正常安装(或已被安装)
// 注意CryptoProvider 全局只能安装一次,多次安装会返回错误
let _ = rustls::crypto::ring::default_provider().install_default();
// 无论首次安装还是已安装,只要能获取到默认 provider 即表示成功
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(provider.is_some(), "CryptoProvider should be available");
}
#[test]
fn test_crypto_provider_get_default() {
// 首先确保 CryptoProvider 已安装(忽略已安装的错误)
let _ = rustls::crypto::ring::default_provider().install_default();
// 测试可以正常获取默认 CryptoProvider
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(
provider.is_some(),
"CryptoProvider should be available after installation"
);
}
}

View File

@@ -0,0 +1,245 @@
use axum::{
Json,
response::{IntoResponse, Response},
};
use http::StatusCode;
use mcp_common::t;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// MCP 应用错误类型
///
/// 包含错误码和详细错误信息,便于程序化处理
#[derive(Error, Debug)]
pub enum AppError {
/// 服务未找到 (0001)
#[error("{0}")]
ServiceNotFound(String),
/// 服务在重启冷却期内 (0002)
#[error("{0}")]
ServiceRestartCooldown(String),
/// 服务正在启动中 (0003)
#[error("{0}")]
ServiceStartupInProgress(String),
/// 服务启动失败 (0004)
#[error("{0}")]
ServiceStartupFailed(String),
/// 后端连接错误 (0005)
#[error("{0}")]
BackendConnection(String),
/// 配置解析错误 (0006)
#[error("{0}")]
ConfigParse(String),
/// MCP 服务器错误 (0007)
#[error("{0}")]
McpServerError(String),
/// JSON 序列化错误 (0008)
#[error("{0}")]
SerdeJsonError(String),
/// IO 错误 (0009)
#[error("{0}")]
IoError(String),
/// 路由未找到 (0010)
#[error("{0}")]
RouteNotFound(String),
/// 无效的请求参数 (0011)
#[error("{0}")]
InvalidParameter(String),
}
impl AppError {
// ===========================================
// 工厂方法 - 创建带国际化消息的错误
// ===========================================
/// 创建服务未找到错误
pub fn service_not_found(service: impl Into<String>) -> Self {
Self::ServiceNotFound(
t!(
"errors.mcp_proxy.service_not_found",
service = service.into()
)
.to_string(),
)
}
/// 创建服务重启冷却期错误
pub fn service_restart_cooldown(service: impl Into<String>) -> Self {
Self::ServiceRestartCooldown(
t!(
"errors.mcp_proxy.service_restart_cooldown",
service = service.into()
)
.to_string(),
)
}
/// 创建服务启动中错误
pub fn service_startup_in_progress(service: impl Into<String>) -> Self {
Self::ServiceStartupInProgress(
t!(
"errors.mcp_proxy.service_startup_in_progress",
service = service.into()
)
.to_string(),
)
}
/// 创建服务启动失败错误
pub fn service_startup_failed(mcp_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self::ServiceStartupFailed(
t!(
"errors.mcp_proxy.service_startup_failed",
mcp_id = mcp_id.into(),
reason = reason.into()
)
.to_string(),
)
}
/// 创建后端连接错误
pub fn backend_connection(detail: impl Into<String>) -> Self {
Self::BackendConnection(
t!(
"errors.mcp_proxy.backend_connection",
detail = detail.into()
)
.to_string(),
)
}
/// 创建配置解析错误
pub fn config_parse(detail: impl Into<String>) -> Self {
Self::ConfigParse(t!("errors.mcp_proxy.config_parse", detail = detail.into()).to_string())
}
/// 创建 MCP 服务器错误
pub fn mcp_server_error(detail: impl Into<String>) -> Self {
Self::McpServerError(
t!("errors.mcp_proxy.mcp_server_error", detail = detail.into()).to_string(),
)
}
/// 创建 JSON 序列化错误
pub fn json_serialization(detail: impl Into<String>) -> Self {
Self::SerdeJsonError(
t!(
"errors.mcp_proxy.json_serialization",
detail = detail.into()
)
.to_string(),
)
}
/// 创建 IO 错误
pub fn io_error(detail: impl Into<String>) -> Self {
Self::IoError(t!("errors.mcp_proxy.io_error", detail = detail.into()).to_string())
}
/// 创建路由未找到错误
pub fn route_not_found(path: impl Into<String>) -> Self {
Self::RouteNotFound(t!("errors.mcp_proxy.route_not_found", path = path.into()).to_string())
}
/// 创建无效参数错误
pub fn invalid_parameter(detail: impl Into<String>) -> Self {
Self::InvalidParameter(
t!("errors.mcp_proxy.invalid_parameter", detail = detail.into()).to_string(),
)
}
// ===========================================
// 错误码和状态码
// ===========================================
/// 获取错误码
pub fn error_code(&self) -> &'static str {
match self {
Self::ServiceNotFound(_) => "0001",
Self::ServiceRestartCooldown(_) => "0002",
Self::ServiceStartupInProgress(_) => "0003",
Self::ServiceStartupFailed { .. } => "0004",
Self::BackendConnection(_) => "0005",
Self::ConfigParse(_) => "0006",
Self::McpServerError(_) => "0007",
Self::SerdeJsonError(_) => "0008",
Self::IoError(_) => "0009",
Self::RouteNotFound(_) => "0010",
Self::InvalidParameter(_) => "0011",
}
}
/// 获取 HTTP 状态码
pub fn status_code(&self) -> StatusCode {
match self {
Self::ServiceNotFound(_) => StatusCode::NOT_FOUND,
Self::ServiceRestartCooldown(_) => StatusCode::TOO_MANY_REQUESTS,
Self::ServiceStartupInProgress(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::ServiceStartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::BackendConnection(_) => StatusCode::BAD_GATEWAY,
Self::ConfigParse(_) => StatusCode::BAD_REQUEST,
Self::McpServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::SerdeJsonError(_) => StatusCode::BAD_REQUEST,
Self::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::RouteNotFound(_) => StatusCode::NOT_FOUND,
Self::InvalidParameter(_) => StatusCode::BAD_REQUEST,
}
}
}
// ===========================================
// 从外部错误类型转换
// ===========================================
impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
Self::mcp_server_error(err.to_string())
}
}
impl From<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
Self::json_serialization(err.to_string())
}
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
Self::io_error(err.to_string())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorOutput {
pub code: String,
pub error: String,
}
impl ErrorOutput {
pub fn with_code(code: &'static str, error: impl Into<String>) -> Self {
Self {
code: code.to_string(),
error: error.into(),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response<axum::body::Body> {
(
self.status_code(),
Json(ErrorOutput::with_code(self.error_code(), self.to_string())),
)
.into_response()
}
}

View File

@@ -0,0 +1,33 @@
use std::{ops::Deref, sync::Arc};
use crate::AppConfig;
#[derive(Debug, Clone)]
pub struct AppState {
inner: Arc<AppStateInner>,
}
#[allow(unused)]
#[derive(Debug)]
pub struct AppStateInner {
pub addr: String,
pub config: AppConfig,
}
impl Deref for AppState {
type Target = AppStateInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AppState {
pub async fn new(config: AppConfig) -> Self {
Self {
inner: Arc::new(AppStateInner {
addr: format!("0.0.0.0:{}", config.server.port),
config,
}),
}
}
}

View File

@@ -0,0 +1,978 @@
use axum::Router;
use dashmap::DashMap;
use log::{debug, error, info};
use moka::future::Cache;
use once_cell::sync::Lazy;
use std::sync::Arc;
use tokio::sync::{Mutex, OwnedMutexGuard};
use tokio::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use tracing::warn;
use anyhow::Result;
use crate::proxy::McpHandler;
use super::{CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpRouterPath, McpType};
// 全局单例路由表
pub static GLOBAL_ROUTES: Lazy<Arc<DashMap<String, Router>>> =
Lazy::new(|| Arc::new(DashMap::new()));
// 全局单例 ProxyHandlerManager
pub static GLOBAL_PROXY_MANAGER: Lazy<ProxyHandlerManager> =
Lazy::new(ProxyHandlerManager::default);
/// 动态路由服务
#[derive(Clone)]
pub struct DynamicRouterService(pub McpProtocol);
impl DynamicRouterService {
// 注册动态 handler
pub fn register_route(path: &str, handler: Router) {
debug!("=== Register Route ===");
debug!("Registration path: {}", path);
GLOBAL_ROUTES.insert(path.to_string(), handler);
debug!("=== Route registration completed ===");
}
// 删除动态 handler
pub fn delete_route(path: &str) {
debug!("=== Delete route ===");
debug!("Delete path: {}", path);
GLOBAL_ROUTES.remove(path);
debug!("=== Route deletion completed ===");
}
// 获取动态 handler
pub fn get_route(path: &str) -> Option<Router> {
let result = GLOBAL_ROUTES.get(path).map(|entry| entry.value().clone());
if result.is_some() {
debug!("get_route('{}') = Some(Router)", path);
} else {
debug!("get_route('{}') = None", path);
}
result
}
// 获取所有已注册的路由debug用
pub fn get_all_routes() -> Vec<String> {
GLOBAL_ROUTES
.iter()
.map(|entry| entry.key().clone())
.collect()
}
}
impl std::fmt::Debug for DynamicRouterService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let routes = GLOBAL_ROUTES
.iter()
.map(|entry| entry.key().clone())
.collect::<Vec<_>>();
write!(f, "DynamicRouterService {{ routes: {routes:?} }}")
}
}
// =============================================================================
// RAII 进程管理设计
// =============================================================================
//
// 设计目标:当 mcp_id 从 map 中移除时,自动释放对应的 MCP 进程资源
//
// 核心结构:
// - McpProcessGuard: 进程生命周期守护器,实现 Drop trait 自动取消 CancellationToken
// - McpService: 封装 McpHandler + McpProcessGuard + 服务状态,作为 map 的 value
// - ProxyHandlerManager: 使用单一 DashMap<String, McpService> 管理所有服务
//
// 资源释放流程:
// 1. 从 map 中 remove mcp_id
// 2. McpService 被 drop
// 3. McpProcessGuard::drop() 被调用
// 4. CancellationToken 被 cancel
// 5. 监听该 token 的 SseServer/子进程收到信号,自动退出
// =============================================================================
/// MCP 进程生命周期守护器
///
/// 实现 RAII 模式:当此结构体被 drop 时,自动取消 CancellationToken
/// 触发关联的 SseServer 和子进程退出。
///
/// # 使用场景
///
/// 1. 从 `ProxyHandlerManager` 移除 mcp_id 时,自动清理进程
/// 2. 服务重启时,旧服务自动被清理
/// 3. 系统关闭时,所有服务自动清理
pub struct McpProcessGuard {
mcp_id: String,
cancellation_token: CancellationToken,
}
impl McpProcessGuard {
pub fn new(mcp_id: String, cancellation_token: CancellationToken) -> Self {
debug!("[RAII] Create process daemon: mcp_id={}", mcp_id);
Self {
mcp_id,
cancellation_token,
}
}
/// 克隆 CancellationToken用于传递给异步任务
pub fn clone_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
}
impl Drop for McpProcessGuard {
fn drop(&mut self) {
info!(
"[RAII] The process daemon was dropped and canceled CancellationToken: mcp_id={}",
self.mcp_id
);
self.cancellation_token.cancel();
}
}
// McpProcessGuard 不实现 Clone确保唯一所有权
impl std::fmt::Debug for McpProcessGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpProcessGuard")
.field("mcp_id", &self.mcp_id)
.field("is_cancelled", &self.cancellation_token.is_cancelled())
.finish()
}
}
/// MCP 服务封装
///
/// 将 McpHandler、McpProcessGuard 和服务状态封装在一起,
/// 作为 `ProxyHandlerManager` 中 DashMap 的 value。
///
/// # RAII 保证
///
/// 当 McpService 被 drop 时:
/// 1. McpProcessGuard 被 drop触发 CancellationToken 取消
/// 2. 关联的子进程收到信号,自动退出
pub struct McpService {
/// 进程守护器RAII 核心)
process_guard: McpProcessGuard,
/// MCP 透明代理处理器(可选,启动中时为 None
handler: Option<McpHandler>,
/// 服务状态信息
status: McpServiceStatusInfo,
}
/// MCP 服务状态信息(不包含 CancellationToken由 McpProcessGuard 管理)
#[derive(Debug, Clone)]
pub struct McpServiceStatusInfo {
pub mcp_id: String,
pub mcp_type: McpType,
pub mcp_router_path: McpRouterPath,
pub check_mcp_status_response_status: CheckMcpStatusResponseStatus,
pub last_accessed: Instant,
pub mcp_config: Option<McpConfig>,
/// 连续健康检查失败次数(用于容错机制)
pub consecutive_probe_failures: u32,
}
impl McpServiceStatusInfo {
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
check_mcp_status_response_status: CheckMcpStatusResponseStatus,
) -> Self {
Self {
mcp_id,
mcp_type,
mcp_router_path,
check_mcp_status_response_status,
last_accessed: Instant::now(),
mcp_config: None,
consecutive_probe_failures: 0,
}
}
pub fn update_last_accessed(&mut self) {
self.last_accessed = Instant::now();
}
/// 重置健康检查失败计数
pub fn reset_probe_failures(&mut self) {
self.consecutive_probe_failures = 0;
}
/// 增加健康检查失败计数,返回增加后的值
pub fn increment_probe_failures(&mut self) -> u32 {
self.consecutive_probe_failures += 1;
self.consecutive_probe_failures
}
}
impl McpService {
/// 创建新的 MCP 服务
///
/// # 参数
/// - `mcp_id`: 服务唯一标识
/// - `mcp_type`: 服务类型
/// - `mcp_router_path`: 路由路径
/// - `cancellation_token`: 用于控制进程生命周期的取消令牌
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
) -> Self {
let process_guard = McpProcessGuard::new(mcp_id.clone(), cancellation_token);
let status = McpServiceStatusInfo::new(
mcp_id,
mcp_type,
mcp_router_path,
CheckMcpStatusResponseStatus::Pending,
);
Self {
process_guard,
handler: None,
status,
}
}
/// 设置 MCP Handler
pub fn set_handler(&mut self, handler: McpHandler) {
self.handler = Some(handler);
}
/// 获取 MCP Handler
pub fn handler(&self) -> Option<&McpHandler> {
self.handler.as_ref()
}
/// 获取服务状态
pub fn status(&self) -> &McpServiceStatusInfo {
&self.status
}
/// 获取可变服务状态
pub fn status_mut(&mut self) -> &mut McpServiceStatusInfo {
&mut self.status
}
/// 克隆 CancellationToken
pub fn clone_token(&self) -> CancellationToken {
self.process_guard.clone_token()
}
/// 更新服务状态
pub fn update_status(&mut self, status: CheckMcpStatusResponseStatus) {
self.status.check_mcp_status_response_status = status;
}
/// 更新最后访问时间
pub fn update_last_accessed(&mut self) {
self.status.update_last_accessed();
}
/// 设置 MCP 配置
pub fn set_mcp_config(&mut self, config: McpConfig) {
self.status.mcp_config = Some(config);
}
}
impl std::fmt::Debug for McpService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpService")
.field("process_guard", &self.process_guard)
.field("handler", &self.handler.is_some())
.field("status", &self.status)
.finish()
}
}
// =============================================================================
// 兼容层:保留 McpServiceStatus 以兼容现有代码
// =============================================================================
/// MCP 服务状态(兼容层)
///
/// 保留此结构体以兼容现有代码,内部委托给 McpServiceStatusInfo
#[derive(Debug, Clone)]
pub struct McpServiceStatus {
pub mcp_id: String,
pub mcp_type: McpType,
pub mcp_router_path: McpRouterPath,
pub cancellation_token: CancellationToken,
pub check_mcp_status_response_status: CheckMcpStatusResponseStatus,
pub last_accessed: Instant,
pub mcp_config: Option<McpConfig>,
/// 连续健康检查失败次数
pub consecutive_probe_failures: u32,
}
impl McpServiceStatus {
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
check_mcp_status_response_status: CheckMcpStatusResponseStatus,
) -> Self {
Self {
mcp_id,
mcp_type,
mcp_router_path,
cancellation_token,
check_mcp_status_response_status,
last_accessed: Instant::now(),
mcp_config: None,
consecutive_probe_failures: 0,
}
}
pub fn with_mcp_config(mut self, mcp_config: McpConfig) -> Self {
self.mcp_config = Some(mcp_config);
self
}
pub fn update_last_accessed(&mut self) {
self.last_accessed = Instant::now();
}
}
// =============================================================================
// ProxyHandlerManager使用 RAII 模式管理 MCP 服务
// =============================================================================
/// MCP 代理管理器
///
/// 使用 RAII 模式管理 MCP 服务:
/// - 从 map 中移除 mcp_id 时,自动释放对应的进程资源
/// - 不需要显式调用 cleanup 方法(但仍提供显式清理接口)
#[derive(Debug)]
pub struct ProxyHandlerManager {
/// 使用单一 DashMap 管理所有 MCP 服务RAII 核心)
services: DashMap<String, McpService>,
}
impl Default for ProxyHandlerManager {
fn default() -> Self {
ProxyHandlerManager {
services: DashMap::new(),
}
}
}
impl ProxyHandlerManager {
/// 添加 MCP 服务RAII 模式)
///
/// 使用新的 RAII 结构创建服务,当服务被移除时会自动清理资源
pub fn add_mcp_service(
&self,
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
) {
let service = McpService::new(
mcp_id.clone(),
mcp_type,
mcp_router_path,
cancellation_token,
);
// RAII: 如果已存在同名服务insert 会返回旧服务,旧服务被 drop 时自动清理
if let Some(old_service) = self.services.insert(mcp_id.clone(), service) {
info!(
"[RAII] Overwrite existing services, old services will be automatically cleaned up: mcp_id={}",
mcp_id
);
drop(old_service);
}
}
/// 添加 MCP 服务状态(兼容旧 API
///
/// 保持与现有代码的兼容性,内部转换为新的 RAII 结构
///
/// 注意:`last_accessed` 会被重置为当前时间(插入视为新访问)
pub fn add_mcp_service_status_and_proxy(
&self,
mcp_service_status: McpServiceStatus,
proxy_handler: Option<McpHandler>,
) {
let mcp_id = mcp_service_status.mcp_id.clone();
// 创建 McpService 使用 RAII 模式
// 注意last_accessed 会在 McpServiceStatusInfo::new() 中重置为 Instant::now()
let mut service = McpService::new(
mcp_id.clone(),
mcp_service_status.mcp_type,
mcp_service_status.mcp_router_path,
mcp_service_status.cancellation_token,
);
// 设置初始状态
service.status_mut().check_mcp_status_response_status =
mcp_service_status.check_mcp_status_response_status;
// 设置配置(如果有)
if let Some(config) = mcp_service_status.mcp_config {
service.set_mcp_config(config);
}
// 设置 handler如果有
if let Some(handler) = proxy_handler {
service.set_handler(handler);
}
// RAII: 如果已存在同名服务insert 会返回旧服务,旧服务被 drop 时自动清理
if let Some(old_service) = self.services.insert(mcp_id.clone(), service) {
info!(
"[RAII] Overwrite existing services, old services will be automatically cleaned up: mcp_id={}",
mcp_id
);
// old_service 在此作用域结束时 drop触发 McpProcessGuard::drop()
drop(old_service);
}
}
/// 获取所有的 MCP 服务状态(兼容旧 API
///
/// 优化:先快速收集所有 keys然后逐个获取详细信息
/// 避免 iter() 长时间锁住多个分片,让其他写操作有机会执行
pub fn get_all_mcp_service_status(&self) -> Vec<McpServiceStatus> {
// 第一步:快速收集所有 keys只 clone String锁持有时间短
let keys: Vec<String> = self
.services
.iter()
.map(|entry| entry.key().clone())
.collect();
// 第二步:逐个获取详细信息(每次只锁一个分片)
keys.into_iter()
.filter_map(|mcp_id| self.get_mcp_service_status(&mcp_id))
.collect()
}
/// 获取 MCP 服务状态(兼容旧 API
pub fn get_mcp_service_status(&self, mcp_id: &str) -> Option<McpServiceStatus> {
self.services.get(mcp_id).map(|entry| {
let service = entry.value();
let status = service.status();
McpServiceStatus {
mcp_id: status.mcp_id.clone(),
mcp_type: status.mcp_type.clone(),
mcp_router_path: status.mcp_router_path.clone(),
cancellation_token: service.clone_token(),
check_mcp_status_response_status: status.check_mcp_status_response_status.clone(),
last_accessed: status.last_accessed,
mcp_config: status.mcp_config.clone(),
consecutive_probe_failures: status.consecutive_probe_failures,
}
})
}
/// 更新最后访问时间
///
/// 使用 entry API 确保原子性操作
pub fn update_last_accessed(&self, mcp_id: &str) {
self.services
.entry(mcp_id.to_string())
.and_modify(|service| service.update_last_accessed());
}
/// 修改 MCP 服务状态 (Ready/Pending/Error)
///
/// 使用 entry API 确保原子性操作
pub fn update_mcp_service_status(&self, mcp_id: &str, status: CheckMcpStatusResponseStatus) {
self.services
.entry(mcp_id.to_string())
.and_modify(|service| service.update_status(status));
}
/// 获取 MCP Handler
pub fn get_proxy_handler(&self, mcp_id: &str) -> Option<McpHandler> {
self.services
.get(mcp_id)
.and_then(|entry| entry.value().handler().cloned())
}
/// 获取服务的 MCP 配置(用于自动重启)
pub fn get_mcp_config(&self, mcp_id: &str) -> Option<McpConfig> {
self.services
.get(mcp_id)
.and_then(|entry| entry.value().status().mcp_config.clone())
}
/// 添加 MCP Handler 到已存在的服务
///
/// 使用 entry API 确保原子性操作
pub fn add_proxy_handler(&self, mcp_id: &str, proxy_handler: McpHandler) {
match self.services.entry(mcp_id.to_string()) {
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
entry.get_mut().set_handler(proxy_handler);
}
dashmap::mapref::entry::Entry::Vacant(_) => {
warn!(
"[RAII] Trying to add handler to non-existent service: mcp_id={}",
mcp_id
);
}
}
}
/// 检查服务是否存在
pub fn contains_service(&self, mcp_id: &str) -> bool {
self.services.contains_key(mcp_id)
}
/// 获取服务数量
pub fn service_count(&self) -> usize {
self.services.len()
}
/// 注册 MCP 配置到缓存
pub async fn register_mcp_config(&self, mcp_id: &str, config: McpConfig) {
GLOBAL_MCP_CONFIG_CACHE
.insert(mcp_id.to_string(), config)
.await;
info!("MCP configuration registered in cache: {}", mcp_id);
}
/// 从缓存获取 MCP 配置
pub async fn get_mcp_config_from_cache(&self, mcp_id: &str) -> Option<McpConfig> {
if let Some(config) = GLOBAL_MCP_CONFIG_CACHE.get(mcp_id).await {
debug!("Get MCP configuration from cache: {}", mcp_id);
Some(config)
} else {
debug!("MCP configuration not found in cache: {}", mcp_id);
None
}
}
/// 从缓存删除 MCP 配置
pub async fn unregister_mcp_config(&self, mcp_id: &str) {
GLOBAL_MCP_CONFIG_CACHE.invalidate(mcp_id).await;
info!("MCP configuration removed from cache: {}", mcp_id);
}
/// 清理资源 (RAII 模式简化版)
///
/// 通过 RAII 模式,从 DashMap 中移除服务会自动:
/// 1. 触发 McpProcessGuard::drop()
/// 2. 取消 CancellationToken
/// 3. 关联的子进程收到信号退出
///
/// 此方法额外清理路由和缓存
pub async fn cleanup_resources(&self, mcp_id: &str) -> Result<()> {
info!("[RAII] Start cleaning up resources: mcp_id={}", mcp_id);
// 创建路径以构建要删除的路由路径
let mcp_sse_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Sse)
.map_err(|e| {
anyhow::anyhow!("Failed to create SSE router path for {}: {}", mcp_id, e)
})?;
let base_sse_path = mcp_sse_router_path.base_path;
let mcp_stream_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Stream)
.map_err(|e| {
anyhow::anyhow!("Failed to create Stream router path for {}: {}", mcp_id, e)
})?;
let base_stream_path = mcp_stream_router_path.base_path;
// 移除相关路由
DynamicRouterService::delete_route(&base_sse_path);
DynamicRouterService::delete_route(&base_stream_path);
// RAII 核心:从 DashMap 移除会触发 McpProcessGuard::drop()
// 这会自动取消 CancellationToken进而触发子进程退出
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed from the map, McpProcessGuard will automatically cancel the token: mcp_id={}",
mcp_id
);
} else {
debug!(
"[RAII] Service does not exist, skip removal: mcp_id={}",
mcp_id
);
}
// 清理配置缓存
self.unregister_mcp_config(mcp_id).await;
// 清理健康状态缓存
GLOBAL_RESTART_TRACKER.clear_health_status(mcp_id);
info!(
"[RAII] MCP service resource cleanup completed: mcp_id={}",
mcp_id
);
Ok(())
}
/// 系统关闭,清理所有资源
///
/// RAII 模式下,清除 DashMap 会自动释放所有资源
pub async fn cleanup_all_resources(&self) -> Result<()> {
info!("[RAII] Start cleaning up all MCP service resources");
// 收集所有 mcp_id
let mcp_ids: Vec<String> = self
.services
.iter()
.map(|entry| entry.key().clone())
.collect();
let count = mcp_ids.len();
// 逐个清理(包括路由和缓存)
for mcp_id in mcp_ids {
if let Err(e) = self.cleanup_resources(&mcp_id).await {
error!(
"[RAII] Failed to clean up resources: mcp_id={}, error={}",
mcp_id, e
);
// 继续清理其他资源
}
}
info!(
"[RAII] All MCP service resources have been cleaned up, and a total of {} services have been cleaned up.",
count
);
Ok(())
}
/// 仅移除服务(依赖 RAII 自动清理进程)
///
/// 从 DashMap 中移除服务,触发 RAII 自动清理。
/// 不会清理路由和缓存,适用于需要快速移除服务的场景。
pub fn remove_service(&self, mcp_id: &str) -> bool {
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed and the process will be automatically cleaned up: mcp_id={}",
mcp_id
);
true
} else {
debug!("[RAII] Service does not exist: mcp_id={}", mcp_id);
false
}
}
/// 重置健康检查失败计数
///
/// 使用 get_mut 避免为不存在的服务创建空 entry
pub fn reset_probe_failures(&self, mcp_id: &str) {
if let Some(mut entry) = self.services.get_mut(mcp_id) {
entry.value_mut().status_mut().reset_probe_failures();
}
}
/// 增加健康检查失败计数,返回增加后的值
///
/// 使用 entry API 确保原子性操作
pub fn increment_probe_failures(&self, mcp_id: &str) -> u32 {
self.services
.get_mut(mcp_id)
.map(|mut entry| entry.value_mut().status_mut().increment_probe_failures())
.unwrap_or(0)
}
/// 清理资源用于重启(保留配置缓存)
///
/// 与 cleanup_resources 不同,此方法不会清理配置缓存,
/// 允许后续使用缓存的配置重新启动服务。
pub async fn cleanup_resources_for_restart(&self, mcp_id: &str) -> Result<()> {
info!(
"[RAII] Start cleaning up resources for restart: mcp_id={}",
mcp_id
);
// 创建路径以构建要删除的路由路径
let mcp_sse_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Sse)
.map_err(|e| {
anyhow::anyhow!("Failed to create SSE router path for {}: {}", mcp_id, e)
})?;
let base_sse_path = mcp_sse_router_path.base_path;
let mcp_stream_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Stream)
.map_err(|e| {
anyhow::anyhow!("Failed to create Stream router path for {}: {}", mcp_id, e)
})?;
let base_stream_path = mcp_stream_router_path.base_path;
// 移除相关路由
DynamicRouterService::delete_route(&base_sse_path);
DynamicRouterService::delete_route(&base_stream_path);
// RAII 核心:从 DashMap 移除会触发 McpProcessGuard::drop()
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed from the map (for restart), McpProcessGuard will automatically cancel the token: mcp_id={}",
mcp_id
);
} else {
debug!(
"[RAII] Service does not exist, skip removal: mcp_id={}",
mcp_id
);
}
// 注意:不清理配置缓存,保留用于重启
// self.unregister_mcp_config(mcp_id).await; // 不调用
// 清理健康状态缓存
GLOBAL_RESTART_TRACKER.clear_health_status(mcp_id);
info!(
"[RAII] MCP service resource cleanup completed (for restart): mcp_id={}",
mcp_id
);
Ok(())
}
}
/// MCP 配置缓存(使用 moka 实现 TTL
///
/// ## 存储架构说明
///
/// MCP 配置存储在两个位置:
///
/// 1. **McpServiceStatus.mcp_config**(服务状态中)
/// - 存储当前运行服务的配置
/// - 随服务清理而被删除
/// - 用于快速访问当前服务的配置
///
/// 2. **GLOBAL_MCP_CONFIG_CACHE**(全局缓存)
/// - 独立于服务状态存储
/// - 有 TTL24 小时)
/// - 用于服务重启时恢复配置
///
/// ## 为什么需要两处存储?
///
/// - 服务清理后McpServiceStatus 被删除,但配置仍在缓存中
/// - 下次请求到来时,可以从缓存恢复配置并重启服务
/// - 实现了服务的自动重启能力
///
/// ## 优先级
///
/// 1. 请求 header 中的配置(最新)
/// 2. 缓存中的配置(兜底)
///
/// ## TTL
///
/// - 24 小时(可配置)
/// - max_capacity: 1000防止内存溢出
pub struct McpConfigCache {
cache: Cache<String, McpConfig>,
}
impl McpConfigCache {
pub fn new() -> Self {
Self {
cache: Cache::builder()
.time_to_live(Duration::from_secs(24 * 60 * 60)) // 24 小时 TTL
.max_capacity(1000) // 最多缓存 1000 个配置,防止内存溢出
.build(),
}
}
pub async fn insert(&self, mcp_id: String, config: McpConfig) {
self.cache.insert(mcp_id.clone(), config).await;
info!("MCP configuration cached: {} (TTL: 24h)", mcp_id);
}
pub async fn get(&self, mcp_id: &str) -> Option<McpConfig> {
self.cache.get(mcp_id).await
}
pub async fn invalidate(&self, mcp_id: &str) {
self.cache.invalidate(mcp_id).await;
}
#[allow(dead_code)]
pub fn invalidate_all(&self) {
self.cache.invalidate_all();
}
}
impl Default for McpConfigCache {
fn default() -> Self {
Self::new()
}
}
// 全局配置缓存单例
pub static GLOBAL_MCP_CONFIG_CACHE: Lazy<McpConfigCache> = Lazy::new(McpConfigCache::default);
/// MCP 服务重启追踪器
///
/// 用于防止服务频繁重启导致的无限循环
///
/// ## 重启限制
///
/// - 最小重启间隔30 秒
/// - 如果服务在 30 秒内被标记为需要重启,将跳过重启
/// - 这防止了服务启动失败时的无限重启循环
///
/// ## 健康状态缓存
///
/// - 缓存后端健康状态,避免频繁检查
/// - 缓存时间5 秒(可配置)
/// - 用于减少 `is_mcp_server_ready()` 调用频率
pub struct RestartTracker {
// mcp_id -> 最后重启时间
last_restart: DashMap<String, Instant>,
// mcp_id -> (健康状态, 检查时间)
health_status: DashMap<String, (bool, Instant)>,
// mcp_id -> 启动锁,防止并发启动同一服务
startup_locks: DashMap<String, Arc<Mutex<()>>>,
}
impl RestartTracker {
pub fn new() -> Self {
Self {
last_restart: DashMap::new(),
health_status: DashMap::new(),
startup_locks: DashMap::new(),
}
}
/// 获取缓存的健康状态
///
/// 如果缓存未过期5秒内返回缓存值
/// 否则返回 None表示需要重新检查
pub fn get_cached_health_status(&self, mcp_id: &str) -> Option<bool> {
let cache_duration = Duration::from_secs(5); // 5 秒缓存
let now = Instant::now();
self.health_status.get(mcp_id).and_then(|entry| {
let (is_healthy, check_time) = *entry.value();
if now.duration_since(check_time) < cache_duration {
Some(is_healthy)
} else {
None
}
})
}
/// 更新健康状态缓存
pub fn update_health_status(&self, mcp_id: &str, is_healthy: bool) {
self.health_status
.insert(mcp_id.to_string(), (is_healthy, Instant::now()));
}
/// 清除健康状态缓存
pub fn clear_health_status(&self, mcp_id: &str) {
self.health_status.remove(mcp_id);
}
/// 检查是否可以重启服务
///
/// 返回 true 表示可以重启false 表示在冷却期内
///
/// 注意:此方法仅检查是否可以重启,不会自动插入时间戳。
/// 时间戳只在服务成功启动后通过 `record_restart()` 方法记录。
pub fn can_restart(&self, mcp_id: &str) -> bool {
let now = Instant::now();
let min_restart_interval = Duration::from_secs(30); // 30 秒最小重启间隔
// 只检查,不自动插入时间戳
if let Some(last_restart) = self.last_restart.get(mcp_id) {
let elapsed = now.duration_since(*last_restart);
if elapsed < min_restart_interval {
warn!(
"Service {} is in the cooldown period and is only {} seconds since its last restart. Restart is skipped.",
mcp_id,
elapsed.as_secs()
);
return false;
}
}
// 不在冷却期内,但不自动更新时间戳
true
}
/// 记录服务成功重启
///
/// 此方法应在服务成功启动后调用,用于记录重启时间戳。
/// 配合 `can_restart()` 使用,避免在服务启动失败时插入时间戳。
pub fn record_restart(&self, mcp_id: &str) {
self.last_restart.insert(mcp_id.to_string(), Instant::now());
info!(
"The service started successfully and the restart time was recorded: {}",
mcp_id
);
}
/// 清除重启时间戳
///
/// 当服务启动失败时,可选择调用此方法清除时间戳,
/// 允许立即重试而不必等待冷却期。
#[allow(dead_code)]
pub fn clear_restart(&self, mcp_id: &str) {
self.last_restart.remove(mcp_id);
info!("Restart timestamp cleared for service {}", mcp_id);
}
/// 尝试获取服务启动锁
///
/// 返回 Some(OwnedMutexGuard) 表示获取成功,可以继续启动服务
/// 返回 None 表示服务正在启动中,应该跳过本次启动
///
/// # 使用方式
///
/// ```ignore
/// if let Some(_guard) = GLOBAL_RESTART_TRACKER.try_acquire_startup_lock(&mcp_id) {
/// // 获取到锁,可以启动服务
/// let result = start_service().await;
/// // _guard 在作用域结束时自动释放
/// } else {
/// // 未获取到锁,服务正在启动中
/// return Ok(Response::503);
/// }
/// ```
pub fn try_acquire_startup_lock(&self, mcp_id: &str) -> Option<OwnedMutexGuard<()>> {
// 使用 entry API 确保原子性,避免竞态条件
let lock = self
.startup_locks
.entry(mcp_id.to_string())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
// 尝试获取 owned 锁,锁会一直保持到返回的 guard 被 drop
match lock.try_lock_owned() {
Ok(guard) => Some(guard),
Err(_) => {
// 锁被占用,服务正在启动中
debug!("Service {} is starting, skip this startup", mcp_id);
None
}
}
}
/// 清理服务启动锁
///
/// 当服务启动完成或失败后,应该清理启动锁以允许后续重试
/// 注意:正常情况下锁会随 MutexGuard 自动释放,此方法用于异常清理
#[allow(dead_code)]
pub fn cleanup_startup_lock(&self, mcp_id: &str) {
self.startup_locks.remove(mcp_id);
debug!("Cleaned startup lock for service {}", mcp_id);
}
}
impl Default for RestartTracker {
fn default() -> Self {
Self::new()
}
}
// 全局重启追踪器单例
pub static GLOBAL_RESTART_TRACKER: Lazy<RestartTracker> = Lazy::new(RestartTracker::default);
// 提供一个便捷的函数来获取全局 ProxyHandlerManager
pub fn get_proxy_manager() -> &'static ProxyHandlerManager {
&GLOBAL_PROXY_MANAGER
}

View File

@@ -0,0 +1,71 @@
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct HttpResult<T> {
pub code: String,
pub message: String,
pub data: Option<T>,
pub tid: Option<String>,
#[serde(skip)]
pub success: bool,
}
impl<T> HttpResult<T> {
pub fn success(data: T, tid: Option<String>) -> Self {
HttpResult {
code: "0000".to_string(),
message: "成功".to_string(),
data: Some(data),
tid,
success: true,
}
}
pub fn error(code: &str, message: &str, tid: Option<String>) -> Self {
HttpResult {
code: code.to_string(),
message: message.to_string(),
data: None,
tid,
success: false,
}
}
}
impl<T: Serialize> Serialize for HttpResult<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("HttpResult", 5)?;
state.serialize_field("code", &self.code)?;
state.serialize_field("message", &self.message)?;
state.serialize_field("data", &self.data)?;
state.serialize_field("tid", &self.tid)?;
let is_success = self.code == "0000";
state.serialize_field("success", &is_success)?;
state.end()
}
}
impl<T: Serialize> IntoResponse for HttpResult<T> {
fn into_response(self) -> Response {
match serde_json::to_string(&self) {
Ok(body) => (
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(axum::http::header::CONTENT_TYPE, "text/plain")],
"Internal Server Error",
)
.into_response(),
}
}
}

View File

@@ -0,0 +1,104 @@
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use super::{McpProtocol, McpType};
//check mcp服务状态的请求参数
#[derive(Deserialize, Debug, Clone)]
pub struct CheckMcpStatusRequestParams {
//mcp的id,必须有
#[serde(rename = "mcpId")]
pub mcp_id: String,
//mcp的json配置,必须有
#[serde(rename = "mcpJsonConfig")]
pub mcp_json_config: String,
//mcp类型,必须有,默认:OneShot
#[serde(rename = "mcpType", default = "default_mcp_type")]
pub mcp_type: McpType,
//后端MCP服务的协议类型可选用于指定连接到后端服务时使用的协议
//如果不指定,则使用客户端协议(由路由路径决定)
#[serde(rename = "backendProtocol")]
#[allow(dead_code)] // 为未来的功能预留
pub backend_protocol: Option<McpProtocol>,
}
//默认的mcp类型
fn default_mcp_type() -> McpType {
McpType::OneShot
}
//check mcp服务状态的响应参数
#[derive(Deserialize, Debug, Serialize)]
pub struct CheckMcpStatusResponseParams {
//是否就绪, READY 状态,表示 true
pub ready: bool,
//状态
pub status: McpStatusResponseEnum,
//消息
pub message: Option<String>,
}
impl CheckMcpStatusResponseParams {
pub fn new(ready: bool, status: CheckMcpStatusResponseStatus, message: Option<String>) -> Self {
//检查是否error,是的话,取error枚举里面的错误,放在 message里
let mut message = message;
if let CheckMcpStatusResponseStatus::Error(err) = status.clone() {
message = Some(err.to_string());
}
let status = McpStatusResponseEnum::from(status);
Self {
ready,
status,
message,
}
}
}
//check mcp服务状态的响应 status 枚举: READY,PENDING,ERROR
#[derive(Deserialize, Debug, Serialize, Clone)]
pub enum McpStatusResponseEnum {
//就绪
Ready,
//处理中
Pending,
//错误
Error,
}
//check mcp服务状态的响应 status 枚举: READY,PENDING,ERROR
#[derive(Deserialize, Debug, Serialize, Clone, Default)]
pub enum CheckMcpStatusResponseStatus {
//就绪
Ready,
//处理中
#[default]
Pending,
//错误
Error(String),
}
impl From<CheckMcpStatusResponseStatus> for McpStatusResponseEnum {
fn from(value: CheckMcpStatusResponseStatus) -> Self {
match value {
CheckMcpStatusResponseStatus::Ready => Self::Ready,
CheckMcpStatusResponseStatus::Pending => Self::Pending,
CheckMcpStatusResponseStatus::Error(_) => Self::Error,
}
}
}
impl IntoResponse for CheckMcpStatusResponseParams {
fn into_response(self) -> Response {
if let Ok(body) = serde_json::to_string(&self) {
(StatusCode::OK, body).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
"serde_json::to_string error".to_string(),
)
.into_response()
}
}
}

View File

@@ -0,0 +1,98 @@
use std::str::FromStr;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::{McpProtocol, mcp_router_model::McpServerConfig};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
//mcp_id
#[serde(rename = "mcpId")]
pub mcp_id: String,
//mcp_json_config,可能没有
#[serde(rename = "mcpJsonConfig")]
pub mcp_json_config: Option<String>,
//mcp类型默认为持续运行
#[serde(default = "default_mcp_type", rename = "mcpType")]
pub mcp_type: McpType,
//客户端协议用于暴露给客户端的API接口类型
#[serde(default = "default_mcp_protocol", rename = "clientProtocol")]
pub client_protocol: McpProtocol,
// 解析后的服务器配置(可选)
#[serde(skip_serializing, skip_deserializing)]
pub server_config: Option<McpServerConfig>,
}
fn default_mcp_protocol() -> McpProtocol {
McpProtocol::Sse
}
fn default_mcp_type() -> McpType {
McpType::OneShot
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum McpType {
// 持续运行
Persistent,
// 一次性任务
#[default]
OneShot,
}
impl FromStr for McpType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"persistent" => Ok(McpType::Persistent),
"oneShot" => Ok(McpType::OneShot),
_ => Err(anyhow::anyhow!("无效的 MCP 类型: {}", s)),
}
}
}
impl McpConfig {
pub fn new(
mcp_id: String,
mcp_json_config: Option<String>,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Self {
Self {
mcp_id,
mcp_json_config,
mcp_type,
client_protocol,
server_config: None,
}
}
pub fn from_json(json: &str) -> Result<Self> {
let config: McpConfig = serde_json::from_str(json)?;
Ok(config)
}
/// 从 JSON 字符串创建并解析服务器配置
pub fn from_json_with_server(
mcp_id: String,
mcp_json_config: String,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Result<Self> {
let mcp_json_server_parameters =
crate::model::McpJsonServerParameters::from(mcp_json_config.clone());
let server_config = mcp_json_server_parameters
.try_get_first_mcp_server()
.context("Failed to parse MCP server config")?;
Ok(Self {
mcp_id,
mcp_json_config: Some(mcp_json_config),
mcp_type,
client_protocol,
server_config: Some(server_config),
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
mod app_state_model;
mod global;
mod http_result;
mod mcp_check_status_model;
mod mcp_config;
mod mcp_router_model;
pub use app_state_model::AppState;
pub use global::{
DynamicRouterService, GLOBAL_RESTART_TRACKER, McpServiceStatus, ProxyHandlerManager,
get_proxy_manager,
};
pub use http_result::HttpResult;
pub use mcp_check_status_model::{
CheckMcpStatusRequestParams, CheckMcpStatusResponseParams, CheckMcpStatusResponseStatus,
};
pub use mcp_config::{McpConfig, McpType};
pub use mcp_router_model::{
AddRouteParams, GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX,
McpJsonServerParameters, McpProtocol, McpProtocolPath, McpRouterPath, McpServerCommandConfig,
McpServerConfig,
};

View File

@@ -0,0 +1,78 @@
//! Proxy module - re-exports handler types from proxy libraries
//!
//! This module provides a unified interface for proxy handlers by re-exporting
//! types from mcp-sse-proxy, mcp-streamable-proxy, and mcp-common libraries.
// Re-export SseHandler as ProxyHandler for backward compatibility
// SseHandler is used because it's based on rmcp 0.10 which supports both
// SSE server mode and CLI stdio mode used in the main project
pub use mcp_sse_proxy::SseHandler as ProxyHandler;
// Re-export SseServerHandler (unified handler for SSE server mode)
pub use mcp_sse_proxy::SseServerHandler;
// Re-export StreamProxyHandler with an alias to distinguish from SSE ProxyHandler
// Both mcp-sse-proxy and mcp-streamable-proxy export ProxyHandler, so we use an alias
pub use mcp_streamable_proxy::ProxyHandler as StreamProxyHandler;
// Re-export ToolFilter from mcp-common
pub use mcp_common::ToolFilter;
// Re-export client connection types for high-level API (each from its own library)
pub use mcp_sse_proxy::{McpClientConfig, SseClientConnection};
pub use mcp_streamable_proxy::StreamClientConnection;
// Re-export Builder APIs for server creation
pub use mcp_sse_proxy::server_builder::{BackendConfig as SseBackendConfig, SseServerBuilder};
pub use mcp_streamable_proxy::server_builder::{
BackendConfig as StreamBackendConfig, StreamServerBuilder,
};
/// Unified handler enum that can hold either SSE or Stream handler
///
/// This allows ProxyHandlerManager to store handlers of either type
/// while providing a common interface for status checks.
#[derive(Clone, Debug)]
pub enum McpHandler {
/// SSE protocol handler (from mcp-sse-proxy)
/// Holds SseServerHandler which unifies SseHandler and BackendSessionHandler
Sse(Box<SseServerHandler>),
/// Streamable HTTP protocol handler (from mcp-streamable-proxy)
Stream(Box<StreamProxyHandler>),
}
impl McpHandler {
/// Check if the underlying MCP server is ready
pub async fn is_mcp_server_ready(&self) -> bool {
match self {
McpHandler::Sse(h) => h.is_mcp_server_ready().await,
McpHandler::Stream(h) => h.is_mcp_server_ready().await,
}
}
/// Check if the backend connection is terminated
pub async fn is_terminated_async(&self) -> bool {
match self {
McpHandler::Sse(h) => h.is_terminated_async().await,
McpHandler::Stream(h) => h.is_terminated_async().await,
}
}
}
impl From<ProxyHandler> for McpHandler {
fn from(handler: ProxyHandler) -> Self {
McpHandler::Sse(Box::new(SseServerHandler::Sse(handler)))
}
}
impl From<SseServerHandler> for McpHandler {
fn from(handler: SseServerHandler) -> Self {
McpHandler::Sse(Box::new(handler))
}
}
impl From<StreamProxyHandler> for McpHandler {
fn from(handler: StreamProxyHandler) -> Self {
McpHandler::Stream(Box::new(handler))
}
}

View File

@@ -0,0 +1,46 @@
use axum::extract::Path;
use log::{info, warn};
use crate::{
AppError, get_proxy_manager,
model::{CheckMcpStatusResponseParams, CheckMcpStatusResponseStatus, HttpResult},
};
///根据 mcpId检查 mcp 透明代理服务是否正在运行的状态
// #[axum::debug_handler]
pub async fn check_mcp_is_status_handler(
Path(mcp_id): Path<String>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
info!("mcp_id: {mcp_id}");
let status = get_proxy_manager()
.get_mcp_service_status(&mcp_id)
.map(|mcp_service_status| mcp_service_status.check_mcp_status_response_status.clone());
if let Some(status) = status {
match status.clone() {
CheckMcpStatusResponseStatus::Ready => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(true, status, None),
None,
)),
CheckMcpStatusResponseStatus::Pending => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(false, status, None),
None,
)),
CheckMcpStatusResponseStatus::Error(err) => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(false, status, Some(err)),
None,
)),
}
} else {
warn!("mcp_id: {mcp_id} does not exist");
Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(
false,
CheckMcpStatusResponseStatus::Error("mcp_id 不存在".to_string()),
None,
),
None,
))
}
}

View File

@@ -0,0 +1,24 @@
use axum::{extract::Path, response::IntoResponse};
use crate::{AppError, get_proxy_manager, model::HttpResult};
use anyhow::Result;
use serde_json::json;
// #[axum::debug_handler]
pub async fn delete_route_handler(
Path(mcp_id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
// 删除动态路由,以及清理资源
get_proxy_manager()
.cleanup_resources(&mcp_id)
.await
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
// 返回成功信息
let data = json!({
"mcp_id": mcp_id,
"message": format!("已删除路由: {}", mcp_id)
});
Ok(HttpResult::success(data, None))
}

View File

@@ -0,0 +1,11 @@
use crate::AppError;
use axum::response::IntoResponse;
///健康检查:health
pub async fn get_health() -> Result<impl IntoResponse, AppError> {
Ok("health".to_string())
}
pub async fn get_ready() -> Result<impl IntoResponse, AppError> {
Ok("ready".to_string())
}

View File

@@ -0,0 +1,98 @@
use axum::{Json, extract::State, response::IntoResponse};
use http::Uri;
use log::{debug, error};
use tracing::instrument;
use uuid::Uuid;
use crate::AppError;
use crate::model::{
AddRouteParams, HttpResult, McpConfig, McpProtocolPath, McpServerConfig, McpType,
};
use crate::model::{AppState, McpRouterPath};
use crate::server::task::integrate_server_with_axum;
use serde_json::json;
// 修改 add_route_handler 函数,使用新的集成方法
#[instrument]
// #[axum::debug_handler]
pub async fn add_route_handler(
State(state): State<AppState>,
uri: Uri,
Json(params): Json<AddRouteParams>,
) -> Result<impl IntoResponse, AppError> {
// 获取请求路径
let request_path = uri.path().to_string();
debug!("Request path: {}", request_path);
debug!("Full URI: {:?}", uri);
let mcp_protocol = McpRouterPath::from_uri_prefix_protocol(&request_path);
if let Some(mcp_protocol) = mcp_protocol {
// 生成mcp_id, 使用uuid,去掉-
let mcp_id = Uuid::now_v7().to_string().replace("-", "");
//根据 mcp_id 和协议,生成 mcp_router_path
let mcp_router_path = McpRouterPath::new(mcp_id, mcp_protocol)
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_plugin_json = params.mcp_json_config;
// 将mcp_plugin_json转换为 McpServerConfig 结构体
let mcp_server_config =
McpServerConfig::try_from(mcp_plugin_json.clone()).expect("解析 MCP 配置失败");
let mcp_type = params.mcp_type.unwrap_or(McpType::default());
debug!("Client protocol: {:?}", mcp_router_path.mcp_protocol);
// 构建完整的 McpConfig用于自动重启
let full_mcp_config = McpConfig {
mcp_id: mcp_router_path.mcp_id.clone(),
client_protocol: mcp_router_path.mcp_protocol.clone(),
mcp_type: mcp_type.clone(),
mcp_json_config: Some(mcp_plugin_json),
server_config: Some(mcp_server_config.clone()),
};
// 使用新的集成方法,后端协议在函数内部解析
let _ = integrate_server_with_axum(
mcp_server_config.clone(),
mcp_router_path.clone(),
full_mcp_config,
)
.await
.map_err(|e| {
error!("Failed to start MCP service: {e}");
AppError::mcp_server_error(e.to_string())
})?;
//区分 mcp协议
let mcp_protocol = mcp_router_path.mcp_protocol_path.clone();
let data = match mcp_protocol {
McpProtocolPath::SsePath(sse_path) => {
// 返回 mcp_id 和 sse_path
let data = json!({
"mcp_id": mcp_router_path.mcp_id,
"sse_path": sse_path.sse_path,
"message_path": sse_path.message_path,
"mcp_type": mcp_type
});
data
}
McpProtocolPath::StreamPath(stream_path) => {
// 返回 mcp_id 和 stream_path
let data = json!({
"mcp_id": mcp_router_path.mcp_id,
"stream_path": stream_path.stream_path,
"mcp_type": mcp_type
});
data
}
};
Ok(HttpResult::success(data, None))
} else {
//返回 bad request,400 错误
return Err(AppError::invalid_parameter("无效的请求路径"));
}
}

View File

@@ -0,0 +1,281 @@
use anyhow::Result;
use axum::{Json, extract::State, http::uri::Uri};
use log::{debug, error, info};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use crate::{
AppError, get_proxy_manager,
model::{
AppState, CheckMcpStatusRequestParams, CheckMcpStatusResponseParams,
CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, HttpResult, McpConfig, McpProtocol,
McpRouterPath, McpServiceStatus, McpType,
},
server::mcp_start_task,
};
/// 创建响应结果的辅助函数
fn create_response(
ready: bool,
status: CheckMcpStatusResponseStatus,
message: Option<String>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
let response = CheckMcpStatusResponseParams::new(ready, status, message);
Ok(HttpResult::success(response, None))
}
/// 检查mcp服务状态,根据 mcp_id 获取有无对应的mcp透明代理服务,如果没有则取 mcp_json_config 中的配置,生成mcp透明代理服务;
/// 这里根据 mcp_json_config配置,启动服务需要异步,不要阻塞,如果服务没准备好,返回 PENDING 状态;
/// 如果服务启动失败,返回 ERROR 状态;
/// 如果服务启动成功,返回 READY 状态;
#[instrument]
pub async fn check_mcp_status_handler(
State(state): State<AppState>,
uri: Uri,
Json(params): Json<CheckMcpStatusRequestParams>,
mcp_protocol: McpProtocol,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
// 使用全局 ProxyHandlerManager
let proxy_manager = get_proxy_manager();
// 检查服务状态
let status = proxy_manager
.get_mcp_service_status(&params.mcp_id)
.map(|mcp_service_status| mcp_service_status.check_mcp_status_response_status.clone());
if let Some(status) = status {
match status {
CheckMcpStatusResponseStatus::Error(error_msg) => {
// 如果有错误状态,返回错误信息,另外删除掉 ERROR 的记录,方便下次检查状态,重新启动服务
// Error 状态不更新 last_accessed因为服务已失败
if let Err(e) = proxy_manager.cleanup_resources(&params.mcp_id).await {
error!("Failed to cleanup resources for {}: {}", params.mcp_id, e);
}
// 返回错误信息
return create_response(
false,
CheckMcpStatusResponseStatus::Error(error_msg),
None,
);
}
CheckMcpStatusResponseStatus::Pending => {
// 如果状态是 Pending说明服务正在启动中直接返回 Pending
// 不要再次尝试启动!
debug!(
"[check_mcp_status] mcp_id={} status is Pending and the service is starting",
params.mcp_id
);
// 更新最后访问时间,避免启动过程中因超时被清理
// 用户调用 check_status 表明对服务有兴趣,应该延长超时
proxy_manager.update_last_accessed(&params.mcp_id);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
CheckMcpStatusResponseStatus::Ready => {
// 如果已经在运行,继续检查服务是否真的可用
debug!(
"[check_mcp_status] mcp_id={} status is Ready, check the backend health status",
params.mcp_id
);
}
}
}
// 检查 proxy_handler 是否存在
let proxy_handler = proxy_manager.get_proxy_handler(&params.mcp_id);
if let Some(handler) = proxy_handler {
// 调用透明代理的 is_mcp_server_ready 方法检查健康状态
let ready_status = handler.is_mcp_server_ready().await;
// 使用 update 方法更新状态(不是修改克隆)
if ready_status {
proxy_manager
.update_mcp_service_status(&params.mcp_id, CheckMcpStatusResponseStatus::Ready);
}
// 更新最后访问时间
proxy_manager.update_last_accessed(&params.mcp_id);
let status = if ready_status {
CheckMcpStatusResponseStatus::Ready
} else {
CheckMcpStatusResponseStatus::Pending
};
return create_response(ready_status, status, None);
}
// ===== 服务不存在,需要启动 =====
// 使用启动锁防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER.try_acquire_startup_lock(&params.mcp_id) {
Some(guard) => {
debug!(
"[check_mcp_status] mcp_id={} Obtained the startup lock successfully and started to start the service",
params.mcp_id
);
guard
}
None => {
// 锁被占用,服务正在启动中
debug!(
"[check_mcp_status] mcp_id={} The startup lock is occupied and the service is starting. Return to Pending.",
params.mcp_id
);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
};
// 双重检查:获取锁后再次检查服务是否已存在
if proxy_manager.get_proxy_handler(&params.mcp_id).is_some() {
debug!(
"[check_mcp_status] mcp_id={} Double check found that the service already exists, return Ready",
params.mcp_id
);
return create_response(true, CheckMcpStatusResponseStatus::Ready, None);
}
// 如果服务状态已存在(可能是 Pending也不要重复启动
if proxy_manager
.get_mcp_service_status(&params.mcp_id)
.is_some()
{
debug!(
"[check_mcp_status] mcp_id={} The service status already exists and may be starting. Return to Pending.",
params.mcp_id
);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
// 启动服务(持有锁的情况下)
spawn_mcp_service(
&params.mcp_id,
params.mcp_json_config,
params.mcp_type,
mcp_protocol.clone(),
)?;
// 返回 PENDING 状态,锁会在函数返回时自动释放
create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
)
}
// SSE协议专用的状态检查处理函数
#[instrument]
// #[axum::debug_handler]
pub async fn check_mcp_status_handler_sse(
state: State<AppState>,
uri: Uri,
params: Json<CheckMcpStatusRequestParams>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
check_mcp_status_handler(state, uri, params, McpProtocol::Sse).await
}
// Stream协议专用的状态检查处理函数
#[instrument]
// #[axum::debug_handler]
pub async fn check_mcp_status_handler_stream(
state: State<AppState>,
uri: Uri,
params: Json<CheckMcpStatusRequestParams>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
check_mcp_status_handler(state, uri, params, McpProtocol::Stream).await
}
/// 异步启动MCP服务
///
/// 注意:此函数假设调用方已持有启动锁,不会重复检查!
///
/// # 参数
/// - `mcp_id`: MCP服务的唯一标识
/// - `mcp_json_config`: MCP服务的JSON配置
/// - `mcp_type`: MCP服务类型OneShot或Persistent
/// - `client_protocol`: 客户端使用的协议决定暴露的API接口类型
fn spawn_mcp_service(
mcp_id: &str,
mcp_json_config: String,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Result<(), AppError> {
let mcp_id = mcp_id.to_string();
info!(
"[spawn_mcp_service] mcp_id={} Start starting the service",
mcp_id
);
// 使用全局 ProxyHandlerManager
let proxy_manager = get_proxy_manager();
// 设置初始化状态 - 使用客户端协议创建路由路径
let mcp_router_path = McpRouterPath::new(mcp_id.clone(), client_protocol.clone())
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_service_status = McpServiceStatus::new(
mcp_id.clone(),
mcp_type.clone(),
mcp_router_path,
CancellationToken::new(),
CheckMcpStatusResponseStatus::Pending,
);
// RAII: 如果已存在同名服务,会自动清理旧服务
proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, None);
// 异步启动 mcp 透明代理服务
let mcp_id_clone = mcp_id.clone();
// 使用客户端协议创建配置
let mcp_config: McpConfig = McpConfig::new(
mcp_id_clone.clone(),
Some(mcp_json_config),
mcp_type,
client_protocol,
);
tokio::spawn(async move {
info!(
"[spawn_mcp_service] mcp_id={} tokio::spawn starts executing mcp_start_task",
mcp_id_clone
);
match mcp_start_task(mcp_config).await {
Ok(_) => {
info!(
"[spawn_mcp_service] mcp_id={} mcp_start_task successful, set status to Ready",
mcp_id_clone
);
get_proxy_manager()
.update_mcp_service_status(&mcp_id_clone, CheckMcpStatusResponseStatus::Ready);
}
Err(e) => {
let error_msg = format!("启动MCP服务失败: {e}");
error!(
"[spawn_mcp_service] mcp_id={} mcp_start_task failed: {}",
mcp_id_clone, e
);
get_proxy_manager().update_mcp_service_status(
&mcp_id_clone,
CheckMcpStatusResponseStatus::Error(error_msg),
);
}
}
});
info!(
"[spawn_mcp_service] mcp_id={} Service startup task has been submitted",
mcp_id
);
Ok(())
}

View File

@@ -0,0 +1,13 @@
mod check_mcp_is_status;
mod delete_route_handler;
mod health;
mod mcp_add_handler;
mod mcp_check_status_handler;
pub mod run_code_handler;
pub use check_mcp_is_status::check_mcp_is_status_handler;
pub use delete_route_handler::delete_route_handler;
pub use health::get_health;
pub use health::get_ready;
pub use mcp_add_handler::add_route_handler;
pub use mcp_check_status_handler::{check_mcp_status_handler_sse, check_mcp_status_handler_stream};
pub use run_code_handler::run_code_handler;

View File

@@ -0,0 +1,92 @@
use std::collections::HashMap;
use axum::{Json, response::IntoResponse};
use http::StatusCode;
use log::{debug, error, info};
use run_code_rmcp::{CodeExecutor, LanguageScript, RunCodeHttpResult};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::AppError;
///代码运行请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCodeMessageRequest {
//js运行参数
pub json_param: HashMap<String, Value>,
//运行的代码
pub code: String,
//前端生成的随机uid,用于查找websocket连接,发送执行过程中的log日志
pub uid: String,
pub engine_type: String,
}
impl RunCodeMessageRequest {
//获取语言脚本
pub fn get_language_script(&self) -> LanguageScript {
match self.engine_type.as_str() {
"js" => LanguageScript::Js,
"ts" => LanguageScript::Ts,
"python" => LanguageScript::Python,
_ => LanguageScript::Js,
}
}
}
/// 执行js/ts/python代码,通过 uv/deno 命令方式执行
// #[axum::debug_handler]
pub async fn run_code_handler(
Json(run_code_message_request): Json<RunCodeMessageRequest>,
) -> Result<impl IntoResponse, AppError> {
//json_param: HashMap<String, Value> 转换为 json 对象 Value
let params = match serde_json::to_value(run_code_message_request.json_param.clone()) {
Ok(v) => v,
Err(e) => {
error!("Run_code_handler parameter serialization failed: {e:?}");
return Err(AppError::from(e));
}
};
//执行代码
let code = run_code_message_request.code.clone();
let language = run_code_message_request.get_language_script();
debug!("run_code_handler language:{language:?}");
debug!("run_code_handler code:{code:?}");
debug!("run_code_handler params:{params:?}");
let result = match CodeExecutor::execute_with_params(&code, language, Some(params), None).await
{
Ok(result) => result,
Err(e) => {
error!("run_code_handler execution failed: {e:?}");
return Err(AppError::from(e));
}
};
if !result.success {
let error_message = result
.error
.as_deref()
.unwrap_or("run_code_handler执行失败但未提供错误信息");
error!("run_code_handler execution returns failure: {error_message}");
}
let data = match serde_json::to_value(&result) {
Ok(data) => data,
Err(e) => {
error!("run_code_handler serialization result failed: {e:?}");
return Err(AppError::from(e));
}
};
//打印结果
info!("run_code_handler result:{:?}", &result.success);
debug!("run_code_handler result:{:?}", &data);
//返回结果,使用 RunCodeHttpResult 封装执行结果
let run_code_http_result = RunCodeHttpResult {
data,
success: result.success,
error: result.error.clone(),
};
let body = serde_json::to_string(&run_code_http_result)?;
Ok((StatusCode::OK, body).into_response())
}

View File

@@ -0,0 +1,647 @@
use std::{
convert::Infallible,
task::{Context, Poll},
};
use axum::{
body::Body,
extract::Request,
response::{IntoResponse, Response},
};
use futures::future::BoxFuture;
use log::{debug, error, info, warn};
use tower::Service;
use crate::{
DynamicRouterService, get_proxy_manager, mcp_start_task,
model::{
CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, HttpResult, McpConfig, McpRouterPath,
McpType,
},
server::middlewares::extract_trace_id,
};
impl Service<Request<Body>> for DynamicRouterService {
type Response = Response;
type Error = Infallible;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
let path = req.uri().path().to_string();
let method = req.method().clone();
let headers = req.headers().clone();
// DEBUG: 详细路径解析日志
debug!("=== Path analysis begins ===");
debug!("Original request path: {}", path);
debug!("Path contains wildcard parameters: {:?}", req.extensions());
// 提取 trace_id
let trace_id = extract_trace_id();
// 创建根 span (使用 debug_span 减少日志量)
let span = tracing::debug_span!(
"DynamicRouterService",
otel.name = "HTTP Request",
http.method = %method,
http.route = %path,
http.url = %req.uri(),
mcp.protocol = format!("{:?}", self.0),
trace_id = %trace_id,
);
// 记录请求头信息
if let Some(content_type) = headers.get("content-type") {
span.record("http.request.content_type", format!("{:?}", content_type));
}
if let Some(content_length) = headers.get("content-length") {
span.record(
"http.request.content_length",
format!("{:?}", content_length),
);
}
debug!("Request path: {path}");
// 解析路由路径
let mcp_router_path = McpRouterPath::from_url(&path);
match mcp_router_path {
Some(mcp_router_path) => {
let mcp_id = mcp_router_path.mcp_id.clone();
let base_path = mcp_router_path.base_path.clone();
span.record("mcp.id", &mcp_id);
span.record("mcp.base_path", &base_path);
debug!("=== Path analysis results ===");
debug!("Parsed mcp_id: {}", mcp_id);
debug!("Parsed base_path: {}", base_path);
debug!("Request path: {} vs base_path: {}", path, base_path);
debug!("=== Path analysis ends ===");
Box::pin(async move {
let _guard = span.enter();
// 先尝试查找已注册的路由
debug!("===Route lookup process ===");
debug!("Find base_path: '{}'", base_path);
if let Some(router_entry) = DynamicRouterService::get_route(&base_path) {
debug!(
"✅ Find the registered route: base_path={}, path={}",
base_path, path
);
// ===== 检查后端健康状态 =====
let mcp_id_for_check = McpRouterPath::from_url(&path);
if let Some(router_path) = mcp_id_for_check {
let proxy_manager = get_proxy_manager();
// ===== 首先检查服务状态 =====
// 如果服务状态是 Pending说明服务正在初始化中uvx/npx 下载中)
// 此时不应该做健康检查,应该等待
if let Some(service_status) =
proxy_manager.get_mcp_service_status(&router_path.mcp_id)
{
match &service_status.check_mcp_status_response_status {
CheckMcpStatusResponseStatus::Pending => {
debug!(
"[MCP status check] mcp_id={} The status is Pending, the service is being initialized, and 503 is returned.",
router_path.mcp_id
);
let message = format!(
"服务 {} 正在初始化中,请稍后再试",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
return Ok(http_result.into_response());
}
CheckMcpStatusResponseStatus::Error(err) => {
// Error 状态:只清理,不重启
// 避免有问题的 MCP 服务无限重启循环
warn!(
"[MCP status check] mcp_id={} status is Error: {}, clean up resources and return error",
router_path.mcp_id, err
);
// 清理资源
if let Err(e) = proxy_manager
.cleanup_resources(&router_path.mcp_id)
.await
{
error!(
"[MCP status check] mcp_id={} Failed to clean up resources: {}",
router_path.mcp_id, e
);
}
// 返回错误,不尝试重启
let message = format!(
"服务 {} 启动失败: {}",
router_path.mcp_id, err
);
let http_result: HttpResult<String> =
HttpResult::error("0005", &message, None);
return Ok(http_result.into_response());
}
CheckMcpStatusResponseStatus::Ready => {
debug!(
"[MCP status check] mcp_id={} status is Ready, continue to check the backend health status",
router_path.mcp_id
);
}
}
}
// ===== 检查启动锁状态 =====
// 如果锁被占用,说明服务正在启动中
let startup_guard = GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(&router_path.mcp_id);
if startup_guard.is_none() {
// 锁被占用,服务正在启动中,返回 503
debug!(
"[Startup lock check] mcp_id={} The startup lock is occupied, the service is starting, and 503 is returned.",
router_path.mcp_id
);
span.record("mcp.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", router_path.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16);
return Ok(http_result.into_response());
}
// 获取到锁,现在可以安全地检查健康状态
let _startup_guard = startup_guard.unwrap();
debug!(
"[Start lock check] mcp_id={} Successfully obtained the startup lock and started health check",
router_path.mcp_id
);
if let Some(handler) =
proxy_manager.get_proxy_handler(&router_path.mcp_id)
{
// ===== 健康检查(带缓存)=====
let is_healthy = if let Some(cached) = GLOBAL_RESTART_TRACKER
.get_cached_health_status(&router_path.mcp_id)
{
debug!(
"[Health Check] mcp_id={} Use cache status: is_healthy={}",
router_path.mcp_id, cached
);
cached
} else {
debug!(
"[Health Check] mcp_id={} Cache miss, start actual health check...",
router_path.mcp_id
);
let status = handler.is_mcp_server_ready().await;
GLOBAL_RESTART_TRACKER
.update_health_status(&router_path.mcp_id, status);
debug!(
"[Health Check] mcp_id={} Actual health check result: is_healthy={}",
router_path.mcp_id, status
);
status
};
if is_healthy {
debug!(
"[Health Check] mcp_id={} The backend service is normal, release the lock and use routing",
router_path.mcp_id
);
// 释放锁,使用路由
drop(_startup_guard);
debug!("=== Route search ended (successful) ===");
return handle_request_with_router(req, router_entry, &path)
.await;
}
// 不健康,获取服务类型以决定是否重启
let mcp_type = proxy_manager
.get_mcp_service_status(&router_path.mcp_id)
.map(|s| s.mcp_type.clone());
// 清理资源
warn!(
"[Health check] mcp_id={} The backend service is unhealthy, clean up resources.",
router_path.mcp_id
);
if let Err(e) =
proxy_manager.cleanup_resources(&router_path.mcp_id).await
{
error!(
"[Clean up resources] mcp_id={} Failed to clean up resources: error={}",
router_path.mcp_id, e
);
} else {
debug!(
"[Clean up resources] mcp_id={} Clean up resources successfully",
router_path.mcp_id
);
}
// OneShot 类型:只清理,不重启
// OneShot 服务执行完成后进程会退出,这是正常行为,不应该自动重启
// 用户需要通过 check_status 接口显式启动新的 OneShot 服务
if matches!(mcp_type, Some(McpType::OneShot)) {
debug!(
"[Health Check] mcp_id={} is a OneShot type, does not automatically restart, and returns that the service has ended",
router_path.mcp_id
);
let message = format!(
"OneShot 服务 {} 已结束,请重新启动",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0006", &message, None);
return Ok(http_result.into_response());
}
// Persistent 类型:清理后重启
info!(
"[Restart process] mcp_id={} is Persistent type, start to restart the service",
router_path.mcp_id
);
// 从配置获取 mcp_config 并启动服务
// 优先从请求 header 获取配置
if let Some(mcp_config) =
req.extensions().get::<McpConfig>().cloned()
&& mcp_config.mcp_json_config.is_some()
{
info!(
"[Restart process] mcp_id={} Use the request header to configure the restart service",
mcp_config.mcp_id
);
proxy_manager
.register_mcp_config(&mcp_config.mcp_id, mcp_config.clone())
.await;
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 从缓存获取配置
if let Some(mcp_config) = proxy_manager
.get_mcp_config_from_cache(&router_path.mcp_id)
.await
{
info!(
"[Restart process] mcp_id={} Restart the service using cache configuration",
router_path.mcp_id
);
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 无法获取配置
warn!(
"[Restart Process] mcp_id={} Unable to obtain the configuration and unable to restart the service",
router_path.mcp_id
);
let message =
format!("服务 {} 不健康且无法获取配置", router_path.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0004", &message, None);
return Ok(http_result.into_response());
} else {
// handler 不存在,但路由存在
// 检查服务类型OneShot 不自动重启
let mcp_type = proxy_manager
.get_mcp_service_status(&router_path.mcp_id)
.map(|s| s.mcp_type.clone());
if matches!(mcp_type, Some(McpType::OneShot)) {
debug!(
"[Service Check] mcp_id={} is OneShot type and the handler does not exist, so it will not restart automatically.",
router_path.mcp_id
);
// 清理残留状态
if let Err(e) =
proxy_manager.cleanup_resources(&router_path.mcp_id).await
{
error!(
"[Clean up resources] mcp_id={} Failed to clean up resources: {}",
router_path.mcp_id, e
);
}
let message = format!(
"OneShot 服务 {} 已结束,请重新启动",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0006", &message, None);
return Ok(http_result.into_response());
}
// Persistent 类型:继续进入启动流程
warn!(
"The route exists but the handler does not exist. Enter the restart process: base_path={}",
base_path
);
}
} else {
// 无法解析路由路径,直接使用路由
debug!("=== Route search ended (successful) ===");
return handle_request_with_router(req, router_entry, &path).await;
}
} else {
debug!(
"❌ No registered route found: base_path='{}', path='{}'",
base_path, path
);
// 显示所有已注册的路由
let all_routes = DynamicRouterService::get_all_routes();
debug!("Currently registered route: {:?}", all_routes);
debug!("=== Route search ended (failed) ===");
}
// 未找到路由,尝试启动服务
warn!(
"No matching path found, try to start the service: base_path={base_path}, path={path}"
);
span.record("error.route_not_found", true);
// ===== 提前解析 mcp_id 用于配置获取 =====
let mcp_router_path_for_config = McpRouterPath::from_url(&path);
// ===== 配置获取优先级 =====
let proxy_manager = get_proxy_manager();
// 优先级 1: 从请求 header 中获取配置(最新)
if let Some(mcp_config) = req.extensions().get::<McpConfig>().cloned()
&& mcp_config.mcp_json_config.is_some()
{
// 检查重启限制(防止无限循环)
if !GLOBAL_RESTART_TRACKER.can_restart(&mcp_config.mcp_id) {
warn!(
"Service {} skips startup during restart cooldown period",
mcp_config.mcp_id
);
span.record("error.restart_in_cooldown", true);
let message =
format!("服务 {} 在重启冷却期内,请稍后再试", mcp_config.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0002", &message, None);
span.record("http.response.status_code", 429u16); // Too Many Requests
return Ok(http_result.into_response());
}
// 尝试获取启动锁,防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(&mcp_config.mcp_id)
{
Some(guard) => guard,
None => {
warn!(
"Service {} is starting, skip this startup",
mcp_config.mcp_id
);
span.record("error.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", mcp_config.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16); // Service Unavailable
return Ok(http_result.into_response());
}
};
info!(
"Use request header configuration to start the service: {}",
mcp_config.mcp_id
);
// 同时更新缓存
proxy_manager
.register_mcp_config(&mcp_config.mcp_id, mcp_config.clone())
.await;
// _startup_guard 会在作用域结束时自动释放
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 优先级 2: 从 moka 缓存中获取配置(兜底)
// 注意OneShot 类型不从缓存自动启动,需要用户显式请求(带 header 配置)
if let Some(mcp_id_for_cache) =
mcp_router_path_for_config.as_ref().map(|p| &p.mcp_id)
&& let Some(mcp_config) = proxy_manager
.get_mcp_config_from_cache(mcp_id_for_cache)
.await
{
// OneShot 类型不从缓存自动启动
// 避免已回收的 OneShot 服务被意外重启
if matches!(mcp_config.mcp_type, McpType::OneShot) {
info!(
"[Startup check] mcp_id={} is a OneShot type, does not automatically start from the cache, and requires an explicit request from the user",
mcp_id_for_cache
);
let message = format!(
"OneShot 服务 {} 需要通过 check_status 接口启动",
mcp_id_for_cache
);
let http_result: HttpResult<String> =
HttpResult::error("0007", &message, None);
return Ok(http_result.into_response());
}
// 检查重启限制(防止无限循环)
if !GLOBAL_RESTART_TRACKER.can_restart(mcp_id_for_cache) {
warn!(
"Service {} skips startup during restart cooldown period",
mcp_id_for_cache
);
span.record("error.restart_in_cooldown", true);
let message =
format!("服务 {} 在重启冷却期内,请稍后再试", mcp_id_for_cache);
let http_result: HttpResult<String> =
HttpResult::error("0002", &message, None);
span.record("http.response.status_code", 429u16); // Too Many Requests
return Ok(http_result.into_response());
}
// 尝试获取启动锁,防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(mcp_id_for_cache)
{
Some(guard) => guard,
None => {
warn!(
"Service {} is starting, skip this startup",
mcp_id_for_cache
);
span.record("error.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", mcp_id_for_cache);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16); // Service Unavailable
return Ok(http_result.into_response());
}
};
info!(
"Start the service using cached configuration: {}",
mcp_id_for_cache
);
// _startup_guard 会在作用域结束时自动释放
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 优先级 3: 无法获取配置,返回错误
warn!(
"No matching path was found, and the configuration was not obtained, so the MCP service could not be started: {path}"
);
span.record("error.mcp_config_missing", true);
let message =
format!("未找到匹配的路径,且未获取到配置,无法启动MCP服务: {path}");
let http_result: HttpResult<String> = HttpResult::error("0001", &message, None);
span.record("http.response.status_code", 404u16);
span.record("error.message", &message);
Ok(http_result.into_response())
})
}
None => {
warn!("Request path resolution failed: {path}");
span.record("error.path_parse_failed", true);
let message = format!("请求路径解析失败: {path}");
let http_result: HttpResult<String> = HttpResult::error("0001", &message, None);
Box::pin(async move {
let _guard = span.enter();
span.record("http.response.status_code", 400u16);
span.record("error.message", &message);
Ok(http_result.into_response())
})
}
}
}
}
/// 使用给定的路由处理请求
async fn handle_request_with_router(
req: Request<Body>,
router_entry: axum::Router,
path: &str,
) -> Result<Response, Infallible> {
// 获取匹配路径的Router并处理请求
let trace_id = extract_trace_id();
let method = req.method().clone();
let uri = req.uri().clone();
info!(
"[handle_request_with_router] Handle request: {} {}",
method, path
);
// 记录请求头中的关键信息
if let Some(content_type) = req.headers().get("content-type")
&& let Ok(content_type_str) = content_type.to_str()
{
debug!(
"[handle_request_with_router] Content-Type: {}",
content_type_str
);
}
if let Some(content_length) = req.headers().get("content-length")
&& let Ok(content_length_str) = content_length.to_str()
{
debug!(
"[handle_request_with_router] Content-Length: {}",
content_length_str
);
}
// 记录 x-mcp-json 头信息(如果存在)
if let Some(mcp_json) = req.headers().get("x-mcp-json")
&& let Ok(mcp_json_str) = mcp_json.to_str()
{
debug!(
"[handle_request_with_router] MCP-JSON Header: {}",
mcp_json_str
);
}
// 记录查询参数
if let Some(query) = uri.query() {
debug!("[handle_request_with_router] Query: {}", query);
}
// 使用 debug_span 减少日志量,因为 DynamicRouterService 已经记录了请求信息
// 移除 #[tracing::instrument] 避免 span 嵌套导致的日志膨胀问题
let span = tracing::debug_span!(
"handle_request_with_router",
otel.name = "Handle Request with Router",
component = "router",
trace_id = %trace_id,
http.method = %method,
http.path = %path,
);
let _guard = span.enter();
let mut service = router_entry.into_service();
match service.call(req).await {
Ok(response) => {
let status = response.status();
// 记录响应头信息
debug!(
"[handle_request_with_router]Response status: {}, response header: {response:?}",
status
);
span.record("http.response.status_code", status.as_u16());
Ok(response)
}
Err(error) => {
span.record("error.router_service_error", true);
span.record("error.message", format!("{:?}", error));
error!("[handle_request_with_router] error: {error:?}");
Ok(axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}
}
/// 启动MCP服务并处理请求
async fn start_mcp_and_handle_request(
req: Request<Body>,
mcp_config: McpConfig,
) -> Result<Response, Infallible> {
let request_path = req.uri().path().to_string();
let trace_id = extract_trace_id();
debug!("Request path: {request_path}");
// 使用 debug_span 减少日志量,移除 #[tracing::instrument] 避免 span 嵌套
let span = tracing::debug_span!(
"start_mcp_and_handle_request",
otel.name = "Start MCP and Handle Request",
component = "mcp_startup",
mcp.id = %mcp_config.mcp_id,
mcp.type = ?mcp_config.mcp_type,
mcp.config.has_config = mcp_config.mcp_json_config.is_some(),
trace_id = %trace_id,
);
let _guard = span.enter();
let ret = mcp_start_task(mcp_config).await;
if let Ok((router, _)) = ret {
span.record("mcp.startup.success", true);
handle_request_with_router(req, router, &request_path).await
} else {
span.record("mcp.startup.failed", true);
span.record("error.mcp_startup_failed", true);
span.record("error.message", format!("{:?}", ret));
warn!("MCP service startup failed: {ret:?}");
Ok(axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}

View File

@@ -0,0 +1,177 @@
// use super::TokenVerify;
// use axum::{
// extract::{FromRequestParts, Query, Request, State},
// http::{request::Parts, StatusCode},
// middleware::Next,
// response::{IntoResponse, Response},
// };
// use axum_extra::{
// headers::{authorization::Bearer, Authorization},
// TypedHeader,
// };
// use serde::Deserialize;
// use tracing::warn;
//
// #[derive(Debug, Deserialize)]
// struct Params {
// token: String,
// }
//
// pub async fn verify_token<T>(State(state): State<T>, req: Request, next: Next) -> Response
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// let (mut parts, body) = req.into_parts();
// match extract_token(&state, &mut parts).await {
// Ok(token) => {
// let mut req = Request::from_parts(parts, body);
// match set_user(&state, &token, &mut req) {
// Ok(_) => next.run(req).await,
// Err(msg) => (StatusCode::FORBIDDEN, msg).into_response(),
// }
// }
// Err(msg) => (StatusCode::UNAUTHORIZED, msg).into_response(),
// }
// }
//
// pub async fn extract_user<T>(State(state): State<T>, req: Request, next: Next) -> Response
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// let (mut parts, body) = req.into_parts();
// let req = if let Ok(token) = extract_token(&state, &mut parts).await {
// let mut req = Request::from_parts(parts, body);
// let _ = set_user(&state, &token, &mut req);
// req
// } else {
// Request::from_parts(parts, body)
// };
//
// next.run(req).await
// }
//
// async fn extract_token<T>(state: &T, parts: &mut Parts) -> Result<String, String>
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// match TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, &state).await {
// Ok(TypedHeader(Authorization(bearer))) => Ok(bearer.token().to_string()),
// Err(e) => {
// if e.is_missing() {
// match Query::<Params>::from_request_parts(parts, &state).await {
// Ok(params) => Ok(params.token.clone()),
// Err(e) => {
// let msg = format!("parse query params failed: {}", e);
// warn!(msg);
// Err(msg)
// }
// }
// } else {
// let msg = format!("parse Authorization header failed: {}", e);
// warn!(msg);
// Err(msg)
// }
// }
// }
// }
//
// fn set_user<T>(state: &T, token: &str, req: &mut Request) -> Result<(), String>
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// match state.verify(token) {
// Ok(user) => {
// req.extensions_mut().insert(user);
// Ok(())
// }
// Err(e) => {
// let msg = format!("verify token failed: {:?}", e);
// warn!(msg);
// Err(msg)
// }
// }
// }
//
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::{DecodingKey, EncodingKey, User};
// use anyhow::Result;
// use axum::{body::Body, middleware::from_fn_with_state, routing::get, Router};
// use std::sync::Arc;
// use tower::ServiceExt;
//
// #[derive(Clone)]
// struct AppState(Arc<AppStateInner>);
//
// struct AppStateInner {
// ek: EncodingKey,
// dk: DecodingKey,
// }
//
// impl TokenVerify for AppState {
// type Error = ();
//
// fn verify(&self, token: &str) -> Result<User, Self::Error> {
// self.0.dk.verify(token).map_err(|_| ())
// }
// }
//
// async fn handler(_req: Request) -> impl IntoResponse {
// (StatusCode::OK, "ok")
// }
//
// #[tokio::test]
// async fn verify_token_middleware_should_work() -> Result<()> {
// let encoding_pem = include_str!("../../fixtures/encoding.pem");
// let decoding_pem = include_str!("../../fixtures/decoding.pem");
// // let ek = EncodingKey::load(encoding_pem)?;
// // let dk = DecodingKey::load(decoding_pem)?;
// let state = AppState(Arc::new(AppStateInner { ek, dk }));
//
// let user = User::new(1, "soddy", "soddygo@acme.org");
// let token = state.0.ek.sign(user)?;
//
// let app = Router::new()
// .route("/", get(handler))
// .layer(from_fn_with_state(state.clone(), verify_token::<AppState>))
// .with_state(state);
//
// // good token
// let req = Request::builder()
// .uri("/")
// .header("Authorization", format!("Bearer {}", token))
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::OK);
//
// // good token in query params
// let req = Request::builder()
// .uri(format!("/?token={}", token))
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::OK);
//
// // no token
// let req = Request::builder().uri("/").body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
//
// // bad token
// let req = Request::builder()
// .uri("/")
// .header("Authorization", "Bearer bad-token")
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::FORBIDDEN);
//
// // bad token in query params
// let req = Request::builder()
// .uri("/?token=bad-token")
// .body(Body::empty())?;
// let res = app.oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::FORBIDDEN);
//
// Ok(())
// }
// }

View File

@@ -0,0 +1,147 @@
//! HTTP request/response logging middleware
//!
//! This middleware logs all HTTP requests and responses at TRACE level.
//! Using TRACE level avoids excessive log output for frequent MCP API calls.
//!
//! To enable HTTP logging, set:
//! - `RUST_LOG=trace` (global)
//! - `RUST_LOG=mcp_proxy=trace` (module-specific)
use axum::{
extract::{MatchedPath, Request},
middleware::Next,
response::Response,
};
use tracing::trace;
/// HTTP request/response logging middleware
///
/// Logs incoming requests with details (method, uri, route, headers)
/// and outgoing responses (status, duration, content length).
///
/// # Log Level
/// Uses TRACE level because MCP API calls are very frequent.
/// Enable with `RUST_LOG=trace` or `RUST_LOG=mcp_proxy=trace`.
///
/// # Middleware Order
/// Should be placed before `opentelemetry_tracing_middleware` to log
/// request entry first, while avoiding duplication of completion logs.
pub async fn http_logging_middleware(request: Request, next: Next) -> Response {
// Extract all needed data before moving the request
let method = request.method().clone();
let uri = request.uri().clone();
let version = request.version();
// Get matched route path (extract to owned String to avoid borrow)
let route = request
.extensions()
.get::<MatchedPath>()
.map(|path| path.as_str().to_owned())
.unwrap_or_else(|| "<unknown>".to_owned());
// Get request headers of interest (extract to owned Strings to avoid borrow)
let headers = request.headers();
let content_type = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
let content_length = headers
.get("content-length")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
let user_agent = headers
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
// Log incoming request at TRACE level (for frequent MCP API calls)
trace!(
method = %method,
uri = %uri,
route = %route,
version = ?version,
content_type = %content_type,
content_length = %content_length,
user_agent = %user_agent,
"HTTP request received"
);
let start = std::time::Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
// Get response details
let status = response.status();
let response_content_length = response
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>");
// Log response at TRACE level
trace!(
method = %method,
uri = %uri,
route = %route,
status = %status,
duration_ms = %duration.as_millis(),
response_content_length = %response_content_length,
"HTTP response sent"
);
response
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Body, http::StatusCode, routing::get};
use tower::ServiceExt;
#[tokio::test]
async fn test_http_logging_middleware() {
// Create a test handler
async fn test_handler() -> &'static str {
"OK"
}
let app = Router::new()
.route("/test", get(test_handler))
.layer(axum::middleware::from_fn(http_logging_middleware));
let response = app
.oneshot(Request::builder().uri("/test").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_http_logging_with_headers() {
async fn test_handler() -> &'static str {
"OK"
}
let app = Router::new()
.route("/test", get(test_handler))
.layer(axum::middleware::from_fn(http_logging_middleware));
let response = app
.oneshot(
Request::builder()
.uri("/test")
.header("content-type", "application/json")
.header("user-agent", "test-agent")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -0,0 +1,55 @@
use std::str::FromStr;
use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use tracing::debug;
use crate::model::{McpConfig, McpRouterPath, McpType};
/// 提取mcp的json配置,从请求的header上,可能没有,也可能有
pub(crate) async fn mcp_json_config_extract(
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let path = req.uri().path().to_string();
debug!("Request path: {path}");
//检查请求路径,是否 /mcp 开头
let check_mcp_path = McpRouterPath::check_mcp_path(&path);
if check_mcp_path {
//请求路径,可能是: /mcp/{mcp_id}/sse,或者 /mcp/{mcp_id}/message
let mcp_router_path = McpRouterPath::from_url(&path);
if let Some(mcp_router_path) = mcp_router_path {
let mcp_id = mcp_router_path.mcp_id.clone();
// 解析header中的 x-mcp-json 字段,这个对应的是 mcp 的json配置
// 现在这个字段是base64编码过的需要先解码
let mcp_json_config = req
.headers()
.get("x-mcp-json")
.and_then(|value| value.to_str().ok())
.and_then(|encoded| {
// 将 base64 编码的值解码为原始 JSON 字符串
let decoded = BASE64
.decode(encoded)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok());
debug!("Parsed MCP configuration, x-mcp-json={:?}", &decoded);
decoded
});
// 解析header中的 x-mcp-type 字段,这个对应的是 mcp 的类型
let mcp_type = req
.headers()
.get("x-mcp-type")
.and_then(|value| value.to_str().ok())
.and_then(|s| McpType::from_str(s).ok())
.unwrap_or_default();
let mcp_protocol = mcp_router_path.mcp_protocol.clone();
let mcp_config = McpConfig::new(mcp_id, mcp_json_config, mcp_type, mcp_protocol);
req.extensions_mut().insert(mcp_config);
}
}
Ok(next.run(req).await)
}

View File

@@ -0,0 +1,67 @@
use std::task::{Context, Poll};
use axum::extract::Request;
use log::debug;
use tower::{Layer, Service};
use crate::{
get_proxy_manager,
model::{AppState, McpRouterPath},
};
#[derive(Clone)]
pub struct MySseRouterLayer {
state: AppState,
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct MySseRouterService<S> {
inner: S,
state: AppState,
}
impl MySseRouterLayer {
pub fn new(state: AppState) -> Self {
Self { state }
}
}
impl<S> Layer<S> for MySseRouterLayer {
type Service = MySseRouterService<S>;
fn layer(&self, inner: S) -> Self::Service {
MySseRouterService {
inner,
state: self.state.clone(),
}
}
}
impl<S, B> Service<Request<B>> for MySseRouterService<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let path = req.uri().path().to_string();
// ===== 复用现有逻辑:检查是否为 MCP 请求 =====
let check_mcp_path = McpRouterPath::check_mcp_path(&path);
if check_mcp_path && let Some(mcp_router_path) = McpRouterPath::from_url(&path) {
let mcp_id = mcp_router_path.mcp_id.clone();
// ===== 更新最后访问时间 =====
debug!("Update last access time, request access MCP ID: {}", mcp_id);
get_proxy_manager().update_last_accessed(&mcp_id);
}
self.inner.call(req)
}
}

View File

@@ -0,0 +1,47 @@
mod auth;
mod http_logging;
mod mcp_router_json;
mod mcp_update_latest_layer;
mod opentelemetry_middleware;
mod server_time;
use crate::model::AppState;
use axum::Router;
use axum::middleware::from_fn;
use http_logging::http_logging_middleware;
use mcp_router_json::mcp_json_config_extract;
use opentelemetry_middleware::opentelemetry_tracing_middleware;
use server_time::ServerTimeLayer;
use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
pub use mcp_update_latest_layer::MySseRouterLayer;
pub use opentelemetry_middleware::extract_trace_id;
// pub use auth::{extract_user, verify_token};
// pub trait TokenVerify {
// type Error: fmt::Debug;
// fn verify(&self, token: &str) -> Result<User, Self::Error>;
// }
const REQUEST_ID_HEADER: &str = "x-request-id";
const SERVER_TIME_HEADER: &str = "x-server-time";
pub fn set_layer(app: Router, state: AppState) -> Router {
app.layer(
ServiceBuilder::new()
// HTTP 请求/响应日志中间件 (TRACE level, 用于调试频繁的 MCP API 调用)
.layer(from_fn(http_logging_middleware))
// OpenTelemetry 追踪中间件 - 自动生成 trace_id 和 span
.layer(from_fn(opentelemetry_tracing_middleware))
// MCP 配置提取中间件
.layer(from_fn(mcp_json_config_extract))
// HTTP 压缩
.layer(CompressionLayer::new().gzip(true).br(true).deflate(true))
// 服务器时间响应头
.layer(ServerTimeLayer)
// SSE 路由层
.layer(MySseRouterLayer::new(state.clone())),
)
}

View File

@@ -0,0 +1,199 @@
use axum::{
extract::{MatchedPath, Request},
http::{HeaderMap, HeaderValue},
middleware::Next,
response::Response,
};
use opentelemetry::{Context, trace::TraceContextExt};
use std::time::Instant;
use tracing::{Instrument, debug_span};
use tracing_opentelemetry::OpenTelemetrySpanExt;
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
/// OpenTelemetry 追踪中间件
///
/// 功能:
/// 1. 自动创建 OpenTelemetry span 和 trace
/// 2. 在响应头中添加 x-request-id (trace_id)
/// 3. 在响应头中添加 x-server-time (请求处理时间)
/// 4. 记录 HTTP 请求的语义化属性
pub async fn opentelemetry_tracing_middleware(request: Request, next: Next) -> Response {
let start_time = Instant::now();
// 提取请求信息
let method = request.method().to_string();
let uri = request.uri().to_string();
let route = request
.extensions()
.get::<MatchedPath>()
.map(|matched_path| matched_path.as_str().to_owned())
.unwrap_or_else(|| request.uri().path().to_owned());
let version = format!("{:?}", request.version());
let user_agent = request
.headers()
.get("user-agent")
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown");
// 创建 OpenTelemetry span
let span = debug_span!(
"http_request",
otel.name = format!("{} {}", method, route).as_str(),
otel.kind = "server",
http.method = method.as_str(),
http.url = uri.as_str(),
http.route = route.as_str(),
http.scheme = "http",
http.version = version.as_str(),
http.user_agent = user_agent,
);
// 设置 OpenTelemetry 属性
let otel_cx = Context::current();
if let Err(error) = span.set_parent(otel_cx) {
tracing::warn!(target: "telemetry", %method, %uri, ?error, "failed to attach OpenTelemetry parent context");
}
// 获取 trace_id
let trace_id = span.context().span().span_context().trace_id().to_string();
// 如果 trace_id 全为0生成一个随机的 trace_id
let trace_id = if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
};
async move {
// 执行请求处理
let mut response = next.run(request).await;
// 计算处理时间
let duration = start_time.elapsed();
let duration_micros = duration.as_micros();
// 记录响应状态码
let status_code = response.status().as_u16();
// 添加响应头
let headers = response.headers_mut();
// 添加 trace_id 到响应头
if let Ok(trace_header) = HeaderValue::from_str(&trace_id) {
headers.insert(REQUEST_ID_HEADER, trace_header);
}
// 添加服务器处理时间到响应头 (微秒)
if let Ok(time_header) = HeaderValue::from_str(&duration_micros.to_string()) {
headers.insert(SERVER_TIME_HEADER, time_header);
}
// 记录请求完成日志(改为 debug 级别,减少日志量)
tracing::debug!(
method = %method,
uri = %uri,
status = %status_code,
duration_micros = %duration_micros,
trace_id = %trace_id,
"HTTP request completed"
);
response
}
.instrument(span)
.await
}
/// 从当前 OpenTelemetry 上下文中提取 trace_id
///
/// 这个函数可以在任何地方调用来获取当前请求的 trace_id
pub fn extract_trace_id() -> String {
let current_span = tracing::Span::current();
let context = current_span.context();
let span_ref = context.span();
let span_context = span_ref.span_context();
let trace_id = if span_context.is_valid() {
span_context.trace_id().to_string()
} else {
"00000000000000000000000000000000".to_string()
};
// 如果 trace_id 全为0生成一个随机的 trace_id
if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
}
}
/// 从请求头中提取现有的 trace_id如果有的话
///
/// 支持标准的 OpenTelemetry 传播头:
/// - traceparent (W3C Trace Context)
/// - x-trace-id (自定义)
#[allow(dead_code)]
pub fn extract_trace_from_headers(headers: &HeaderMap) -> Option<String> {
// 尝试从 W3C Trace Context 中提取
if let Some(traceparent) = headers.get("traceparent")
&& let Ok(traceparent_str) = traceparent.to_str()
{
// traceparent 格式: 00-{trace_id}-{span_id}-{flags}
let parts: Vec<&str> = traceparent_str.split('-').collect();
if parts.len() >= 2 {
return Some(parts[1].to_string());
}
}
// 尝试从自定义头中提取
if let Some(trace_id) = headers.get("x-trace-id")
&& let Ok(trace_id_str) = trace_id.to_str()
{
return Some(trace_id_str.to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderMap, HeaderValue};
#[test]
fn test_extract_trace_from_headers_traceparent() {
let mut headers = HeaderMap::new();
headers.insert(
"traceparent",
HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(
trace_id,
Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
);
}
#[test]
fn test_extract_trace_from_headers_custom() {
let mut headers = HeaderMap::new();
headers.insert(
"x-trace-id",
HeaderValue::from_static("custom-trace-id-123"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, Some("custom-trace-id-123".to_string()));
}
#[test]
fn test_extract_trace_from_headers_none() {
let headers = HeaderMap::new();
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, None);
}
}

View File

@@ -0,0 +1,65 @@
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
use axum::{extract::Request, response::Response};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tokio::time::Instant;
use tower::{Layer, Service};
use tracing::warn;
#[derive(Clone)]
pub struct ServerTimeLayer;
impl<S> Layer<S> for ServerTimeLayer {
type Service = ServerTimeMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
ServerTimeMiddleware { inner }
}
}
#[derive(Clone)]
pub struct ServerTimeMiddleware<S> {
inner: S,
}
impl<S> Service<Request> for ServerTimeMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
// `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request) -> Self::Future {
let start = Instant::now();
let future = self.inner.call(request);
Box::pin(async move {
let mut res: Response = future.await?;
let elapsed = format!("{}us", start.elapsed().as_micros());
match elapsed.parse() {
Ok(v) => {
res.headers_mut().insert(SERVER_TIME_HEADER, v);
}
Err(e) => {
warn!(
"Parse elapsed time failed: {} for request {:?}",
e,
res.headers().get(REQUEST_ID_HEADER)
);
}
}
Ok(res)
})
}
}

View File

@@ -0,0 +1,18 @@
pub mod handlers;
mod mcp_dynamic_router_service;
mod middlewares;
pub mod protocol_detector;
mod router_layer;
mod task;
pub mod telemetry;
pub use handlers::{get_health, get_ready};
pub use middlewares::set_layer;
pub use protocol_detector::{detect_mcp_protocol, detect_mcp_protocol_with_headers};
pub use router_layer::get_router;
pub use task::{mcp_start_task, schedule_check_mcp_live, start_schedule_task};
pub use telemetry::{
create_telemetry_layer, init_tracer_provider, log_service_info, shutdown_telemetry,
};

View File

@@ -0,0 +1,87 @@
//! Protocol Detection (Re-export Layer)
//!
//! This module re-exports protocol detection functions and provides
//! a convenient combined detection function.
//!
//! Detection logic: If SSE detected → Sse, else → Stream (default fallback)
// Re-export the detection functions
pub use mcp_sse_proxy::is_sse_with_headers;
use crate::model::McpProtocol;
use anyhow::Result;
use log::info;
use std::collections::HashMap;
/// Automatically detect the MCP service protocol type
///
/// Convenience wrapper around [`detect_mcp_protocol_with_headers`] that passes no
/// custom headers.
pub async fn detect_mcp_protocol(url: &str) -> Result<McpProtocol> {
detect_mcp_protocol_with_headers(url, None).await
}
/// Automatically detect the MCP service protocol type, with optional custom headers
///
/// Detection logic:
/// 1. First try to detect SSE protocol (GET /sse returns text/event-stream)
/// 2. If not SSE, default to Streamable HTTP (modern MCP standard)
///
/// # Arguments
///
/// * `url` - The URL to detect
/// * `headers` - Optional custom headers to include in the detection request
///
/// # Returns
///
/// Returns the detected protocol type (Sse or Stream)
pub async fn detect_mcp_protocol_with_headers(
url: &str,
headers: Option<&HashMap<String, String>>,
) -> Result<McpProtocol> {
info!(
"Start automatically detecting MCP service protocol: {}",
url
);
// Try SSE first
if is_sse_with_headers(url, headers).await {
info!("SSE protocol detected: {}", url);
return Ok(McpProtocol::Sse);
}
// Default to Streamable HTTP (modern MCP standard)
info!("Default uses Streamable HTTP protocol: {}", url);
Ok(McpProtocol::Stream)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_detect_invalid_url() {
// Invalid URL should default to Stream
let result = detect_mcp_protocol("not-a-url").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
#[tokio::test]
async fn test_detect_nonexistent_server() {
// Non-existent server should default to Stream
let result = detect_mcp_protocol("http://localhost:99999/mcp").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
#[tokio::test]
async fn test_detect_with_headers_nonexistent_server() {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
let result =
detect_mcp_protocol_with_headers("http://localhost:99999/mcp", Some(&headers)).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
}

View File

@@ -0,0 +1,82 @@
use axum::{
Router,
extract::DefaultBodyLimit,
routing::{delete, get, post},
};
use http::Method;
use tower_http::cors::{self, CorsLayer};
use crate::{
AppError, AppState, DynamicRouterService,
model::{GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX, McpProtocol},
server::handlers::check_mcp_is_status_handler,
};
use super::{
get_health, get_ready,
handlers::{
add_route_handler, check_mcp_status_handler_sse, check_mcp_status_handler_stream,
delete_route_handler, run_code_handler,
},
set_layer,
};
/// 获取路由
pub async fn get_router(state: AppState) -> Result<Router, AppError> {
let health = Router::new()
.route("/health", get(get_health))
.route("/ready", get(get_ready));
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([
Method::GET,
Method::POST,
Method::PATCH,
Method::DELETE,
Method::PUT,
])
.allow_origin(cors::Any)
.allow_headers(cors::Any);
let api = Router::new()
// .layer(from_fn_with_state(state.clone(), verify_token::<AppState>))
//mcp sse 协议路由
.route_service(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{{*path}}"),
DynamicRouterService(McpProtocol::Sse),
)
.route(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/add"),
post(add_route_handler),
)
.route("/mcp/config/delete/{mcp_id}", delete(delete_route_handler))
.route(
"/mcp/check/status/{mcp_id}",
get(check_mcp_is_status_handler),
)
.route(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/check_status"),
post(check_mcp_status_handler_sse),
)
//mcp stream 协议路由
.route_service(
&format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{{*path}}"),
DynamicRouterService(McpProtocol::Stream),
)
.route(
&format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/check_status"),
post(check_mcp_status_handler_stream),
)
.route("/api/run_code_with_log", post(run_code_handler))
.layer(DefaultBodyLimit::max(20 * 1024 * 1024))
.layer(cors);
// 创建基本路由
let app: Router<AppState> = Router::new().merge(health).merge(api);
// 添加状态
let app = app.with_state(state.clone());
let router = set_layer(app, state);
Ok(router)
}

View File

@@ -0,0 +1,592 @@
//! MCP Service Start Task
//!
//! This module handles starting MCP services using the Builder APIs from
//! mcp-sse-proxy and mcp-streamable-proxy libraries.
//!
//! The refactored implementation removes direct rmcp dependency by delegating
//! protocol-specific logic to the proxy libraries.
use crate::{
AppError, DynamicRouterService, get_proxy_manager,
model::GLOBAL_RESTART_TRACKER,
model::{
CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpProtocolPath, McpRouterPath,
McpServerCommandConfig, McpServerConfig, McpServiceStatus, McpType,
},
proxy::{
McpHandler, SseBackendConfig, SseServerBuilder, StreamBackendConfig, StreamServerBuilder,
},
};
use anyhow::{Context, Result};
use log::{debug, info};
use std::collections::HashMap;
/// Start an MCP service based on configuration
///
/// This function creates and configures an MCP proxy service based on the
/// provided configuration. It supports both SSE and Streamable HTTP client
/// protocols, with automatic backend protocol detection for URL-based services.
pub async fn mcp_start_task(
mcp_config: McpConfig,
) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
let mcp_id = mcp_config.mcp_id.clone();
let client_protocol = mcp_config.client_protocol.clone();
// Create router path based on client protocol (determines exposed API interface)
let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, client_protocol)
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_json_config = mcp_config
.mcp_json_config
.clone()
.expect("mcp_json_config is required");
let mcp_server_config = McpServerConfig::try_from(mcp_json_config)?;
// Use the integrated method to create the server
integrate_server_with_axum(
mcp_server_config.clone(),
mcp_router_path.clone(),
mcp_config.clone(),
)
.await
}
/// Integrate MCP server with axum router
///
/// This function:
/// 1. Determines backend protocol (stdio, SSE, or Streamable HTTP)
/// 2. Creates the appropriate server using Builder APIs
/// 3. Registers the handler with ProxyManager
/// 4. Sets up dynamic routing
pub async fn integrate_server_with_axum(
mcp_config: McpServerConfig,
mcp_router_path: McpRouterPath,
full_mcp_config: McpConfig,
) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
let mcp_type = full_mcp_config.mcp_type.clone();
let base_path = mcp_router_path.base_path.clone();
let mcp_id = mcp_router_path.mcp_id.clone();
// Determine backend protocol from configuration
let backend_protocol = match &mcp_config {
// Command-line config: use stdio protocol
McpServerConfig::Command(_) => McpProtocol::Stdio,
// URL config: parse type field or auto-detect
McpServerConfig::Url(url_config) => {
// Merge headers + auth_token for protocol detection
let mut detection_headers = normalize_headers(&url_config.headers).unwrap_or_default();
if let Some(auth_token) = &url_config.auth_token {
let value = if auth_token.starts_with("Bearer ") {
auth_token.clone()
} else {
format!("Bearer {}", auth_token)
};
detection_headers.insert("Authorization".to_string(), value);
}
let detection_headers_ref = if detection_headers.is_empty() {
None
} else {
Some(&detection_headers)
};
// Check type field first
if let Some(type_str) = &url_config.r#type {
match type_str.parse::<McpProtocol>() {
Ok(protocol) => {
debug!(
"Using configured protocol type: {} -> {:?}",
type_str, protocol
);
protocol
}
Err(_) => {
// If parsing fails, auto-detect
debug!("Protocol type '{}' unrecognized, auto-detecting", type_str);
let detected_protocol = crate::server::detect_mcp_protocol_with_headers(
url_config.get_url(),
detection_headers_ref,
)
.await
.map_err(|e| {
anyhow::anyhow!(
"Protocol type '{}' unrecognized and auto-detection failed: {}",
type_str,
e
)
})?;
debug!(
"Auto-detected protocol: {:?} (original config: '{}')",
detected_protocol, type_str
);
detected_protocol
}
}
} else {
// No type field, auto-detect
debug!("No type field specified, auto-detecting protocol");
crate::server::detect_mcp_protocol_with_headers(
url_config.get_url(),
detection_headers_ref,
)
.await
.map_err(|e| anyhow::anyhow!("Auto-detection failed: {}", e))?
}
}
};
debug!(
"MCP ID: {}, client protocol: {:?}, backend protocol: {:?}",
mcp_id, mcp_router_path.mcp_protocol, backend_protocol
);
// Create server based on client protocol using Builder APIs
let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
// ================ Client uses SSE protocol ================
McpProtocol::Sse => {
let sse_path = match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => sse_path,
_ => unreachable!(),
};
// Build backend config for SSE
let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
// Streamable HTTP backend: connect via mcp-streamable-proxy (rmcp 1.4.0),
// then pass as BackendBridge to decouple mcp-sse-proxy from mcp-streamable-proxy
let bridge = connect_stream_backend(&mcp_config, &mcp_id).await?;
SseBackendConfig::BackendBridge(bridge)
} else {
build_sse_backend_config(&mcp_config, backend_protocol)?
};
debug!(
"Creating SSE server, sse_path={}, post_path={}",
sse_path.sse_path, sse_path.message_path
);
// 对于 OneShot 服务,使用更短的 keep_alive 间隔5秒来保持后端活跃
// 防止后端进程因空闲超时而退出
let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
5
} else {
15
};
// 对于 OneShot 服务,禁用 stateful 模式以加快响应速度
// stateful=false 会跳过 MCP 初始化步骤,直接处理请求
let stateful = !matches!(mcp_type, McpType::OneShot);
let (router, ct, handler) = SseServerBuilder::new(backend_config)
.mcp_id(mcp_id.clone())
.sse_path(sse_path.sse_path.clone())
.post_path(sse_path.message_path.clone())
.keep_alive(keep_alive_secs)
.stateful(stateful)
.build()
.await
.with_context(|| {
format!(
"SSE server build failed - MCP ID: {}, type: {:?}",
mcp_id, mcp_type
)
})?;
info!(
"SSE server started - MCP ID: {}, type: {:?}",
mcp_router_path.mcp_id, mcp_type
);
(router, ct, McpHandler::Sse(Box::new(handler)))
}
// ================ Client uses Streamable HTTP protocol ================
McpProtocol::Stream => {
// Build backend config for Stream
let backend_config = build_stream_backend_config(&mcp_config, backend_protocol)?;
let (router, ct, handler) = StreamServerBuilder::new(backend_config)
.mcp_id(mcp_id.clone())
.stateful(false)
.build()
.await
.with_context(|| {
format!(
"Stream server build failed - MCP ID: {}, type: {:?}",
mcp_id, mcp_type
)
})?;
info!(
"Streamable HTTP server started - MCP ID: {}, type: {:?}",
mcp_router_path.mcp_id, mcp_type
);
(router, ct, McpHandler::Stream(Box::new(handler)))
}
// Client stdio protocol is not supported in server mode
McpProtocol::Stdio => {
return Err(anyhow::anyhow!(
"Client protocol cannot be Stdio. McpRouterPath::new does not support creating Stdio protocol router paths"
));
}
};
// Clone cancellation token for monitoring
let ct_clone = ct.clone();
let mcp_id_clone = mcp_id.clone();
// Store MCP service status with full mcp_config for auto-restart
let mcp_service_status = McpServiceStatus::new(
mcp_id_clone.clone(),
mcp_type.clone(),
mcp_router_path.clone(),
ct_clone.clone(),
CheckMcpStatusResponseStatus::Ready,
)
.with_mcp_config(full_mcp_config.clone());
// Add MCP service status and proxy handler to global manager
let proxy_manager = get_proxy_manager();
proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
// ===== 新增:注册配置到缓存 =====
proxy_manager
.register_mcp_config(&mcp_id, full_mcp_config.clone())
.await;
// Add base path fallback handler for SSE protocol
let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) {
let modified_router = router.fallback(base_path_fallback_handler);
info!("SSE base path handler added, base_path: {}", base_path);
modified_router
} else {
router
};
// Register route to global route table
info!(
"Registering route: base_path={}, mcp_id={}",
base_path, mcp_id
);
info!(
"SSE path config: sse_path={}, post_path={}",
match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path,
_ => "N/A",
},
match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => &sse_path.message_path,
_ => "N/A",
}
);
DynamicRouterService::register_route(&base_path, router.clone());
info!("Route registration complete: base_path={}", base_path);
// 记录重启时间戳(仅在服务成功启动后)
GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
Ok((router, ct))
}
/// Connect to a Streamable HTTP backend and return a BackendBridge
///
/// This lives in mcp-proxy (not mcp-sse-proxy) because it uses mcp-streamable-proxy types.
/// The returned `Arc<dyn BackendBridge>` is protocol-agnostic, allowing mcp-sse-proxy
/// to use it without depending on mcp-streamable-proxy.
async fn connect_stream_backend(
mcp_config: &McpServerConfig,
mcp_id: &str,
) -> Result<std::sync::Arc<dyn mcp_common::BackendBridge>> {
use crate::proxy::{StreamClientConnection, StreamProxyHandler};
let url_config = match mcp_config {
McpServerConfig::Url(url_config) => url_config,
_ => {
return Err(anyhow::anyhow!(
"Stream backend requires URL-based config"
))
}
};
let url = url_config.get_url();
info!(
"Connecting to Streamable HTTP backend (SSE frontend) \
- MCP ID: {}, URL: {}",
mcp_id, url
);
let mut config = mcp_common::McpClientConfig::new(url.to_string());
let normalized = normalize_headers(&url_config.headers);
if let Some(ref headers) = normalized {
for (k, v) in headers {
config = config.with_header(k, v);
}
}
// auth_token 合并到 Authorization header与 build_sse_backend_config 逻辑一致)
if let Some(ref auth_token) = url_config.auth_token {
let value = if auth_token.starts_with("Bearer ") {
auth_token.clone()
} else {
format!("Bearer {}", auth_token)
};
config = config.with_header("Authorization", value);
}
let conn = StreamClientConnection::connect(config).await?;
let proxy_handler =
StreamProxyHandler::with_mcp_id(conn.into_running_service(), mcp_id.to_string());
Ok(std::sync::Arc::new(proxy_handler))
}
/// Build SSE backend configuration from MCP server config
fn build_sse_backend_config(
mcp_config: &McpServerConfig,
backend_protocol: McpProtocol,
) -> Result<SseBackendConfig> {
match mcp_config {
McpServerConfig::Command(cmd_config) => {
log_command_details(cmd_config);
Ok(SseBackendConfig::Stdio {
command: cmd_config.command.clone(),
args: cmd_config.args.clone(),
env: cmd_config.env.clone(),
})
}
McpServerConfig::Url(url_config) => match backend_protocol {
McpProtocol::Stdio => Err(anyhow::anyhow!(
"URL-based MCP service cannot use Stdio protocol"
)),
McpProtocol::Sse => {
info!("Connecting to SSE backend: {}", url_config.get_url());
Ok(SseBackendConfig::SseUrl {
url: url_config.get_url().to_string(),
headers: normalize_headers(&url_config.headers),
})
}
McpProtocol::Stream => Err(anyhow::anyhow!(
"Stream backend should be handled via connect_stream_backend(), \
not build_sse_backend_config()"
))
},
}
}
/// Build Stream backend configuration from MCP server config
fn build_stream_backend_config(
mcp_config: &McpServerConfig,
backend_protocol: McpProtocol,
) -> Result<StreamBackendConfig> {
match mcp_config {
McpServerConfig::Command(cmd_config) => {
log_command_details(cmd_config);
Ok(StreamBackendConfig::Stdio {
command: cmd_config.command.clone(),
args: cmd_config.args.clone(),
env: cmd_config.env.clone(),
})
}
McpServerConfig::Url(url_config) => {
match backend_protocol {
McpProtocol::Stdio => Err(anyhow::anyhow!(
"URL-based MCP service cannot use Stdio protocol"
)),
McpProtocol::Sse => {
// Note: StreamServerBuilder currently only supports Streamable HTTP URL backend
// SSE backend with Stream frontend would require protocol conversion
// For now, we return an error for this combination
Err(anyhow::anyhow!(
"SSE backend with Streamable HTTP frontend is not yet supported. \
Please use SSE frontend or configure a Streamable HTTP backend."
))
}
McpProtocol::Stream => {
info!(
"Connecting to Streamable HTTP backend: {}",
url_config.get_url()
);
Ok(StreamBackendConfig::Url {
url: url_config.get_url().to_string(),
headers: normalize_headers(&url_config.headers),
})
}
}
}
}
}
/// 规范化 headers确保 Authorization header 有 "Bearer " 前缀
///
/// 与 client 模式 (`convert.rs:build_mcp_config`) 行为一致,
/// 对没有 "Bearer " 前缀的 Authorization header 自动添加前缀。
fn normalize_headers(headers: &Option<HashMap<String, String>>) -> Option<HashMap<String, String>> {
headers.as_ref().map(|h| {
h.iter()
.map(|(k, v)| {
if k.eq_ignore_ascii_case("Authorization") && !v.starts_with("Bearer ") {
(k.clone(), format!("Bearer {}", v))
} else {
(k.clone(), v.clone())
}
})
.collect()
})
}
/// Log command execution details for debugging
fn log_command_details(mcp_config: &McpServerCommandConfig) {
let args_str = mcp_config
.args
.as_ref()
.map_or(String::new(), |args| args.join(" "));
info!("Executing command: {} {}", mcp_config.command, args_str);
// 只输出 env 变量的 key 列表,避免泄露敏感 value
if let Some(env_vars) = &mcp_config.env {
let keys: Vec<&String> = env_vars.keys().collect();
if !keys.is_empty() {
debug!("Config env keys: {:?}", keys);
}
}
// 输出进程级关键环境变量PATH 摘要 + 镜像变量)
debug!(
"Process PATH: {}",
mcp_common::diagnostic::format_path_summary(3)
);
for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
debug!("Process env: {}={}", key, val);
}
}
/// Base path fallback handler - supports direct access to base path with automatic redirection
#[axum::debug_handler]
async fn base_path_fallback_handler(
method: axum::http::Method,
uri: axum::http::Uri,
headers: axum::http::HeaderMap,
) -> impl axum::response::IntoResponse {
let path = uri.path();
info!("Base path handler: {} {}", method, path);
// Determine if SSE or Stream protocol
if path.contains("/sse/proxy/") {
// SSE protocol handling
match method {
axum::http::Method::GET => {
// Extract MCP ID from path
let mcp_id = path.split("/sse/proxy/").nth(1);
if let Some(mcp_id) = mcp_id {
// Check if MCP service exists
let proxy_manager = get_proxy_manager();
if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
// MCP service not found
(
axum::http::StatusCode::NOT_FOUND,
[("Content-Type", "text/plain".to_string())],
format!("MCP service '{}' not found", mcp_id).to_string(),
)
} else {
// MCP service exists, check Accept header
let accept_header = headers.get("accept");
if let Some(accept) = accept_header {
let accept_str = accept.to_str().unwrap_or("");
if accept_str.contains("text/event-stream") {
// Correct Accept header, redirect to /sse
let redirect_uri = format!("{}/sse", path);
info!("SSE redirect to: {}", redirect_uri);
(
axum::http::StatusCode::FOUND,
[("Location", redirect_uri.to_string())],
"Redirecting to SSE endpoint".to_string(),
)
} else {
// Incorrect Accept header
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
)
}
} else {
// No Accept header
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Missing Accept header, expected 'text/event-stream'"
.to_string(),
)
}
}
} else {
// Cannot extract MCP ID from path
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Invalid SSE path".to_string(),
)
}
}
axum::http::Method::POST => {
// POST request redirect to /message
let redirect_uri = format!("{}/message", path);
info!("SSE redirect to: {}", redirect_uri);
(
axum::http::StatusCode::FOUND,
[("Location", redirect_uri.to_string())],
"Redirecting to message endpoint".to_string(),
)
}
_ => {
// Other methods return 405 Method Not Allowed
(
axum::http::StatusCode::METHOD_NOT_ALLOWED,
[("Allow", "GET, POST".to_string())],
"Only GET and POST methods are allowed".to_string(),
)
}
}
} else if path.contains("/stream/proxy/") {
// Stream protocol handling - return success directly without redirect
match method {
axum::http::Method::GET => {
// GET request returns server info
(
axum::http::StatusCode::OK,
[("Content-Type", "application/json".to_string())],
r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
)
}
axum::http::Method::POST => {
// POST request returns success, let StreamableHttpService handle
(
axum::http::StatusCode::OK,
[("Content-Type", "application/json".to_string())],
r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
)
}
_ => {
// Other methods return 405 Method Not Allowed
(
axum::http::StatusCode::METHOD_NOT_ALLOWED,
[("Allow", "GET, POST".to_string())],
"Only GET and POST methods are allowed".to_string(),
)
}
}
} else {
// Unknown protocol
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"Unknown protocol or path".to_string(),
)
}
}

View File

@@ -0,0 +1,7 @@
mod mcp_start_task;
mod schedule_check_mcp_live;
mod schedule_task;
pub use mcp_start_task::{integrate_server_with_axum, mcp_start_task};
pub use schedule_check_mcp_live::schedule_check_mcp_live;
pub use schedule_task::start_schedule_task;

View File

@@ -0,0 +1,195 @@
use crate::get_proxy_manager;
use crate::model::{CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, McpType};
use crate::server::task::mcp_start_task::mcp_start_task;
use tokio::time::Duration;
use tracing::{error, info, warn};
// OneShot 服务超时时间5分钟无活动则清理
const ONESHOT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
// 连续健康检查失败阈值:连续失败 3 次才触发重启
const MAX_PROBE_FAILURES: u32 = 3;
/// 定期检查全局动态 router 里的 MCP 服务状态
///
/// ## 处理逻辑
///
/// 1. Error 状态 → 清理资源
/// 2. 空闲超时5分钟→ 清理资源(资源回收)
/// 3. 健康检查(只对 Ready 状态)
/// - Pending → 跳过(等待启动完成)
/// - Ready → 执行探测
/// - 成功 → 重置失败计数
/// - 失败 → 失败计数 + 1
/// - 连续失败 >= 3 → 重启后端服务
pub async fn schedule_check_mcp_live() {
// 获取全局动态 router
let proxy_manager = get_proxy_manager();
// 获取所有 mcp 服务状态
let mcp_service_statuses = proxy_manager.get_all_mcp_service_status();
// 打印当前有多少个 mcp 插件服务在运行
info!(
"There are currently {} mcp plug-in services running",
mcp_service_statuses.len()
);
// 遍历所有 mcp 服务状态
for mcp_service_status in mcp_service_statuses {
// 获取服务信息
let mcp_id = mcp_service_status.mcp_id.clone();
let mcp_type = mcp_service_status.mcp_type.clone();
let cancellation_token = mcp_service_status.cancellation_token.clone();
// 1. 如果 mcp 的状态是 ERROR则清理资源
if let CheckMcpStatusResponseStatus::Error(_) =
mcp_service_status.check_mcp_status_response_status
{
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 根据 MCP 类型进行不同处理
match mcp_type {
McpType::Persistent => {
// 检查持久化服务是否已被取消或子进程已终止
if cancellation_token.is_cancelled() {
info!(
"The persistent MCP service {mcp_id} has been manually canceled and resources are being cleaned up."
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 检查子进程是否还在运行
if let Some(handler) = proxy_manager.get_proxy_handler(&mcp_id)
&& handler.is_terminated_async().await
{
info!(
"The persistent MCP service {mcp_id} child process ended abnormally and cleaned up resources."
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
}
}
McpType::OneShot => {
// 2. 检查空闲超时(基于所有请求的活动)
let idle_time = mcp_service_status.last_accessed.elapsed();
// 空闲超时 → 清理资源
if idle_time > ONESHOT_TIMEOUT {
info!(
"OneShot service {} idle timeout (idle time: {} seconds), clean up resources",
mcp_id,
idle_time.as_secs()
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 3. 健康检查(只对 Ready 状态)
// Pending 状态跳过探测,等待启动完成
if !matches!(
mcp_service_status.check_mcp_status_response_status,
CheckMcpStatusResponseStatus::Ready
) {
// Pending 状态跳过探测
continue;
}
// 执行健康探测
let handler = proxy_manager.get_proxy_handler(&mcp_id);
if let Some(handler) = handler {
let is_terminated = handler.is_terminated_async().await;
if is_terminated {
let failures = proxy_manager.increment_probe_failures(&mcp_id);
info!(
"OneShot service {} health check failed ({}/{})",
mcp_id, failures, MAX_PROBE_FAILURES
);
if failures >= MAX_PROBE_FAILURES {
info!(
"OneShot service {} failed continuously {} times, triggering a restart",
mcp_id, failures
);
restart_mcp_service(&mcp_id, proxy_manager).await;
}
} else {
// 探测成功,重置失败计数
proxy_manager.reset_probe_failures(&mcp_id);
}
}
}
}
}
}
/// 重启 MCP 服务
///
/// ## 重启流程
///
/// 1. 检查重启冷却期30秒
/// 2. 获取配置(从服务状态或缓存)
/// 3. 清理旧资源(保留配置缓存)
/// 4. 重新启动服务(复用 mcp_start_task
async fn restart_mcp_service(mcp_id: &str, proxy_manager: &crate::model::ProxyHandlerManager) {
// 1. 检查重启冷却期
if !GLOBAL_RESTART_TRACKER.can_restart(mcp_id) {
info!(
"Service {} is skipped during the restart cooling period.",
mcp_id
);
return;
}
// 2. 获取配置(优先从服务状态,其次从缓存)
let mcp_config = proxy_manager.get_mcp_config(mcp_id);
let mcp_config = match mcp_config {
Some(config) => Some(config),
None => proxy_manager.get_mcp_config_from_cache(mcp_id).await,
};
let Some(mcp_config) = mcp_config else {
warn!(
"Service {} has no configuration and cannot be restarted. Clean up resources.",
mcp_id
);
if let Err(e) = proxy_manager.cleanup_resources(mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
return;
};
// 3. 清理旧资源(保留配置缓存)
if let Err(e) = proxy_manager.cleanup_resources_for_restart(mcp_id).await {
error!("Cleanup service {} resource failed: {}", mcp_id, e);
return;
}
// 4. 重新启动服务(复用 mcp_start_task自动设置 Pending 状态)
match mcp_start_task(mcp_config).await {
Ok((_router, _cancellation_token)) => {
// 重置失败计数(已在新的服务实例中初始化为 0
// 注意:此时 mcp_id 对应的是新的服务实例
proxy_manager.reset_probe_failures(mcp_id);
// 记录重启时间
GLOBAL_RESTART_TRACKER.record_restart(mcp_id);
info!("Service {} restarted successfully", mcp_id);
}
Err(e) => {
error!("Service {} failed to restart: {}", mcp_id, e);
// 重启失败,设置 Error 状态
// 注意:此时服务已被清理,无法设置状态,只能记录日志
// 下次请求到来时会触发重新启动
}
}
}

View File

@@ -0,0 +1,65 @@
use crate::server::task::schedule_check_mcp_live;
use log::{debug, info, warn};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::{Duration, interval};
/// 启动定时任务定期检查MCP服务状态
///
/// 这个函数会创建一个tokio定时任务每隔指定的时间间隔执行一次`schedule_check_mcp_live`函数
/// 用于检查和清理不再需要的MCP服务资源
pub async fn start_schedule_task() {
info!("Start the MCP service status check scheduled task");
// 创建一个tokio定时器每20秒执行一次
let mut interval = interval(Duration::from_secs(20));
// 使用原子布尔值来跟踪任务是否正在执行
let is_running = Arc::new(AtomicBool::new(false));
// 启动一个新的异步任务
tokio::spawn(async move {
loop {
// 等待下一个时间点
interval.tick().await;
// 检查是否有任务正在执行
if is_running.load(Ordering::SeqCst) {
warn!(
"The last MCP service status check task has not been completed and this execution will be skipped."
);
continue;
}
// 标记任务开始执行
is_running.store(true, Ordering::SeqCst);
// 执行MCP服务状态检查
debug!("Perform periodic checks on MCP service status...");
// 在一个新的任务中执行检查,这样可以捕获任何异常
let is_running_clone = is_running.clone();
tokio::spawn(async move {
// 执行检查任务
match tokio::time::timeout(
Duration::from_secs(10), // 设置超时时间为10秒小于间隔时间
schedule_check_mcp_live(),
)
.await
{
Ok(_) => {
debug!("MCP service status check completed");
}
Err(_) => {
warn!("MCP service status check task timed out");
}
}
// 无论成功还是失败,都标记任务已完成
is_running_clone.store(false, Ordering::SeqCst);
});
}
});
info!("MCP service status check scheduled task has been started");
}

View File

@@ -0,0 +1,60 @@
use anyhow::Result;
use opentelemetry::global;
use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider};
/// 初始化 OpenTelemetry tracer provider
///
/// 这个函数必须在创建 telemetry layer 之前调用
pub fn init_tracer_provider(_service_name: &str, _service_version: &str) -> Result<()> {
// 创建 tracer provider
let tracer_provider = SdkTracerProvider::builder()
.with_sampler(Sampler::AlwaysOn)
.with_id_generator(RandomIdGenerator::default())
.build();
// 设置全局 tracer provider
global::set_tracer_provider(tracer_provider);
Ok(())
}
/// 创建增强的 OpenTelemetry layer
///
/// 这个函数创建一个配置好的 OpenTelemetry layer可以与现有的 tracing 配置集成
/// 注意:必须先调用 init_tracer_provider()
pub fn create_telemetry_layer() -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
tracing_opentelemetry::layer()
}
/// 记录服务启动信息
///
/// 在 telemetry 系统初始化后调用,记录服务的基本信息
pub fn log_service_info(service_name: &str, service_version: &str) -> Result<()> {
tracing::info!(
service_name = %service_name,
service_version = %service_version,
"Service started with OpenTelemetry tracing enabled"
);
Ok(())
}
/// 优雅关闭 OpenTelemetry
pub fn shutdown_telemetry() {
tracing::info!("Shutting down OpenTelemetry");
// 注意:在新版本的 OpenTelemetry 中shutdown 方法可能不同
// 这里我们简单地记录关闭信息
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_service_info() {
let result = log_service_info("test-service", "0.1.0");
assert!(result.is_ok());
// 清理
shutdown_telemetry();
}
}

View File

@@ -0,0 +1,157 @@
//! Coze MCP Service Integration Tests
//!
//! This module tests the protocol conversion from Streamable HTTP (backend) to SSE (frontend)
//! when connecting to Coze MCP services.
//!
//! # Configuration
//!
//! These tests require the following environment variables:
//!
//! - `COZE_PLUGIN_ID` - Your Coze plugin ID
//! - `COZE_BEARER_TOKEN` - Your Coze API Bearer token
//!
//! # Running the tests
//!
//! ```bash
//! # Set environment variables and run the test
//! COZE_PLUGIN_ID="your_plugin_id" \
//! COZE_BEARER_TOKEN="your_bearer_token" \
//! cargo test -p mcp-stdio-proxy test_coze_streamable_to_sse_proxy
//!
//! # Or run with logging
//! COZE_PLUGIN_ID="your_plugin_id" \
//! COZE_BEARER_TOKEN="your_bearer_token" \
//! RUST_LOG=debug cargo test -p mcp-stdio-proxy test_coze_streamable_to_sse_proxy
//! ```
use anyhow::Result;
use std::time::Duration;
use tokio::net::TcpListener;
use crate::{
mcp_start_task,
model::{McpConfig, McpProtocol, McpType},
proxy::{McpClientConfig, SseClientConnection},
};
/// Builds Coze MCP configuration from environment variables
fn get_coze_config() -> Result<String> {
let plugin_id = std::env::var("COZE_PLUGIN_ID")
.map_err(|_| anyhow::anyhow!("COZE_PLUGIN_ID environment variable not set"))?;
let bearer_token = std::env::var("COZE_BEARER_TOKEN")
.map_err(|_| anyhow::anyhow!("COZE_BEARER_TOKEN environment variable not set"))?;
let config = r#"{
"mcpServers": {
"coze_plugin_tianyancha": {
"url": "https://mcp.coze.cn/v1/plugins/PLUGIN_ID",
"headers": {
"Authorization": "Bearer BEARER_TOKEN"
}
}
}
}"#;
Ok(config
.replace("PLUGIN_ID", &plugin_id)
.replace("BEARER_TOKEN", &bearer_token))
}
/// Test: Streamable HTTP backend to SSE frontend protocol conversion
///
/// This test verifies that the mcp-proxy correctly:
/// 1. Configures SSE protocol for the client (frontend)
/// 2. Auto-detects Streamable HTTP protocol for the Coze backend
/// 3. Transparently converts between the two protocols
/// 4. Returns valid tools/list responses
#[tokio::test]
#[ignore] // Mark as ignored since it requires network access
async fn test_coze_streamable_to_sse_proxy() -> Result<()> {
// Initialize logging for test
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
println!("🧪 Starting Coze MCP test: Streamable HTTP -> SSE conversion");
// Step 1: Create configuration with SSE client protocol
// The backend protocol (Streamable HTTP) will be auto-detected
let coze_config = get_coze_config()?;
let mcp_config = McpConfig::from_json_with_server(
"coze_plugin_tianyancha".to_string(),
coze_config,
McpType::OneShot,
McpProtocol::Sse, // SSE frontend (client protocol)
)?;
println!("✅ Configuration created with SSE client protocol");
// Step 2: Start MCP service
// The proxy will auto-detect the backend protocol and create appropriate routes
let (router, ct) = mcp_start_task(mcp_config).await?;
println!("✅ MCP service started");
// Step 3: Start HTTP server with the router
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
println!("✅ HTTP server listening on 127.0.0.1:{}", port);
// Spawn server in background
let server_handle = tokio::spawn(async move {
axum::serve(listener, router.into_make_service())
.await
.expect("Server error");
});
// Step 4: Wait for backend to be ready
tokio::time::sleep(Duration::from_secs(3)).await;
println!("✅ Backend ready");
// Step 5: Construct SSE endpoint path
// The SSE server exposes endpoints at /mcp/sse/proxy/{mcp_id}/sse
let sse_url = format!(
"http://127.0.0.1:{}/mcp/sse/proxy/coze_plugin_tianyancha/sse",
port
);
// Step 6: Connect SSE client
let client_config = McpClientConfig::new(sse_url.to_string());
let conn = tokio::time::timeout(
Duration::from_secs(30),
SseClientConnection::connect(client_config.clone()),
)
.await
.map_err(|_| anyhow::anyhow!("SSE connection timeout (30s)"))?
.map_err(|e| anyhow::anyhow!("SSE connection failed: {}", e))?;
println!("✅ SSE client connected to {}", sse_url);
// Step 7: Get tools list using the high-level API
let tools = tokio::time::timeout(Duration::from_secs(30), conn.list_tools())
.await
.map_err(|_| anyhow::anyhow!("list_tools timeout (30s)"))?
.map_err(|e| anyhow::anyhow!("list_tools failed: {}", e))?;
println!("📋 Received tools/list response: {} tools", tools.len());
// Step 8: Verify response structure
if tools.is_empty() {
println!("⚠️ Warning: tools/list returned empty array");
} else {
println!("✅ Found {} tools:", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
println!(" - {} : {}", tool.name, desc);
}
}
// Step 9: Verify tool structure
for tool in &tools {
assert!(!tool.name.is_empty(), "Tool name should not be empty");
// Description is optional, so we just check the name
println!(" ✓ Tool '{}' has valid structure", tool.name);
}
// Step 10: Cleanup
ct.cancel();
server_handle.abort();
println!("🧹 Cleanup complete");
Ok(())
}

View File

@@ -0,0 +1,12 @@
#[cfg(test)]
pub mod test_utils {
// Tests utility module for common test setup
}
// Coze MCP integration tests - Streamable HTTP to SSE conversion
#[cfg(test)]
pub mod coze_mcp_test;
// Protocol detection tests - SSE vs Streamable HTTP
#[cfg(test)]
pub mod protocol_detection_test;

View File

@@ -0,0 +1,270 @@
//! Protocol Detection Integration Tests
//!
//! Tests protocol detection (SSE vs Streamable HTTP) against real MCP services.
//!
//! # Running the tests
//!
//! ```bash
//! # Run the zimage streamable HTTP test (requires DASHSCOPE_API_KEY)
//! DASHSCOPE_API_KEY=sk-xxx cargo test -p mcp-stdio-proxy test_zimage_protocol_detection -- --ignored --nocapture
//!
//! # Run the howtocook SSE test
//! cargo test -p mcp-stdio-proxy test_howtocook_sse_protocol_detection -- --ignored --nocapture
//!
//! # Run all protocol detection network tests
//! DASHSCOPE_API_KEY=sk-xxx cargo test -p mcp-stdio-proxy protocol_detection_test -- --ignored --nocapture
//!
//! # Run with debug logging
//! DASHSCOPE_API_KEY=sk-xxx RUST_LOG=debug cargo test -p mcp-stdio-proxy protocol_detection_test -- --ignored --nocapture
//! ```
use crate::model::{McpJsonServerParameters, McpProtocol, McpServerConfig};
/// The howtocook SSE MCP JSON config (SSE protocol, 测试环境密钥)
const HOWTOCOOK_MCP_JSON: &str = r#"{
"mcpServers": {
"howtocook-跳跳糖": {
"type": "sse",
"url": "https://testagent.xspaceagi.com/api/mcp/sse?ak=ak-27b83516dfd4417a82f764fe3e859a6e"
}
}
}"#;
/// Build zimage MCP JSON config with Authorization token from environment variable
///
/// 环境变量: `DASHSCOPE_API_KEY`
/// 运行网络测试前需设置: `export DASHSCOPE_API_KEY=sk-xxx`
fn build_zimage_mcp_json(api_key: &str) -> String {
format!(
r#"{{
"mcpServers": {{
"zimage": {{
"type": "streamableHttp",
"description": "Z-Image-Turbo 图像生成模型",
"isActive": true,
"name": "阿里云百炼_Z Image 图像生成",
"baseUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp",
"headers": {{
"Authorization": "Bearer {}"
}}
}}
}}
}}"#,
api_key
)
}
/// 用于本地解析测试的占位密钥(不需要真实密钥)
const ZIMAGE_TEST_PLACEHOLDER_KEY: &str = "test-placeholder-key";
// ==================== 本地解析测试(无网络) ====================
#[test]
fn test_zimage_config_type_parsed_as_stream() {
let zimage_json = build_zimage_mcp_json(ZIMAGE_TEST_PLACEHOLDER_KEY);
let params = McpJsonServerParameters::from(zimage_json);
let config = params.try_get_first_mcp_server().unwrap();
match config {
McpServerConfig::Url(url_config) => {
// 1. 原始 type 字段值
assert_eq!(url_config.r#type, Some("streamableHttp".to_string()));
// 2. get_protocol_type() 返回 Some且 is_streamable
let protocol_type = url_config.get_protocol_type();
assert!(
protocol_type.is_some(),
"streamableHttp should be recognized"
);
assert!(protocol_type.as_ref().unwrap().is_streamable());
// 3. 转换为 McpProtocol::Stream
assert_eq!(
protocol_type.unwrap().to_mcp_protocol(),
McpProtocol::Stream
);
// 4. FromStr 解析也能识别 "streamableHttp"
assert_eq!(
"streamableHttp".parse::<McpProtocol>(),
Ok(McpProtocol::Stream)
);
// 5. URL 正确解析
assert_eq!(
url_config.get_url(),
"https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp"
);
// 6. headers 包含 Authorization
let headers = url_config.headers.as_ref().unwrap();
assert!(headers.contains_key("Authorization"));
assert_eq!(
headers["Authorization"],
format!("Bearer {}", ZIMAGE_TEST_PLACEHOLDER_KEY)
);
}
McpServerConfig::Command(_) => panic!("Expected URL config"),
}
}
// ==================== 网络探测测试(需要网络,默认 ignore ====================
/// Test: SSE detector should return false for a Streamable HTTP service
///
/// 使用真实 zimage 服务验证:
/// - is_sse_with_headers → false不是 SSE
/// - is_streamable_http_with_headers → true是 Streamable HTTP
/// - detect_mcp_protocol_with_headers → Stream
///
/// 需要设置环境变量: `DASHSCOPE_API_KEY`
#[tokio::test]
#[ignore] // 需要网络访问 + DASHSCOPE_API_KEY 环境变量
async fn test_zimage_protocol_detection() {
let api_key = std::env::var("DASHSCOPE_API_KEY")
.expect("需要设置 DASHSCOPE_API_KEY 环境变量才能运行此测试");
let zimage_json = build_zimage_mcp_json(&api_key);
let params = McpJsonServerParameters::from(zimage_json);
let config = params.try_get_first_mcp_server().unwrap();
let (url, headers) = match config {
McpServerConfig::Url(url_config) => {
let url = url_config.get_url().to_string();
let headers = url_config.headers.clone().unwrap_or_default();
(url, headers)
}
_ => panic!("Expected URL config"),
};
println!("=== Protocol detection test: {} ===", url);
// 1. SSE 探测应返回 false
println!("\\n--- SSE Probing ---");
let is_sse = mcp_sse_proxy::is_sse_with_headers(&url, Some(&headers)).await;
println!("is_sse_with_headers = {}", is_sse);
assert!(!is_sse, "Streamable HTTP 服务不应被识别为 SSE");
// 2. Streamable HTTP 探测应返回 true
println!("\\n--- Streamable HTTP probe ---");
let is_stream =
mcp_streamable_proxy::is_streamable_http_with_headers(&url, Some(&headers)).await;
println!("is_streamable_http_with_headers = {}", is_stream);
assert!(
is_stream,
"zimage 服务应被识别为 Streamable HTTP需要传递 Authorization header"
);
// 3. 综合探测应返回 Stream
println!("\\n--- Comprehensive protocol detection ---");
let detected = crate::server::detect_mcp_protocol_with_headers(&url, Some(&headers))
.await
.unwrap();
println!("detect_mcp_protocol_with_headers = {:?}", detected);
assert_eq!(detected, McpProtocol::Stream, "综合探测应返回 Stream 协议");
println!("\\n=== All detection results are correct ===");
}
// ==================== howtocook SSE 本地解析测试 ====================
#[test]
fn test_howtocook_config_type_parsed_as_sse() {
let params = McpJsonServerParameters::from(HOWTOCOOK_MCP_JSON.to_string());
let config = params.try_get_first_mcp_server().unwrap();
match config {
McpServerConfig::Url(url_config) => {
// 1. 原始 type 字段值
assert_eq!(url_config.r#type, Some("sse".to_string()));
// 2. get_protocol_type() 返回 Some且不是 streamable
let protocol_type = url_config.get_protocol_type();
assert!(protocol_type.is_some(), "sse should be recognized");
assert!(!protocol_type.as_ref().unwrap().is_streamable());
// 3. 转换为 McpProtocol::Sse
assert_eq!(protocol_type.unwrap().to_mcp_protocol(), McpProtocol::Sse);
// 4. FromStr 解析也能识别 "sse"
assert_eq!("sse".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
assert_eq!("SSE".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
// 5. URL 正确解析(使用 url 字段而非 baseUrl
assert_eq!(
url_config.get_url(),
"https://testagent.xspaceagi.com/api/mcp/sse?ak=ak-27b83516dfd4417a82f764fe3e859a6e"
);
// 6. 无 headers
assert!(url_config.headers.is_none());
}
McpServerConfig::Command(_) => panic!("Expected URL config"),
}
}
// ==================== howtocook SSE 网络探测测试 ====================
/// Test: SSE detector should return true for a real SSE service
///
/// 使用真实 howtocook SSE 服务验证:
/// - is_sse_with_headers → true是 SSE应探测到 event: endpoint
/// - detect_mcp_protocol_with_headers → Sse
#[tokio::test]
#[ignore] // 需要网络访问,使用 --ignored 运行
async fn test_howtocook_sse_protocol_detection() {
let params = McpJsonServerParameters::from(HOWTOCOOK_MCP_JSON.to_string());
let config = params.try_get_first_mcp_server().unwrap();
let url = match config {
McpServerConfig::Url(url_config) => url_config.get_url().to_string(),
_ => panic!("Expected URL config"),
};
println!("=== SSE protocol detection test: {} ===", url);
// 1. SSE 探测应返回 true该服务是真实 MCP SSE会发送 event: endpoint
println!("\\n--- SSE Probing ---");
let is_sse = mcp_sse_proxy::is_sse(&url).await;
println!("is_sse = {}", is_sse);
assert!(
is_sse,
"howtocook SSE 服务应被识别为 SSE发现 event: endpoint"
);
// 2. Streamable HTTP 探测应返回 falseSSE 服务不应被识别为 Streamable HTTP
println!("\\n--- Streamable HTTP probe ---");
let is_stream = mcp_streamable_proxy::is_streamable_http(&url).await;
println!("is_streamable_http = {}", is_stream);
assert!(!is_stream, "SSE 服务不应被识别为 Streamable HTTP");
// 3. 综合探测应返回 Sse
println!("\\n--- Comprehensive protocol detection ---");
let detected = crate::server::detect_mcp_protocol(&url).await.unwrap();
println!("detect_mcp_protocol = {:?}", detected);
assert_eq!(detected, McpProtocol::Sse, "综合探测应返回 Sse 协议");
println!("\\n=== SSE detection result is correct ===");
}
/// Test: protocol detection without headers should still default to Stream
///
/// 不传 Authorization header探测可能失败但应兜底为 Stream
#[tokio::test]
#[ignore] // 需要网络访问
async fn test_zimage_detection_without_headers() {
let url = "https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp";
println!("=== No header detection test: {} ===", url);
// SSE 探测应返回 false
let is_sse = mcp_sse_proxy::is_sse(url).await;
println!("is_sse = {}", is_sse);
assert!(!is_sse);
// 综合探测应兜底为 Stream
let detected = crate::server::detect_mcp_protocol(url).await.unwrap();
println!("detect_mcp_protocol = {:?}", detected);
assert_eq!(detected, McpProtocol::Stream, "无 header 时应兜底为 Stream");
println!("=== No header, the detection result is correct ===");
}