// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 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 ] [--read-only] [--stdio] [--idle-timeout ] [--print-config]` // // 仅识别首个非程序名参数是否为 `mcp-server`;其余情况走 GUI。 // stdio 句柄由父进程(MCP 客户端)继承,无需额外配置。 let args: Vec = std::env::args().collect(); if args.iter().nth(1).map(|s| s.as_str()) == Some("mcp-server") { return run_mcp_server(&args[2..]); } devflow_lib::run(); ExitCode::SUCCESS } /// 解析 mcp-server 子命令参数并启动 MCP server。 /// /// - `--db `:指定 SQLite 路径(默认:系统 app_data_dir/devflow[-dev].db) /// - `--read-only`:仅暴露 list/get 工具(生产环境建议开启,拒绝全部写库) /// - `--stdio`:显式声明 stdio 形态(默认即 stdio;headless/CI/GUI 未启动场景) /// - `--idle-timeout `:空闲 N 秒无请求自动退出(0=禁用;默认 60) /// - `--print-config`:输出 MCP 配置到 stdout 后退出(默认 HTTP 形态,加 --stdio 输出 stdio command 形态) fn run_mcp_server(args: &[String]) -> ExitCode { let mut db_path: Option = 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 = None; let mut i = 0; while i < args.len() { match args[i].as_str() { "--db" => { i += 1; if i >= args.len() { eprintln!("--db 需要参数"); return ExitCode::FAILURE; } db_path = Some(PathBuf::from(&args[i])); } "--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::() { Ok(secs) => idle_timeout_arg = Some(secs), Err(_) => { eprintln!("--idle-timeout 参数需为整数秒: {}", args[i]); return ExitCode::FAILURE; } } } "-h" | "--help" => { println!("devflow mcp-server [--db ] [--read-only] [--stdio] [--idle-timeout ] [--print-config]"); println!(); println!(" --db 指定 SQLite 数据库路径(默认:app_data_dir//devflow[-dev].db)"); println!(" --read-only 仅暴露 list/get 工具(拒绝全部写库,生产环境建议开启)"); println!(" --stdio 显式声明 stdio 形态(默认即 stdio;headless/CI/GUI 未启动场景)"); println!(" --idle-timeout 空闲 N 秒无请求自动退出(防孤儿进程残留;0=禁用;默认 60)"); println!(" --print-config 输出 MCP 配置(默认 HTTP,加 --stdio 输出 stdio command 形态)"); return ExitCode::SUCCESS; } other => { eprintln!("未知参数: {other}(用 --help 查看)"); return ExitCode::FAILURE; } } i += 1; } if print_config { // 说明性注释(非 JSON)打 stderr,保证 stdout 是纯 JSON 可直接被客户端消费。 if !stdio_mode { // 默认输出 HTTP 形态:桌面进程内嵌 HTTP MCP server + 托盘常驻, // Claude Code 用 --transport http 直连同一进程。端口 18765 与 // crates/df-mcp/src/server_http.rs 的 DEFAULT_MCP_PORT 对齐(本文件不 import,直接写死)。 let json = r#"{"mcpServers":{"devflow":{"transport":"http","url":"http://127.0.0.1:18765/mcp"}}}"#; println!("{json}"); eprintln!("# Claude Code 接入(需先启动 DevFlow 桌面进程,托盘常驻):"); eprintln!("# claude mcp add --transport http devflow http://127.0.0.1:18765/mcp"); eprintln!("# stdio 备选(无 GUI 场景):"); eprintln!("# devflow mcp-server --stdio --print-config"); return ExitCode::SUCCESS; } // 输出 Claude Code / Cursor 兼容的 MCP 配置片段(command/args 形态,显式带 --stdio)。 // 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}{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; } // 解析 db 路径(未指定则用系统 app_data_dir) let db_path = match db_path { Some(p) => p, None => match default_db_path() { Ok(p) => p, Err(e) => { eprintln!("无法定位默认数据库目录: {e}"); eprintln!("请用 --db 显式指定"); return ExitCode::FAILURE; } }, }; // 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, Err(e) => { eprintln!("启动 tokio runtime 失败: {e}"); return ExitCode::FAILURE; } }; match rt.block_on(df_mcp::run_server(&db_path, config)) { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("MCP server 异常退出: {e}"); ExitCode::FAILURE } } } /// 默认数据库路径:与 GUI 应用同库(系统 app_data_dir/{identifier}/devflow[-dev].db)。 /// /// GUI 模式通过 Tauri 的 `app.path().app_data_dir()` 获取路径(含 identifier), /// CLI 模式无 app handle,改用编译时注入的 MCP_APP_IDENTIFIER 拼出相同路径。 /// 保证 MCP server 与 GUI 读写同一数据库,无需手动传 --db。 fn default_db_path() -> anyhow::Result { let base = if cfg!(target_os = "windows") { std::env::var("APPDATA").map(PathBuf::from)? } else if cfg!(target_os = "macos") { let home = std::env::var("HOME").map(PathBuf::from)?; home.join("Library/Application Support") } else { let home = std::env::var("HOME").map(PathBuf::from)?; home.join(".local/share") }; // 从编译时环境变量 MCP_APP_IDENTIFIER 读取 Tauri identifier(如 top.1216.devflow), // 与 tauri::path::PathResolver::app_data_dir 行为一致,拼出正确路径。 let identifier = option_env!("MCP_APP_IDENTIFIER").unwrap_or("devflow"); let dir = base.join(identifier); std::fs::create_dir_all(&dir)?; let name = if cfg!(debug_assertions) { "devflow-dev.db" } else { "devflow.db" }; Ok(dir.join(name)) }