修复: MCP 多进程缺陷(update CAS乐观锁 + 审计落盘 + 空闲超时 + busy_timeout + stdio写回调 + list分页)
This commit is contained in:
+87
-10
@@ -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}");
|
||||
|
||||
Reference in New Issue
Block a user