新增: MCP HTTP transport + 系统托盘 + 实体解析(会话前基线收尾)

- 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 接线)
This commit is contained in:
lxy
2026-08-05 22:14:27 +08:00
parent c480627ba6
commit a91e950874
16 changed files with 2238 additions and 94 deletions
+25 -7
View File
@@ -23,9 +23,9 @@ use crate::protocol::{
use crate::tools::{self, Ctx, RiskLevel};
/// 协议版本(MCP 2025-06-18)
const PROTOCOL_VERSION: &str = "2025-06-18";
const SERVER_NAME: &str = "devflow-mcp";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const PROTOCOL_VERSION: &str = "2025-06-18";
pub(crate) const SERVER_NAME: &str = "devflow-mcp";
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// 启动 MCP server。
///
@@ -79,7 +79,7 @@ where
Err(e) => {
// 解析失败:无 id 时无法回响应,只能 log;有 id(尽力猜)回 PARSE_ERROR
tracing::warn!(target: "df_mcp", line = %trimmed, err = %e, "解析 JSON-RPC 行失败");
let resp = Response::err(None, crate::protocol::PARSE_ERROR, "Parse error", None);
let resp = Response::err(Some(Value::Null), crate::protocol::PARSE_ERROR, "Parse error", None);
write_response(&mut writer, &resp).await?;
continue;
}
@@ -111,7 +111,8 @@ where
/// 方法分发 → 构造 Response。
///
/// `id`:JSON-RPC 请求 id(回响应时原样回填;通知由 main_loop 已过滤)。
async fn dispatch(ctx: &Ctx, read_only: bool, id: Option<Value>, method: McpMethod) -> Response {
/// `pub(crate)`:stdio(main_loop)与 HTTP(server_http)transport 共用。
pub(crate) async fn dispatch(ctx: &Ctx, read_only: bool, id: Option<Value>, method: McpMethod) -> Response {
match method {
McpMethod::Initialize { .. } => {
let result = InitializeResult {
@@ -135,9 +136,10 @@ async fn dispatch(ctx: &Ctx, read_only: bool, id: Option<Value>, method: McpMeth
}
McpMethod::ToolsList => {
let tools: Vec<_> = tools::all_tools()
.into_iter()
.iter()
.filter(|t| visible(read_only, t.risk))
.map(|t| serde_json::to_value(&t.tool).unwrap_or(Value::Null))
.filter(|v| !v.is_null())
.collect();
Response::ok(id, json!({ "tools": tools }))
}
@@ -180,7 +182,7 @@ async fn dispatch(ctx: &Ctx, read_only: bool, id: Option<Value>, method: McpMeth
}
/// 工具可见性:read-only 仅 Low,否则 Low + Medium(High 永不可见)
fn visible(read_only: bool, risk: RiskLevel) -> bool {
pub(crate) fn visible(read_only: bool, risk: RiskLevel) -> bool {
if read_only {
risk == RiskLevel::Low
} else {
@@ -287,6 +289,22 @@ mod tests {
assert_eq!(v["error"]["code"], crate::protocol::METHOD_NOT_FOUND);
}
#[tokio::test]
async fn tools_call_missing_name_returns_clear_error() {
// tools/call 缺 name 参数:不应回「未知工具: 」(空名),应回明确 METHOD_NOT_FOUND
let input =
r#"{"jsonrpc":"2.0","id":41,"method":"tools/call","params":{"arguments":{}}}"#;
let out = run_io_lines(&[input], false).await;
let v: Value = serde_json::from_str(&out[0]).unwrap();
assert_eq!(v["error"]["code"], crate::protocol::METHOD_NOT_FOUND);
let msg = v["error"]["message"].as_str().unwrap();
assert!(
msg.contains("name"),
"空 name 应给出明确提示,实际: {msg}"
);
assert!(!msg.contains("未知工具: "), "不应是空名「未知工具: 」: {msg}");
}
#[tokio::test]
async fn tools_call_high_risk_is_rejected() {
let input =