From c985db8f3d80b405773fdb01d981338b51067448 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 14 Nov 2025 12:51:24 +0800 Subject: [PATCH] feat(mcp): implement unified MCP management for v3.7.0 BREAKING CHANGE: Migrate from app-specific MCP storage to unified structure ## Phase 1: Data Structure Migration - Add McpApps struct with claude/codex/gemini boolean fields - Add McpServer struct for unified server definition - Add migration logic in MultiAppConfig::migrate_mcp_to_unified() - Integrate automatic migration into MultiAppConfig::load() - Support backward compatibility through Optional fields ## Phase 2: Backend Services Refactor - Completely rewrite services/mcp.rs for unified management: * get_all_servers() - retrieve all MCP servers * upsert_server() - add/update unified server * delete_server() - remove server * toggle_app() - enable/disable server for specific app * sync_all_enabled() - sync to all live configs - Add single-server sync functions to mcp.rs: * sync_single_server_to_claude/codex/gemini() * remove_server_from_claude/codex/gemini() - Add read_mcp_servers_map() to claude_mcp.rs and gemini_mcp.rs - Add new Tauri commands to commands/mcp.rs: * get_mcp_servers, upsert_mcp_server, delete_mcp_server * toggle_mcp_app, sync_all_mcp_servers - Register new commands in lib.rs ## Migration Strategy - Detects old structure (mcp.claude/codex/gemini.servers) - Merges into unified mcp.servers with apps markers - Handles conflicts by merging enabled apps - Clears old structures after migration - Saves migrated config automatically ## Known Issues - Old commands still need compatibility layer (WIP) - toml_edit type conversion needs fixing (WIP) - Frontend not yet implemented (Phase 3 pending) Related: v3.6.2 -> v3.7.0 --- src-tauri/src/app_config.rs | 230 +++++++++++++++++++++++++- src-tauri/src/claude_mcp.rs | 21 +++ src-tauri/src/commands/mcp.rs | 50 ++++++ src-tauri/src/gemini_mcp.rs | 21 +++ src-tauri/src/lib.rs | 14 +- src-tauri/src/mcp.rs | 137 ++++++++++++++++ src-tauri/src/services/mcp.rs | 294 ++++++++++++++++------------------ 7 files changed, 599 insertions(+), 168 deletions(-) diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 0bea690..2c648ce 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -2,7 +2,75 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; -/// MCP 配置:单客户端维度(claude 或 codex 下的一组服务器) +/// MCP 服务器应用状态(标记应用到哪些客户端) +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct McpApps { + #[serde(default)] + pub claude: bool, + #[serde(default)] + pub codex: bool, + #[serde(default)] + pub gemini: bool, +} + +impl McpApps { + /// 检查指定应用是否启用 + pub fn is_enabled_for(&self, app: &AppType) -> bool { + match app { + AppType::Claude => self.claude, + AppType::Codex => self.codex, + AppType::Gemini => self.gemini, + } + } + + /// 设置指定应用的启用状态 + pub fn set_enabled_for(&mut self, app: &AppType, enabled: bool) { + match app { + AppType::Claude => self.claude = enabled, + AppType::Codex => self.codex = enabled, + AppType::Gemini => self.gemini = enabled, + } + } + + /// 获取所有启用的应用列表 + pub fn enabled_apps(&self) -> Vec { + let mut apps = Vec::new(); + if self.claude { + apps.push(AppType::Claude); + } + if self.codex { + apps.push(AppType::Codex); + } + if self.gemini { + apps.push(AppType::Gemini); + } + apps + } + + /// 检查是否所有应用都未启用 + pub fn is_empty(&self) -> bool { + !self.claude && !self.codex && !self.gemini + } +} + +/// MCP 服务器定义(v3.7.0 统一结构) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpServer { + pub id: String, + pub name: String, + pub server: serde_json::Value, + pub apps: McpApps, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub homepage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, +} + +/// MCP 配置:单客户端维度(v3.6.x 及以前,保留用于向后兼容) #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct McpConfig { /// 以 id 为键的服务器定义(宽松 JSON 对象,包含 enabled/source 等 UI 辅助字段) @@ -10,15 +78,27 @@ pub struct McpConfig { pub servers: HashMap, } -/// MCP 根:按客户端分开维护(无历史兼容压力,直接以 v2 结构落地) +impl McpConfig { + /// 检查配置是否为空 + pub fn is_empty(&self) -> bool { + self.servers.is_empty() + } +} + +/// MCP 根配置(v3.7.0 新旧结构并存) #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct McpRoot { - #[serde(default)] + /// 统一的 MCP 服务器存储(v3.7.0+) + #[serde(skip_serializing_if = "Option::is_none")] + pub servers: Option>, + + /// 旧的分应用存储(v3.6.x 及以前,保留用于迁移) + #[serde(default, skip_serializing_if = "McpConfig::is_empty")] pub claude: McpConfig, - #[serde(default)] + #[serde(default, skip_serializing_if = "McpConfig::is_empty")] pub codex: McpConfig, - #[serde(default)] - pub gemini: McpConfig, // Gemini MCP 配置(预留) + #[serde(default, skip_serializing_if = "McpConfig::is_empty")] + pub gemini: McpConfig, } /// Prompt 配置:单客户端维度 @@ -169,6 +249,13 @@ impl MultiAppConfig { .insert("gemini".to_string(), ProviderManager::default()); } + // 执行 MCP 迁移(v3.6.x → v3.7.0) + let migrated = config.migrate_mcp_to_unified()?; + if migrated { + log::info!("MCP 配置已迁移到 v3.7.0 统一结构,保存配置..."); + config.save()?; + } + Ok(config) } @@ -296,6 +383,137 @@ impl MultiAppConfig { log::info!("自动导入完成: {}", app.as_str()); Ok(()) } + + /// 将 v3.6.x 的分应用 MCP 结构迁移到 v3.7.0 的统一结构 + /// + /// 迁移策略: + /// 1. 检查是否已经迁移(mcp.servers 是否存在) + /// 2. 收集所有应用的 MCP,按 ID 去重合并 + /// 3. 生成统一的 McpServer 结构,标记应用到哪些客户端 + /// 4. 清空旧的分应用配置 + pub fn migrate_mcp_to_unified(&mut self) -> Result { + // 检查是否已经是新结构 + if self.mcp.servers.is_some() { + log::debug!("MCP 配置已是统一结构,跳过迁移"); + return Ok(false); + } + + log::info!("检测到旧版 MCP 配置格式,开始迁移到 v3.7.0 统一结构..."); + + let mut unified_servers: HashMap = HashMap::new(); + let mut conflicts = Vec::new(); + + // 收集所有应用的 MCP + for app in [AppType::Claude, AppType::Codex, AppType::Gemini] { + let old_servers = match app { + AppType::Claude => &self.mcp.claude.servers, + AppType::Codex => &self.mcp.codex.servers, + AppType::Gemini => &self.mcp.gemini.servers, + }; + + for (id, entry) in old_servers { + let enabled = entry + .get("enabled") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + if let Some(existing) = unified_servers.get_mut(id) { + // 该 ID 已存在,合并 apps 字段 + existing.apps.set_enabled_for(&app, enabled); + + // 检测配置冲突(同 ID 但配置不同) + if existing.server != *entry.get("server").unwrap_or(&serde_json::json!({})) { + conflicts.push(format!( + "MCP '{id}' 在 {} 和之前的应用中配置不同,将使用首次遇到的配置", + app.as_str() + )); + } + } else { + // 首次遇到该 MCP,创建新条目 + let name = entry + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(id) + .to_string(); + + let server = entry + .get("server") + .cloned() + .unwrap_or(serde_json::json!({})); + + let description = entry + .get("description") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let homepage = entry + .get("homepage") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let docs = entry + .get("docs") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let tags = entry + .get("tags") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + + let mut apps = McpApps::default(); + apps.set_enabled_for(&app, enabled); + + unified_servers.insert( + id.clone(), + McpServer { + id: id.clone(), + name, + server, + apps, + description, + homepage, + docs, + tags, + }, + ); + } + } + } + + // 记录冲突警告 + if !conflicts.is_empty() { + log::warn!("MCP 迁移过程中检测到配置冲突:"); + for conflict in &conflicts { + log::warn!(" - {conflict}"); + } + } + + log::info!( + "MCP 迁移完成,共迁移 {} 个服务器{}", + unified_servers.len(), + if !conflicts.is_empty() { + format!("(存在 {} 个冲突)", conflicts.len()) + } else { + String::new() + } + ); + + // 替换为新结构 + self.mcp.servers = Some(unified_servers); + + // 清空旧的分应用配置 + self.mcp.claude = McpConfig::default(); + self.mcp.codex = McpConfig::default(); + self.mcp.gemini = McpConfig::default(); + + Ok(true) + } } #[cfg(test)] diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index 54cc6f4..12c079f 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -231,6 +231,27 @@ pub fn validate_command_in_path(cmd: &str) -> Result { Ok(false) } +/// 读取 ~/.claude.json 中的 mcpServers 映射 +pub fn read_mcp_servers_map() -> Result, AppError> { + let path = user_config_path(); + if !path.exists() { + return Ok(std::collections::HashMap::new()); + } + + let root = read_json_value(&path)?; + let servers = root + .get("mcpServers") + .and_then(|v| v.as_object()) + .map(|obj| { + obj.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + }) + .unwrap_or_default(); + + Ok(servers) +} + /// 将给定的启用 MCP 服务器映射写入到用户级 ~/.claude.json 的 mcpServers 字段 /// 仅覆盖 mcpServers,其他字段保持不变 pub fn set_mcp_servers_map( diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 61e21f7..d943437 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -143,3 +143,53 @@ pub async fn sync_enabled_mcp_to_gemini(state: State<'_, AppState>) -> Result) -> Result { McpService::import_from_gemini(&state).map_err(|e| e.to_string()) } + +// ============================================================================ +// v3.7.0 新增:统一 MCP 管理命令 +// ============================================================================ + +use crate::app_config::McpServer; + +/// 获取所有 MCP 服务器(统一结构) +#[tauri::command] +pub async fn get_mcp_servers( + state: State<'_, AppState>, +) -> Result, String> { + McpService::get_all_servers(&state).map_err(|e| e.to_string()) +} + +/// 添加或更新 MCP 服务器 +#[tauri::command] +pub async fn upsert_mcp_server( + state: State<'_, AppState>, + server: McpServer, +) -> Result<(), String> { + McpService::upsert_server(&state, server).map_err(|e| e.to_string()) +} + +/// 删除 MCP 服务器 +#[tauri::command] +pub async fn delete_mcp_server( + state: State<'_, AppState>, + id: String, +) -> Result { + McpService::delete_server(&state, &id).map_err(|e| e.to_string()) +} + +/// 切换 MCP 服务器在指定应用的启用状态 +#[tauri::command] +pub async fn toggle_mcp_app( + state: State<'_, AppState>, + server_id: String, + app: String, + enabled: bool, +) -> Result<(), String> { + let app_ty = AppType::from_str(&app).map_err(|e| e.to_string())?; + McpService::toggle_app(&state, &server_id, app_ty, enabled).map_err(|e| e.to_string()) +} + +/// 手动同步所有启用的 MCP 服务器到对应的应用 +#[tauri::command] +pub async fn sync_all_mcp_servers(state: State<'_, AppState>) -> Result<(), String> { + McpService::sync_all_enabled(&state).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/gemini_mcp.rs b/src-tauri/src/gemini_mcp.rs index 4a0fd67..c9427be 100644 --- a/src-tauri/src/gemini_mcp.rs +++ b/src-tauri/src/gemini_mcp.rs @@ -157,6 +157,27 @@ pub fn delete_mcp_server(id: &str) -> Result { Ok(true) } +/// 读取 Gemini settings.json 中的 mcpServers 映射 +pub fn read_mcp_servers_map() -> Result, AppError> { + let path = user_config_path(); + if !path.exists() { + return Ok(std::collections::HashMap::new()); + } + + let root = read_json_value(&path)?; + let servers = root + .get("mcpServers") + .and_then(|v| v.as_object()) + .map(|obj| { + obj.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + }) + .unwrap_or_default(); + + Ok(servers) +} + /// 将给定的启用 MCP 服务器映射写入到 Gemini settings.json 的 mcpServers 字段 /// 仅覆盖 mcpServers,其他字段保持不变 pub fn set_mcp_servers_map( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4b9b477..54cbd61 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,14 +18,16 @@ mod settings; mod store; mod usage_script; -pub use app_config::{AppType, MultiAppConfig}; +pub use app_config::{AppType, McpServer, MultiAppConfig}; pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic}; pub use commands::*; pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file}; pub use error::AppError; pub use mcp::{ - import_from_claude, import_from_codex, import_from_gemini, sync_enabled_to_claude, - sync_enabled_to_codex, sync_enabled_to_gemini, + import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude, + remove_server_from_codex, remove_server_from_gemini, sync_enabled_to_claude, + sync_enabled_to_codex, sync_enabled_to_gemini, sync_single_server_to_claude, + sync_single_server_to_codex, sync_single_server_to_gemini, }; pub use provider::{Provider, ProviderMeta}; pub use services::{ @@ -545,6 +547,12 @@ pub fn run() { commands::import_mcp_from_claude, commands::import_mcp_from_codex, commands::import_mcp_from_gemini, + // v3.7.0: Unified MCP management + commands::get_mcp_servers, + commands::upsert_mcp_server, + commands::delete_mcp_server, + commands::toggle_mcp_app, + commands::sync_all_mcp_servers, // Prompt management commands::get_prompts, commands::upsert_prompt, diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index fb33879..050c05b 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -791,3 +791,140 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Result<(), AppError> { + // 读取现有的 MCP 配置 + let current = crate::claude_mcp::read_mcp_servers_map()?; + + // 创建新的 HashMap,包含现有的所有服务器 + 当前要同步的服务器 + let mut updated = current; + updated.insert(id.to_string(), server_spec.clone()); + + // 写回 + crate::claude_mcp::set_mcp_servers_map(&updated) +} + +/// 从 Claude live 配置中移除单个 MCP 服务器 +pub fn remove_server_from_claude(id: &str) -> Result<(), AppError> { + // 读取现有的 MCP 配置 + let mut current = crate::claude_mcp::read_mcp_servers_map()?; + + // 移除指定服务器 + current.remove(id); + + // 写回 + crate::claude_mcp::set_mcp_servers_map(¤t) +} + +/// 将单个 MCP 服务器同步到 Codex live 配置 +pub fn sync_single_server_to_codex( + _config: &MultiAppConfig, + id: &str, + server_spec: &Value, +) -> Result<(), AppError> { + // 读取现有的 config.toml + 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) + .map_err(|e| AppError::io(&config_path, e))?; + content + .parse::() + .map_err(|e| AppError::McpValidation(format!("解析 Codex config.toml 失败: {e}")))? + } else { + toml_edit::DocumentMut::new() + }; + + // 确保 [mcp] 表存在 + if !doc.contains_key("mcp") { + doc["mcp"] = toml_edit::table(); + } + + // 确保 [mcp.servers] 子表存在 + if !doc["mcp"] + .as_table() + .and_then(|t| t.get("servers")) + .is_some() + { + 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}")))?; + + doc["mcp"]["servers"][id] = toml_edit::value(toml_value); + + // 写回文件 + std::fs::write(&config_path, doc.to_string()) + .map_err(|e| AppError::io(&config_path, e))?; + + Ok(()) +} + +/// 从 Codex live 配置中移除单个 MCP 服务器 +pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> { + let config_path = crate::codex_config::get_codex_config_path()?; + + if !config_path.exists() { + return Ok(()); // 文件不存在,无需删除 + } + + let content = std::fs::read_to_string(&config_path) + .map_err(|e| AppError::io(&config_path, e))?; + + let mut doc = content + .parse::() + .map_err(|e| AppError::McpValidation(format!("解析 Codex config.toml 失败: {e}")))?; + + // 从 [mcp.servers] 中删除 + if let Some(mcp_table) = doc.get_mut("mcp").and_then(|t| t.as_table_mut()) { + if let Some(servers) = mcp_table.get_mut("servers").and_then(|s| s.as_table_mut()) { + servers.remove(id); + } + } + + // 写回文件 + std::fs::write(&config_path, doc.to_string()) + .map_err(|e| AppError::io(&config_path, e))?; + + Ok(()) +} + +/// 将单个 MCP 服务器同步到 Gemini live 配置 +pub fn sync_single_server_to_gemini( + _config: &MultiAppConfig, + id: &str, + server_spec: &Value, +) -> Result<(), AppError> { + // 读取现有的 MCP 配置 + let current = crate::gemini_mcp::read_mcp_servers_map()?; + + // 创建新的 HashMap,包含现有的所有服务器 + 当前要同步的服务器 + let mut updated = current; + updated.insert(id.to_string(), server_spec.clone()); + + // 写回 + crate::gemini_mcp::set_mcp_servers_map(&updated) +} + +/// 从 Gemini live 配置中移除单个 MCP 服务器 +pub fn remove_server_from_gemini(id: &str) -> Result<(), AppError> { + // 读取现有的 MCP 配置 + let mut current = crate::gemini_mcp::read_mcp_servers_map()?; + + // 移除指定服务器 + current.remove(id); + + // 写回 + crate::gemini_mcp::set_mcp_servers_map(¤t) +} diff --git a/src-tauri/src/services/mcp.rs b/src-tauri/src/services/mcp.rs index 07c6e10..112e059 100644 --- a/src-tauri/src/services/mcp.rs +++ b/src-tauri/src/services/mcp.rs @@ -1,213 +1,189 @@ use std::collections::HashMap; -use serde_json::Value; - -use crate::app_config::{AppType, MultiAppConfig}; +use crate::app_config::{AppType, McpServer, MultiAppConfig}; use crate::error::AppError; use crate::mcp; use crate::store::AppState; -/// MCP 相关业务逻辑 +/// MCP 相关业务逻辑(v3.7.0 统一结构) pub struct McpService; impl McpService { - /// 获取指定应用的 MCP 服务器快照,并在必要时回写归一化后的配置。 - pub fn get_servers(state: &AppState, app: AppType) -> Result, AppError> { - let mut cfg = state.config.write()?; - let (snapshot, normalized) = mcp::get_servers_snapshot_for(&mut cfg, &app); - drop(cfg); - if normalized > 0 { - state.save()?; + /// 获取所有 MCP 服务器(统一结构) + pub fn get_all_servers(state: &AppState) -> Result, AppError> { + let cfg = state.config.read()?; + + // 如果是新结构,直接返回 + if let Some(servers) = &cfg.mcp.servers { + return Ok(servers.clone()); } - Ok(snapshot) + + // 理论上不应该走到这里,因为 load 时会自动迁移 + Err(AppError::localized( + "mcp.old_structure", + "检测到旧版 MCP 结构,请重启应用完成迁移", + "Old MCP structure detected, please restart app to complete migration", + )) } - /// 在 config.json 中新增或更新指定 MCP 服务器,并按需同步到对应客户端。 - pub fn upsert_server( - state: &AppState, - app: AppType, - id: &str, - spec: Value, - sync_other_side: bool, - ) -> Result { - let (changed, snapshot, sync_claude, sync_codex, sync_gemini): ( - bool, - Option, - bool, - bool, - bool, - ) = { + /// 添加或更新 MCP 服务器 + pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> { + { let mut cfg = state.config.write()?; - let changed = mcp::upsert_in_config_for(&mut cfg, &app, id, spec)?; - // 修复:默认启用(unwrap_or(true)) - // 新增的 MCP 如果缺少 enabled 字段,应该默认为启用状态 - let enabled = cfg - .mcp_for(&app) - .servers - .get(id) - .and_then(|entry| entry.get("enabled")) - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - let mut sync_claude = matches!(app, AppType::Claude) && enabled; - let mut sync_codex = matches!(app, AppType::Codex) && enabled; - let mut sync_gemini = matches!(app, AppType::Gemini) && enabled; - - // 修复:sync_other_side=true 时,先将 MCP 复制到另一侧,然后强制同步 - // 这才是"同步到另一侧"的正确语义:将 MCP 跨应用复制 - if sync_other_side && app != AppType::Gemini { - // Gemini 暂不支持跨应用复制,直接跳过 - // 获取当前 MCP 条目的克隆(刚刚插入的,不可能失败) - let current_entry = cfg - .mcp_for(&app) - .servers - .get(id) - .cloned() - .expect("刚刚插入的 MCP 条目必定存在"); - - // 将该 MCP 复制到另一侧的 servers - let other_app = match app { - AppType::Claude => AppType::Codex, - AppType::Codex => AppType::Claude, - AppType::Gemini => unreachable!("Gemini 已在外层 if 中跳过"), - }; - - cfg.mcp_for_mut(&other_app) - .servers - .insert(id.to_string(), current_entry); - - // 强制同步另一侧 - match app { - AppType::Claude => sync_codex = true, - AppType::Codex => sync_claude = true, - AppType::Gemini => unreachable!("Gemini 已在外层 if 中跳过"), - } + // 确保 servers 字段存在 + if cfg.mcp.servers.is_none() { + cfg.mcp.servers = Some(HashMap::new()); } - let snapshot = if sync_claude || sync_codex || sync_gemini { - Some(cfg.clone()) - } else { - None - }; + let servers = cfg.mcp.servers.as_mut().unwrap(); + let id = server.id.clone(); - (changed, snapshot, sync_claude, sync_codex, sync_gemini) - }; + // 插入或更新 + servers.insert(id, server.clone()); + } - // 保持原有行为:始终尝试持久化,避免遗漏 normalize 带来的隐式变更 state.save()?; - if let Some(snapshot) = snapshot { - if sync_claude { - mcp::sync_enabled_to_claude(&snapshot)?; - } - if sync_codex { - mcp::sync_enabled_to_codex(&snapshot)?; - } - if sync_gemini { - mcp::sync_enabled_to_gemini(&snapshot)?; - } - } + // 同步到各个启用的应用 + Self::sync_server_to_apps(state, &server)?; - Ok(changed) + Ok(()) } - /// 删除 config.json 中的 MCP 服务器条目,并同步客户端配置。 - pub fn delete_server(state: &AppState, app: AppType, id: &str) -> Result { - let (existed, snapshot): (bool, Option) = { + /// 删除 MCP 服务器 + pub fn delete_server(state: &AppState, id: &str) -> Result { + let server = { let mut cfg = state.config.write()?; - let existed = mcp::delete_in_config_for(&mut cfg, &app, id)?; - let snapshot = if existed { Some(cfg.clone()) } else { None }; - (existed, snapshot) - }; - if existed { - state.save()?; - if let Some(snapshot) = snapshot { - match app { - AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, - AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?, - } + + if let Some(servers) = &mut cfg.mcp.servers { + servers.remove(id) + } else { + None } + }; + + if let Some(server) = server { + state.save()?; + + // 从所有应用的 live 配置中移除 + Self::remove_server_from_all_apps(state, id, &server)?; + Ok(true) + } else { + Ok(false) } - Ok(existed) } - /// 设置 MCP 启用状态,并同步到客户端配置。 - pub fn set_enabled( + /// 切换指定应用的启用状态 + pub fn toggle_app( state: &AppState, + server_id: &str, app: AppType, - id: &str, enabled: bool, - ) -> Result { - let (existed, snapshot): (bool, Option) = { + ) -> Result<(), AppError> { + let server = { let mut cfg = state.config.write()?; - let existed = mcp::set_enabled_flag_for(&mut cfg, &app, id, enabled)?; - let snapshot = if existed { Some(cfg.clone()) } else { None }; - (existed, snapshot) + + if let Some(servers) = &mut cfg.mcp.servers { + if let Some(server) = servers.get_mut(server_id) { + server.apps.set_enabled_for(&app, enabled); + Some(server.clone()) + } else { + None + } + } else { + None + } }; - if existed { + if let Some(server) = server { state.save()?; - if let Some(snapshot) = snapshot { - match app { - AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, - AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?, - } + + // 同步到对应应用 + if enabled { + Self::sync_server_to_app(state, &server, &app)?; + } else { + Self::remove_server_from_app(state, server_id, &app)?; } } - Ok(existed) + + Ok(()) } - /// 手动同步已启用的 MCP 服务器到客户端配置。 - pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> { - let (snapshot, normalized): (MultiAppConfig, usize) = { - let mut cfg = state.config.write()?; - let normalized = mcp::normalize_servers_for(&mut cfg, &app); - (cfg.clone(), normalized) - }; - if normalized > 0 { - state.save()?; + /// 将 MCP 服务器同步到所有启用的应用 + fn sync_server_to_apps(state: &AppState, server: &McpServer) -> Result<(), AppError> { + let cfg = state.config.read()?; + + for app in server.apps.enabled_apps() { + Self::sync_server_to_app_internal(&cfg, server, &app)?; } + + Ok(()) + } + + /// 将 MCP 服务器同步到指定应用 + fn sync_server_to_app( + state: &AppState, + server: &McpServer, + app: &AppType, + ) -> Result<(), AppError> { + let cfg = state.config.read()?; + Self::sync_server_to_app_internal(&cfg, server, app) + } + + fn sync_server_to_app_internal( + cfg: &MultiAppConfig, + server: &McpServer, + app: &AppType, + ) -> Result<(), AppError> { match app { - AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, - AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?, + AppType::Claude => { + mcp::sync_single_server_to_claude(cfg, &server.id, &server.server)?; + } + AppType::Codex => { + mcp::sync_single_server_to_codex(cfg, &server.id, &server.server)?; + } + AppType::Gemini => { + mcp::sync_single_server_to_gemini(cfg, &server.id, &server.server)?; + } } Ok(()) } - /// 从 Claude 客户端配置导入 MCP 定义。 - pub fn import_from_claude(state: &AppState) -> Result { - let mut cfg = state.config.write()?; - let changed = mcp::import_from_claude(&mut cfg)?; - drop(cfg); - if changed > 0 { - state.save()?; + /// 从所有曾启用过该服务器的应用中移除 + fn remove_server_from_all_apps( + state: &AppState, + id: &str, + server: &McpServer, + ) -> Result<(), AppError> { + // 从所有曾启用的应用中移除 + for app in server.apps.enabled_apps() { + Self::remove_server_from_app(state, id, &app)?; } - Ok(changed) + Ok(()) } - /// 从 Codex 客户端配置导入 MCP 定义。 - pub fn import_from_codex(state: &AppState) -> Result { - let mut cfg = state.config.write()?; - let changed = mcp::import_from_codex(&mut cfg)?; - drop(cfg); - if changed > 0 { - state.save()?; + fn remove_server_from_app( + _state: &AppState, + id: &str, + app: &AppType, + ) -> Result<(), AppError> { + match app { + AppType::Claude => mcp::remove_server_from_claude(id)?, + AppType::Codex => mcp::remove_server_from_codex(id)?, + AppType::Gemini => mcp::remove_server_from_gemini(id)?, } - Ok(changed) + Ok(()) } - /// 从 Gemini 客户端配置导入 MCP 定义。 - pub fn import_from_gemini(state: &AppState) -> Result { - let mut cfg = state.config.write()?; - let changed = mcp::import_from_gemini(&mut cfg)?; - drop(cfg); - if changed > 0 { - state.save()?; + /// 手动同步所有启用的 MCP 服务器到对应的应用 + pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> { + let servers = Self::get_all_servers(state)?; + + for server in servers.values() { + Self::sync_server_to_apps(state, server)?; } - Ok(changed) + + Ok(()) } }