新增: MCP Server(df-mcp crate + stdio 协议 + CLI 子命令)
依据 F-260619-02 设计,对外暴露数据层接入 Claude Code/Cursor: - 新建 df-mcp crate:JSON-RPC 2.0 over stdio(initialize/tools/list/tools/call) - 暴露 19 数据层工具(项目/任务/灵感/工作流/回收站),复用 df-storage Repo 零重复 CRUD - 三层安全降级:High 风险(delete/purge/run_workflow)tools/list 不暴露+dispatch 拒+handler 二次拒;Medium 允许+审计日志;--read-only 仅 Low - 不暴露文件系统工具(防绕过应用内路径校验) - Tauri CLI 子命令 devflow mcp-server [--db/--read-only/--print-config] - SQLite WAL 已启用(复用 Database::open),应用与 MCP server 并发写安全 自验: df-mcp 8 passed + workspace EXIT 0
This commit is contained in:
@@ -33,6 +33,7 @@ df-execute = { path = "../crates/df-execute" }
|
||||
df-ai = { path = "../crates/df-ai" }
|
||||
df-ideas = { path = "../crates/df-ideas" }
|
||||
df-project = { path = "../crates/df-project" }
|
||||
df-mcp = { path = "../crates/df-mcp" }
|
||||
futures = "0.3"
|
||||
# write_file base64 编码:写二进制/非 UTF-8 文件(图片/PDF/Excel 等)
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -1,6 +1,133 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
devflow_lib::run()
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
// CLI 子命令:手动解析 argv(避免引入 clap 重依赖)。
|
||||
//
|
||||
// `devflow mcp-server [--db <path>] [--read-only]`
|
||||
// `devflow mcp-server --print-config`
|
||||
//
|
||||
// 仅识别首个非程序名参数是否为 `mcp-server`;其余情况走 GUI。
|
||||
// stdio 句柄由父进程(MCP 客户端)继承,无需额外配置。
|
||||
let args: Vec<String> = 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 <path>`:指定 SQLite 路径(默认:系统 app_data_dir/devflow[-dev].db)
|
||||
/// - `--read-only`:仅暴露 list/get 工具
|
||||
/// - `--print-config`:输出 Claude Code / Cursor 的 MCP 配置 JSON 片段到 stdout 后退出
|
||||
fn run_mcp_server(args: &[String]) -> ExitCode {
|
||||
let mut db_path: Option<PathBuf> = None;
|
||||
let mut read_only = false;
|
||||
let mut print_config = false;
|
||||
|
||||
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,
|
||||
"--print-config" => print_config = true,
|
||||
"-h" | "--help" => {
|
||||
println!("devflow mcp-server [--db <path>] [--read-only] [--print-config]");
|
||||
println!();
|
||||
println!(" --db <path> 指定 SQLite 数据库路径(默认:app_data_dir/devflow.db)");
|
||||
println!(" --read-only 仅暴露 list/get 工具(拒绝 create/update/delete)");
|
||||
println!(" --print-config 输出 MCP 配置 JSON 片段到 stdout 后退出");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
other => {
|
||||
eprintln!("未知参数: {other}(用 --help 查看)");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if print_config {
|
||||
// 输出 Claude Code / Cursor 兼容的 MCP 配置片段(command/args 形态)。
|
||||
// Claude Code: claude mcp add devflow -- devflow mcp-server [--read-only]
|
||||
// Cursor: 在 ~/.cursor/mcp.json 加 mcpServers.devflow
|
||||
let read_only_flag = if read_only { r#","--read-only""# } else { "" };
|
||||
let json = format!(
|
||||
r#"{{"mcpServers":{{"devflow":{{"command":"devflow","args":["mcp-server"{read_only_flag}]}}}}}}"#
|
||||
);
|
||||
println!("{json}");
|
||||
println!();
|
||||
println!("# Claude Code 接入:");
|
||||
println!("# claude mcp add devflow -- devflow mcp-server");
|
||||
println!("# Cursor 接入:把上面 JSON 写入 ~/.cursor/mcp.json");
|
||||
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 <path> 显式指定");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 启动 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, read_only)) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("MCP server 异常退出: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 默认数据库路径:与 GUI 应用同库(系统 app_data_dir/devflow.db 或 dev-dev.db)。
|
||||
///
|
||||
/// 复用与 lib.rs::run 一致的命名(debug 用 devflow-dev.db,release 用 devflow.db),
|
||||
/// 保证 MCP server 与 GUI 读写同一文件。
|
||||
fn default_db_path() -> anyhow::Result<PathBuf> {
|
||||
// tauri 的 app_data_dir 需要 app handle,CLI 无 handle;用 dirs crate 或平台约定降级。
|
||||
// 这里用 std 环境变量推导(不引 dirs):优先 APPDATA/tav(Windows)、HOME( unix)。
|
||||
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")
|
||||
};
|
||||
// Tauri app_data_dir 末段为包名/产品名(devflow)。dev 模式可能为 devflow-dev。
|
||||
let dir = base.join("devflow");
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let name = if cfg!(debug_assertions) {
|
||||
"devflow-dev.db"
|
||||
} else {
|
||||
"devflow.db"
|
||||
};
|
||||
Ok(dir.join(name))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user