修复: MCP 多进程缺陷(update CAS乐观锁 + 审计落盘 + 空闲超时 + busy_timeout + stdio写回调 + list分页)

This commit is contained in:
lxy
2026-08-08 18:43:38 +08:00
parent 6aa334fc9b
commit 02c8d8e5ea
5 changed files with 559 additions and 55 deletions
+87 -10
View File
@@ -4,10 +4,42 @@
use std::path::PathBuf;
use std::process::ExitCode;
/// MCP 进程本地时区日志时间戳格式化器(与 GUI lib.rs 同款,对齐日志格式)。
/// tracing_subscriber 默认输出 UTC,改为 chrono::Local 输出含时区偏移的本地时间。
struct LocalTimer;
impl tracing_subscriber::fmt::time::FormatTime for LocalTimer {
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
write!(w, "{}", chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.6f%:z"))
}
}
/// 初始化 MCP 进程的文件日志:追加到 %TEMP%/devflow-trace.log(与 GUI 同文件),
/// RUST_LOG 控制级别(默认 info)。返回的 guard 需保持存活到 server 退出,
/// 否则 non_blocking worker 提前关闭会丢失日志。
fn init_mcp_tracing() -> tracing_appender::non_blocking::WorkerGuard {
let log_path = std::env::temp_dir().join("devflow-trace.log");
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.expect("创建日志文件失败");
let (non_blocking, guard) = tracing_appender::non_blocking(log_file);
tracing_subscriber::fmt()
.with_timer(LocalTimer)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
)
.with_writer(non_blocking)
.with_ansi(false)
.init();
guard
}
fn main() -> ExitCode {
// CLI 子命令:手动解析 argv(避免引入 clap 重依赖)。
//
// `devflow mcp-server [--db <path>] [--read-only] [--stdio] [--print-config]`
// `devflow mcp-server [--db <path>] [--read-only] [--stdio] [--idle-timeout <secs>] [--print-config]`
//
// 仅识别首个非程序名参数是否为 `mcp-server`;其余情况走 GUI。
// stdio 句柄由父进程(MCP 客户端)继承,无需额外配置。
@@ -22,14 +54,17 @@ fn main() -> ExitCode {
/// 解析 mcp-server 子命令参数并启动 MCP server。
///
/// - `--db <path>`:指定 SQLite 路径(默认:系统 app_data_dir/devflow[-dev].db)
/// - `--read-only`:仅暴露 list/get 工具
/// - `--read-only`:仅暴露 list/get 工具(生产环境建议开启,拒绝全部写库)
/// - `--stdio`:显式声明 stdio 形态(默认即 stdio;headless/CI/GUI 未启动场景)
/// - `--idle-timeout <secs>`:空闲 N 秒无请求自动退出(0=禁用;默认 60)
/// - `--print-config`:输出 MCP 配置到 stdout 后退出(默认 HTTP 形态,加 --stdio 输出 stdio command 形态)
fn run_mcp_server(args: &[String]) -> ExitCode {
let mut db_path: Option<PathBuf> = None;
let mut read_only = false;
let mut stdio_mode = false;
let mut print_config = false;
// 空闲超时:None=未显式指定(用默认 60s);Some(0)=禁用;Some(n)=n 秒
let mut idle_timeout_arg: Option<u64> = None;
let mut i = 0;
while i < args.len() {
@@ -45,13 +80,28 @@ fn run_mcp_server(args: &[String]) -> ExitCode {
"--read-only" => read_only = true,
"--stdio" => stdio_mode = true,
"--print-config" => print_config = true,
"--idle-timeout" => {
i += 1;
if i >= args.len() {
eprintln!("--idle-timeout 需要参数(整数秒,0=禁用)");
return ExitCode::FAILURE;
}
match args[i].parse::<u64>() {
Ok(secs) => idle_timeout_arg = Some(secs),
Err(_) => {
eprintln!("--idle-timeout 参数需为整数秒: {}", args[i]);
return ExitCode::FAILURE;
}
}
}
"-h" | "--help" => {
println!("devflow mcp-server [--db <path>] [--read-only] [--stdio] [--print-config]");
println!("devflow mcp-server [--db <path>] [--read-only] [--stdio] [--idle-timeout <secs>] [--print-config]");
println!();
println!(" --db <path> 指定 SQLite 数据库路径(默认:app_data_dir/<identifier>/devflow[-dev].db)");
println!(" --read-only 仅暴露 list/get 工具(拒绝 create/update/delete)");
println!(" --stdio 显式声明 stdio 形态(默认即 stdio;headless/CI/GUI 未启动场景)");
println!(" --print-config 输出 MCP 配置(默认 HTTP,加 --stdio 输出 stdio command 形态)");
println!(" --db <path> 指定 SQLite 数据库路径(默认:app_data_dir/<identifier>/devflow[-dev].db)");
println!(" --read-only 仅暴露 list/get 工具(拒绝全部写库,生产环境建议开启)");
println!(" --stdio 显式声明 stdio 形态(默认即 stdio;headless/CI/GUI 未启动场景)");
println!(" --idle-timeout <secs> 空闲 N 秒无请求自动退出(防孤儿进程残留;0=禁用;默认 60)");
println!(" --print-config 输出 MCP 配置(默认 HTTP,加 --stdio 输出 stdio command 形态)");
return ExitCode::SUCCESS;
}
other => {
@@ -78,21 +128,27 @@ fn run_mcp_server(args: &[String]) -> ExitCode {
}
// 输出 Claude Code / Cursor 兼容的 MCP 配置片段(command/args 形态,显式带 --stdio)。
// Claude Code: claude mcp add devflow -- devflow mcp-server --stdio [--read-only]
// Claude Code: claude mcp add devflow -- devflow mcp-server --stdio [--read-only] [--idle-timeout N]
// Cursor: 在 ~/.cursor/mcp.json 加 mcpServers.devflow
let read_only_flag = if read_only { r#","--read-only""# } else { "" };
// 空闲超时:用户显式指定过才写进配置片段(未指定由 server 用默认 60s,不必冗余)。
let idle_flag = match idle_timeout_arg {
Some(secs) => format!(r#","--idle-timeout","{secs}""#),
None => String::new(),
};
// 用当前可执行文件绝对路径(command 需要能被客户端直接执行;硬编码 devflow
// 在 Windows 上是 DevFlow.exe,且 PATH 未必含安装目录)。
let exe = std::env::current_exe()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_else(|_| "devflow".to_owned());
let json = format!(
r#"{{"mcpServers":{{"devflow":{{"command":"{exe}","args":["mcp-server","--stdio"{read_only_flag}]}}}}}}"#
r#"{{"mcpServers":{{"devflow":{{"command":"{exe}","args":["mcp-server","--stdio"{read_only_flag}{idle_flag}]}}}}}}"#
);
println!("{json}");
eprintln!("# Claude Code 接入(stdio 形态):");
eprintln!("# claude mcp add devflow -- {exe} mcp-server --stdio");
eprintln!("# Cursor 接入:把上面 JSON 写入 ~/.cursor/mcp.json");
eprintln!("# 安全提示:生产环境建议加 --read-only(只读),写库请在 DevFlow 应用内操作");
return ExitCode::SUCCESS;
}
@@ -109,6 +165,27 @@ fn run_mcp_server(args: &[String]) -> ExitCode {
},
};
// MCP 是独立进程,不经 GUI lib.rs 的 run() 路径,须单独 init tracing subscriber;
// 否则 df_mcp 内 medium_audit 的 tracing::warn! 无 subscriber 是 no-op,审计不落盘。
// guard 绑定在此函数作用域,存活到 server 退出,保证日志 worker 持续运行。
let _log_guard = init_mcp_tracing();
tracing::info!(path = %db_path.display(), "[startup] MCP server 日志已初始化");
// 空闲超时:未显式指定用默认 60s(对齐 ServerConfig::default);显式 0 = 禁用(常驻)。
let idle_timeout = match idle_timeout_arg {
Some(0) => None,
Some(secs) => Some(std::time::Duration::from_secs(secs)),
None => Some(std::time::Duration::from_secs(60)),
};
let config = df_mcp::ServerConfig {
read_only,
idle_timeout,
// 写操作回调:GUI 内嵌 HTTP 形态经 src-tauri/src/mcp.rs → on_tool_call 已接
// emit df-data-changed 刷新前端;stdio 独立进程暂无 AppHandle,暂传 None,
// 架构上预留接线点,后续注入 AppHandle 即可让 stdio 形态 MCP 写库也触发 GUI 刷新。
on_write_call: None,
};
// 启动 MCP server(tokio runtime)
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
@@ -117,7 +194,7 @@ fn run_mcp_server(args: &[String]) -> ExitCode {
return ExitCode::FAILURE;
}
};
match rt.block_on(df_mcp::run_server(&db_path, read_only)) {
match rt.block_on(df_mcp::run_server(&db_path, config)) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("MCP server 异常退出: {e}");