新增: 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:
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user