From 7ae2a9f55625cd2d0155c893c47dfe76984fa541 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 14 Nov 2025 12:57:14 +0800 Subject: [PATCH] fix(mcp): resolve compilation errors and add backward compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Compilation Fixes - Add deprecated compatibility methods to McpService: * get_servers() - filters servers by app * set_enabled() - delegates to toggle_app() * sync_enabled() - syncs enabled servers for specific app * import_from_claude/codex/gemini() - wraps mcp:: functions - Fix toml_edit type conversion in sync_single_server_to_codex(): * Add json_server_to_toml_table() helper function * Manually construct toml_edit::Table instead of invalid serde conversion - Fix get_codex_config_path() calls (returns PathBuf, not Result) - Update upsert_mcp_server_in_config() to work with unified structure: * Converts old per-app API to unified McpServer structure * Preserves existing server data when updating * Supports sync_other_side parameter for multi-app enable - Update delete_mcp_server_in_config() to ignore app parameter ## Backward Compatibility - All old v3.6.x commands continue to work with deprecation warnings - Frontend migration can be done incrementally - Old commands transparently use new unified backend ## Status ✅ Backend compiles successfully (cargo check passes) ⚠️ 16 warnings (8 deprecation + 8 unused functions - expected) --- src-tauri/src/commands/mcp.rs | 58 ++++++++++++++++++++++++-- src-tauri/src/mcp.rs | 76 +++++++++++++++++++++++++++++++--- src-tauri/src/services/mcp.rs | 78 +++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index d943437..35b2555 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -66,6 +66,7 @@ pub async fn get_mcp_config( } /// 在 config.json 中新增或更新一个 MCP 服务器定义 +/// [已废弃] 该命令仍然使用旧的分应用API,会转换为统一结构 #[tauri::command] pub async fn upsert_mcp_server_in_config( state: State<'_, AppState>, @@ -74,8 +75,58 @@ pub async fn upsert_mcp_server_in_config( spec: serde_json::Value, sync_other_side: Option, ) -> Result { + use crate::app_config::McpServer; + let app_ty = AppType::from_str(&app).map_err(|e| e.to_string())?; - McpService::upsert_server(&state, app_ty, &id, spec, sync_other_side.unwrap_or(false)) + + // 读取现有的服务器(如果存在) + let existing_server = { + let cfg = state.config.read().map_err(|e| e.to_string())?; + if let Some(servers) = &cfg.mcp.servers { + servers.get(&id).cloned() + } else { + None + } + }; + + // 构建新的统一服务器结构 + let mut new_server = if let Some(mut existing) = existing_server { + // 更新现有服务器 + existing.server = spec.clone(); + existing.apps.set_enabled_for(&app_ty, true); + existing + } else { + // 创建新服务器 + let mut apps = crate::app_config::McpApps::default(); + apps.set_enabled_for(&app_ty, true); + + // 尝试从 spec 中提取 name,否则使用 id + let name = spec.get("name") + .and_then(|v| v.as_str()) + .unwrap_or(&id) + .to_string(); + + McpServer { + id: id.clone(), + name, + server: spec, + apps, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + } + }; + + // 如果 sync_other_side 为 true,也启用其他应用 + if sync_other_side.unwrap_or(false) { + new_server.apps.claude = true; + new_server.apps.codex = true; + new_server.apps.gemini = true; + } + + McpService::upsert_server(&state, new_server) + .map(|_| true) .map_err(|e| e.to_string()) } @@ -83,11 +134,10 @@ pub async fn upsert_mcp_server_in_config( #[tauri::command] pub async fn delete_mcp_server_in_config( state: State<'_, AppState>, - app: String, + _app: String, // 参数保留用于向后兼容,但在统一结构中不再需要 id: String, ) -> Result { - let app_ty = AppType::from_str(&app).map_err(|e| e.to_string())?; - McpService::delete_server(&state, app_ty, &id).map_err(|e| e.to_string()) + McpService::delete_server(&state, &id).map_err(|e| e.to_string()) } /// 设置启用状态并同步到客户端配置 diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 050c05b..38589b5 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -825,14 +825,79 @@ pub fn remove_server_from_claude(id: &str) -> Result<(), AppError> { crate::claude_mcp::set_mcp_servers_map(¤t) } +/// Helper: 将 JSON MCP 服务器规范转换为 toml_edit::Table +fn json_server_to_toml_table(spec: &Value) -> Result { + use toml_edit::{Array, Item, Table}; + + let mut t = Table::new(); + let typ = spec.get("type").and_then(|v| v.as_str()).unwrap_or("stdio"); + t["type"] = toml_edit::value(typ); + + match typ { + "stdio" => { + let cmd = spec.get("command").and_then(|v| v.as_str()).unwrap_or(""); + t["command"] = toml_edit::value(cmd); + + if let Some(args) = spec.get("args").and_then(|v| v.as_array()) { + let mut arr_v = Array::default(); + for a in args.iter().filter_map(|x| x.as_str()) { + arr_v.push(a); + } + if !arr_v.is_empty() { + t["args"] = Item::Value(toml_edit::Value::Array(arr_v)); + } + } + + if let Some(cwd) = spec.get("cwd").and_then(|v| v.as_str()) { + if !cwd.trim().is_empty() { + t["cwd"] = toml_edit::value(cwd); + } + } + + if let Some(env) = spec.get("env").and_then(|v| v.as_object()) { + let mut env_tbl = Table::new(); + for (k, v) in env.iter() { + if let Some(s) = v.as_str() { + env_tbl[&k[..]] = toml_edit::value(s); + } + } + if !env_tbl.is_empty() { + t["env"] = Item::Table(env_tbl); + } + } + } + "http" => { + let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or(""); + t["url"] = toml_edit::value(url); + + if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) { + let mut h_tbl = Table::new(); + for (k, v) in headers.iter() { + if let Some(s) = v.as_str() { + h_tbl[&k[..]] = toml_edit::value(s); + } + } + if !h_tbl.is_empty() { + t["headers"] = Item::Table(h_tbl); + } + } + } + _ => {} + } + + Ok(t) +} + /// 将单个 MCP 服务器同步到 Codex live 配置 pub fn sync_single_server_to_codex( _config: &MultiAppConfig, id: &str, server_spec: &Value, ) -> Result<(), AppError> { + use toml_edit::Item; + // 读取现有的 config.toml - let config_path = crate::codex_config::get_codex_config_path()?; + let config_path = crate::codex_config::get_codex_config_path(); let mut doc = if config_path.exists() { let content = std::fs::read_to_string(&config_path) @@ -858,11 +923,10 @@ pub fn sync_single_server_to_codex( doc["mcp"]["servers"] = toml_edit::table(); } - // 将服务器转换为 TOML 格式并插入 - let toml_value = serde_json::from_value::(server_spec.clone()) - .map_err(|e| AppError::McpValidation(format!("无法将 MCP 服务器转换为 TOML: {e}")))?; + // 将 JSON 服务器规范转换为 TOML 表 + let toml_table = json_server_to_toml_table(server_spec)?; - doc["mcp"]["servers"][id] = toml_edit::value(toml_value); + doc["mcp"]["servers"][id] = Item::Table(toml_table); // 写回文件 std::fs::write(&config_path, doc.to_string()) @@ -873,7 +937,7 @@ pub fn sync_single_server_to_codex( /// 从 Codex live 配置中移除单个 MCP 服务器 pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> { - let config_path = crate::codex_config::get_codex_config_path()?; + let config_path = crate::codex_config::get_codex_config_path(); if !config_path.exists() { return Ok(()); // 文件不存在,无需删除 diff --git a/src-tauri/src/services/mcp.rs b/src-tauri/src/services/mcp.rs index 112e059..ffd52ec 100644 --- a/src-tauri/src/services/mcp.rs +++ b/src-tauri/src/services/mcp.rs @@ -186,4 +186,82 @@ impl McpService { Ok(()) } + + // ======================================================================== + // 兼容层:支持旧的 v3.6.x 命令(已废弃,将在 v4.0 移除) + // ======================================================================== + + /// [已废弃] 获取指定应用的 MCP 服务器(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Use get_all_servers instead")] + pub fn get_servers( + state: &AppState, + app: AppType, + ) -> Result, AppError> { + let all_servers = Self::get_all_servers(state)?; + let mut result = HashMap::new(); + + for (id, server) in all_servers { + if server.apps.is_enabled_for(&app) { + result.insert(id, server.server); + } + } + + Ok(result) + } + + /// [已废弃] 设置 MCP 服务器在指定应用的启用状态(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Use toggle_app instead")] + pub fn set_enabled( + state: &AppState, + app: AppType, + id: &str, + enabled: bool, + ) -> Result { + Self::toggle_app(state, id, app, enabled)?; + Ok(true) + } + + /// [已废弃] 同步启用的 MCP 到指定应用(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Use sync_all_enabled instead")] + pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> { + let servers = Self::get_all_servers(state)?; + + for server in servers.values() { + if server.apps.is_enabled_for(&app) { + Self::sync_server_to_app(state, server, &app)?; + } + } + + Ok(()) + } + + /// [已废弃] 从 Claude 导入 MCP(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Import will be handled differently in unified structure")] + pub fn import_from_claude(state: &AppState) -> Result { + let mut cfg = state.config.write()?; + let count = mcp::import_from_claude(&mut cfg)?; + drop(cfg); + state.save()?; + Ok(count) + } + + /// [已废弃] 从 Codex 导入 MCP(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Import will be handled differently in unified structure")] + pub fn import_from_codex(state: &AppState) -> Result { + let mut cfg = state.config.write()?; + let count = mcp::import_from_codex(&mut cfg)?; + drop(cfg); + state.save()?; + Ok(count) + } + + /// [已废弃] 从 Gemini 导入 MCP(兼容旧 API) + #[deprecated(since = "3.7.0", note = "Import will be handled differently in unified structure")] + pub fn import_from_gemini(state: &AppState) -> Result { + let mut cfg = state.config.write()?; + let count = mcp::import_from_gemini(&mut cfg)?; + drop(cfg); + state.save()?; + Ok(count) + } }