提交qiming-mcp-proxy
This commit is contained in:
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
11
qiming-mcp-proxy/mcp-proxy/src/server/handlers/health.rs
Normal file
11
qiming-mcp-proxy/mcp-proxy/src/server/handlers/health.rs
Normal 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())
|
||||
}
|
||||
@@ -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("无效的请求路径"));
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.mcp_id, CheckMcpStatusResponseStatus::Ready);
|
||||
}
|
||||
// 更新最后访问时间
|
||||
proxy_manager.update_last_accessed(¶ms.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(¶ms.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(¶ms.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(¶ms.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(
|
||||
¶ms.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(())
|
||||
}
|
||||
13
qiming-mcp-proxy/mcp-proxy/src/server/handlers/mod.rs
Normal file
13
qiming-mcp-proxy/mcp-proxy/src/server/handlers/mod.rs
Normal 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;
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
177
qiming-mcp-proxy/mcp-proxy/src/server/middlewares/auth.rs
Normal file
177
qiming-mcp-proxy/mcp-proxy/src/server/middlewares/auth.rs
Normal 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(())
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
47
qiming-mcp-proxy/mcp-proxy/src/server/middlewares/mod.rs
Normal file
47
qiming-mcp-proxy/mcp-proxy/src/server/middlewares/mod.rs
Normal 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())),
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
18
qiming-mcp-proxy/mcp-proxy/src/server/mod.rs
Normal file
18
qiming-mcp-proxy/mcp-proxy/src/server/mod.rs
Normal 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,
|
||||
};
|
||||
87
qiming-mcp-proxy/mcp-proxy/src/server/protocol_detector.rs
Normal file
87
qiming-mcp-proxy/mcp-proxy/src/server/protocol_detector.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
82
qiming-mcp-proxy/mcp-proxy/src/server/router_layer.rs
Normal file
82
qiming-mcp-proxy/mcp-proxy/src/server/router_layer.rs
Normal 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)
|
||||
}
|
||||
592
qiming-mcp-proxy/mcp-proxy/src/server/task/mcp_start_task.rs
Normal file
592
qiming-mcp-proxy/mcp-proxy/src/server/task/mcp_start_task.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
qiming-mcp-proxy/mcp-proxy/src/server/task/mod.rs
Normal file
7
qiming-mcp-proxy/mcp-proxy/src/server/task/mod.rs
Normal 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;
|
||||
@@ -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 状态
|
||||
// 注意:此时服务已被清理,无法设置状态,只能记录日志
|
||||
// 下次请求到来时会触发重新启动
|
||||
}
|
||||
}
|
||||
}
|
||||
65
qiming-mcp-proxy/mcp-proxy/src/server/task/schedule_task.rs
Normal file
65
qiming-mcp-proxy/mcp-proxy/src/server/task/schedule_task.rs
Normal 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");
|
||||
}
|
||||
60
qiming-mcp-proxy/mcp-proxy/src/server/telemetry.rs
Normal file
60
qiming-mcp-proxy/mcp-proxy/src/server/telemetry.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user