新增: 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:
2026-06-19 19:24:02 +08:00
parent e981c1492a
commit f217e39bfe
8 changed files with 1431 additions and 2 deletions

View File

@@ -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"

View File

@@ -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))
}