- df-mcp 加 streamable HTTP transport 层(axum 0.7 + tower,server_http.rs),lib.rs pub mod 接线 - src-tauri 加 mcp.rs(spawn_mcp_http + mcp_get_status IPC + mcp-server CLI)+ tray.rs(系统托盘 show_main/setup_tray),lib.rs 集成 + main.rs mcp-server 参数路由 - AI 工具加 entity_resolve.rs(实体解析,tools/mod.rs pub mod 接线)
100 lines
4.2 KiB
Rust
100 lines
4.2 KiB
Rust
//! 桌面进程内嵌 HTTP MCP server。
|
|
//!
|
|
//! 复用 AppState.db 与 GUI 共享同一数据库连接,在 tauri 后台任务里常驻监听
|
|
//! 127.0.0.1:<port>/mcp,多 Claude 会话 HTTP 直连同一桌面进程读写 DevFlow 数据。
|
|
//!
|
|
//! 运行时状态(McpStatus)以 `tauri::State<Mutex<McpStatus>>` 管理:
|
|
//! - setup 阶段 manage 初始 `{ running: false, port }`
|
|
//! - spawn 内 bind 成功/失败/异常退出时更新 running
|
|
//! - 前端经 `mcp_get_status` IPC 查询(Mutex 保证跨线程一致)
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use serde::Serialize;
|
|
use tauri::{AppHandle, Manager, State};
|
|
|
|
use df_mcp::server_http::{self, McpHttpState};
|
|
use df_storage::db::Database;
|
|
|
|
/// 前端可查的 MCP 状态。
|
|
#[derive(Clone, Serialize)]
|
|
pub struct McpStatus {
|
|
/// server 是否正在监听(bind 成功为 true;绑定失败/异常退出为 false)
|
|
pub running: bool,
|
|
/// 实际监听端口(env `DEVFLOW_MCP_PORT` 覆盖,默认 18765)
|
|
pub port: u16,
|
|
}
|
|
|
|
/// managed state 别名:跨线程共享的 MCP 运行时状态。
|
|
/// setup 中 `app.manage(Mutex::new(McpStatus { running: false, port }))` 注入,
|
|
/// spawn 内更新、`mcp_get_status` 读取。
|
|
pub type McpStatusState = Mutex<McpStatus>;
|
|
|
|
/// 解析 MCP 监听端口:优先 env `DEVFLOW_MCP_PORT`,否则默认 18765。
|
|
/// 供 setup(spawn 前 manage 初始状态)与 mcp_get_status(兜底)共用,保证一致。
|
|
pub fn mcp_port() -> u16 {
|
|
std::env::var("DEVFLOW_MCP_PORT")
|
|
.ok()
|
|
.and_then(|p| p.parse::<u16>().ok())
|
|
.unwrap_or(server_http::DEFAULT_MCP_PORT)
|
|
}
|
|
|
|
/// 在 tauri 后台任务里启动 HTTP MCP server(复用 AppState.db,与 GUI 共享连接)。
|
|
///
|
|
/// 前置条件:setup 中已 `app.manage(Mutex::new(McpStatus{...}))`(本函数内只更新不注入)。
|
|
/// bind 成功/失败/异常退出都会同步更新 managed 状态,前端 mcp_get_status 可查实时值。
|
|
pub fn spawn_mcp_http(app: &AppHandle, db: Arc<Database>) {
|
|
// 工具调用回调:复用现有 df-data-changed 机制,让 GUI 前端在 MCP 写库后自动刷新。
|
|
// 非变更类工具(如 list/get)在 emit_data_changed 内部映射不到即 noop,零副作用。
|
|
let app_for_cb = app.clone();
|
|
let on_tool_call: Option<Arc<dyn Fn(&str) + Send + Sync>> = Some(Arc::new(move |tool_name: &str| {
|
|
crate::commands::ai::audit::emit_data_changed(&app_for_cb, tool_name);
|
|
}));
|
|
let state = McpHttpState::new(db, false, on_tool_call);
|
|
let port = mcp_port();
|
|
// 后台 task 持有 app clone,用于 bind 结果写回 managed 状态
|
|
let app_for_status = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let listener = match tokio::net::TcpListener::bind(("127.0.0.1", port)).await {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
tracing::error!(port, err = %e, "[mcp-http] 绑定端口失败, MCP server 不可用(可设 DEVFLOW_MCP_PORT 换端口)");
|
|
set_mcp_running(&app_for_status, false);
|
|
return;
|
|
}
|
|
};
|
|
// 绑定成功,更新状态为 running(供前端查询)
|
|
set_mcp_running(&app_for_status, true);
|
|
tracing::info!(port, "[mcp-http] MCP server listening on 127.0.0.1:{port}/mcp");
|
|
if let Err(e) = server_http::serve_on(listener, state).await {
|
|
tracing::error!(err = %e, "[mcp-http] server 异常退出");
|
|
set_mcp_running(&app_for_status, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 更新 managed McpStatus.running。state 未注入/try_state 失败/锁中毒时静默跳过
|
|
/// (状态保持原值,非致命,防 panic 拖垮后台任务)。
|
|
fn set_mcp_running(app: &AppHandle, running: bool) {
|
|
if let Some(state) = app.try_state::<McpStatusState>() {
|
|
if let Ok(mut guard) = state.lock() {
|
|
guard.running = running;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// IPC command:前端查询 MCP server 状态。
|
|
#[tauri::command]
|
|
pub fn mcp_get_status(state: State<'_, McpStatusState>) -> McpStatus {
|
|
match state.lock() {
|
|
Ok(guard) => McpStatus {
|
|
running: guard.running,
|
|
port: guard.port,
|
|
},
|
|
// 锁中毒(理论上不应发生):降级返回静态配置,防 panic
|
|
Err(_) => McpStatus {
|
|
running: false,
|
|
port: mcp_port(),
|
|
},
|
|
}
|
|
}
|