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