diff --git a/src-tauri/src/mcp/claude.rs b/src-tauri/src/mcp/claude.rs index 5d177272..c2108b9b 100644 --- a/src-tauri/src/mcp/claude.rs +++ b/src-tauri/src/mcp/claude.rs @@ -8,6 +8,12 @@ use crate::error::AppError; use super::validation::{extract_server_spec, validate_server_spec}; +fn should_sync_claude_mcp() -> bool { + // Claude 未安装/未初始化时:通常 ~/.claude 目录与 ~/.claude.json 都不存在。 + // 按用户偏好:此时跳过写入/删除,不创建任何文件或目录。 + crate::config::get_claude_config_dir().exists() || crate::config::get_claude_mcp_path().exists() +} + /// 返回已启用的 MCP 服务器(过滤 enabled==true) fn collect_enabled_servers(cfg: &McpConfig) -> HashMap { let mut out = HashMap::new(); @@ -33,6 +39,9 @@ fn collect_enabled_servers(cfg: &McpConfig) -> HashMap { /// 将 config.json 中 enabled==true 的项投影写入 ~/.claude.json pub fn sync_enabled_to_claude(config: &MultiAppConfig) -> Result<(), AppError> { + if !should_sync_claude_mcp() { + return Ok(()); + } let enabled = collect_enabled_servers(&config.mcp.claude); crate::claude_mcp::set_mcp_servers_map(&enabled) } @@ -107,6 +116,9 @@ pub fn sync_single_server_to_claude( id: &str, server_spec: &Value, ) -> Result<(), AppError> { + if !should_sync_claude_mcp() { + return Ok(()); + } // 读取现有的 MCP 配置 let current = crate::claude_mcp::read_mcp_servers_map()?; @@ -120,6 +132,9 @@ pub fn sync_single_server_to_claude( /// 从 Claude live 配置中移除单个 MCP 服务器 pub fn remove_server_from_claude(id: &str) -> Result<(), AppError> { + if !should_sync_claude_mcp() { + return Ok(()); + } // 读取现有的 MCP 配置 let mut current = crate::claude_mcp::read_mcp_servers_map()?; diff --git a/src-tauri/src/mcp/codex.rs b/src-tauri/src/mcp/codex.rs index 43761412..d86b5679 100644 --- a/src-tauri/src/mcp/codex.rs +++ b/src-tauri/src/mcp/codex.rs @@ -13,6 +13,12 @@ use crate::error::AppError; use super::validation::{extract_server_spec, validate_server_spec}; +fn should_sync_codex_mcp() -> bool { + // Codex 未安装/未初始化时:~/.codex 目录不存在。 + // 按用户偏好:目录缺失时跳过写入/删除,不创建任何文件或目录。 + crate::codex_config::get_codex_config_dir().exists() +} + /// 返回已启用的 MCP 服务器(过滤 enabled==true) fn collect_enabled_servers(cfg: &McpConfig) -> HashMap { let mut out = HashMap::new(); @@ -273,6 +279,9 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result /// - 仅更新 `mcp_servers` 表,保留其它键 /// - 仅写入启用项;无启用项时清理 mcp_servers 表 pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> { + if !should_sync_codex_mcp() { + return Ok(()); + } use toml_edit::{Item, Table}; // 1) 收集启用项(Codex 维度) @@ -339,6 +348,9 @@ pub fn sync_single_server_to_codex( id: &str, server_spec: &Value, ) -> Result<(), AppError> { + if !should_sync_codex_mcp() { + return Ok(()); + } use toml_edit::Item; // 读取现有的 config.toml @@ -385,6 +397,9 @@ pub fn sync_single_server_to_codex( /// 从 Codex live 配置中移除单个 MCP 服务器 /// 从正确的 [mcp_servers] 表中删除,同时清理可能存在于错误位置 [mcp.servers] 的数据 pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> { + if !should_sync_codex_mcp() { + return Ok(()); + } let config_path = crate::codex_config::get_codex_config_path(); if !config_path.exists() { diff --git a/src-tauri/src/mcp/gemini.rs b/src-tauri/src/mcp/gemini.rs index fde8e119..9a1a3064 100644 --- a/src-tauri/src/mcp/gemini.rs +++ b/src-tauri/src/mcp/gemini.rs @@ -8,6 +8,12 @@ use crate::error::AppError; use super::validation::{extract_server_spec, validate_server_spec}; +fn should_sync_gemini_mcp() -> bool { + // Gemini 未安装/未初始化时:~/.gemini 目录不存在。 + // 按用户偏好:目录缺失时跳过写入/删除,不创建任何文件或目录。 + crate::gemini_config::get_gemini_dir().exists() +} + /// 返回已启用的 MCP 服务器(过滤 enabled==true) fn collect_enabled_servers(cfg: &McpConfig) -> HashMap { let mut out = HashMap::new(); @@ -33,6 +39,9 @@ fn collect_enabled_servers(cfg: &McpConfig) -> HashMap { /// 将 config.json 中 Gemini 的 enabled==true 项写入 Gemini MCP 配置 pub fn sync_enabled_to_gemini(config: &MultiAppConfig) -> Result<(), AppError> { + if !should_sync_gemini_mcp() { + return Ok(()); + } let enabled = collect_enabled_servers(&config.mcp.gemini); crate::gemini_mcp::set_mcp_servers_map(&enabled) } @@ -103,6 +112,9 @@ pub fn sync_single_server_to_gemini( id: &str, server_spec: &Value, ) -> Result<(), AppError> { + if !should_sync_gemini_mcp() { + return Ok(()); + } // 读取现有的 MCP 配置 let mut current = crate::gemini_mcp::read_mcp_servers_map()?; @@ -115,6 +127,9 @@ pub fn sync_single_server_to_gemini( /// 从 Gemini live 配置中移除单个 MCP 服务器 pub fn remove_server_from_gemini(id: &str) -> Result<(), AppError> { + if !should_sync_gemini_mcp() { + return Ok(()); + } // 读取现有的 MCP 配置 let mut current = crate::gemini_mcp::read_mcp_servers_map()?; diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index 9a853aeb..a5ff40e3 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -154,6 +154,12 @@ fn sync_enabled_to_codex_writes_enabled_servers() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + // 模拟 Codex 已安装/已初始化:存在 ~/.codex 目录 + let path = cc_switch_lib::get_codex_config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("create codex dir"); + } + let mut config = MultiAppConfig::default(); config.mcp.codex.servers.insert( "stdio-enabled".into(), @@ -170,7 +176,6 @@ fn sync_enabled_to_codex_writes_enabled_servers() { cc_switch_lib::sync_enabled_to_codex(&config).expect("sync codex"); - let path = cc_switch_lib::get_codex_config_path(); assert!(path.exists(), "config.toml should be created"); let text = fs::read_to_string(&path).expect("read config.toml"); assert!( @@ -594,6 +599,11 @@ command = "echo" fn sync_claude_enabled_mcp_projects_to_user_config() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + let home = ensure_test_home(); + + // 模拟 Claude 已安装/已初始化:存在 ~/.claude 目录 + fs::create_dir_all(home.join(".claude")).expect("create claude dir"); + let mut config = MultiAppConfig::default(); config.mcp.claude.servers.insert( diff --git a/src-tauri/tests/mcp_commands.rs b/src-tauri/tests/mcp_commands.rs index 50508a1a..be7aa44e 100644 --- a/src-tauri/tests/mcp_commands.rs +++ b/src-tauri/tests/mcp_commands.rs @@ -247,11 +247,63 @@ fn set_mcp_enabled_for_codex_writes_live_config() { ); } +#[test] +fn enabling_codex_mcp_skips_when_codex_dir_missing() { + use support::create_test_state; + + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + // 确认 Codex 配置目录不存在(模拟“未安装/未运行过 Codex CLI”) + assert!( + !home.join(".codex").exists(), + "~/.codex should not exist in fresh test environment" + ); + + let state = create_test_state().expect("create test state"); + + // 先插入一个未启用 Codex 的 MCP 服务器(避免 upsert 触发同步) + McpService::upsert_server( + &state, + McpServer { + id: "codex-server".to_string(), + name: "Codex Server".to_string(), + server: json!({ + "type": "stdio", + "command": "echo" + }), + apps: McpApps { + claude: false, + codex: false, + gemini: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("insert server without syncing"); + + // 启用 Codex:目录缺失时应跳过写入(不创建 ~/.codex/config.toml) + McpService::toggle_app(&state, "codex-server", AppType::Codex, true) + .expect("toggle codex should succeed even when ~/.codex is missing"); + + assert!( + !home.join(".codex").exists(), + "~/.codex should still not exist after skipped sync" + ); +} + #[test] fn upsert_mcp_server_disabling_app_removes_from_claude_live_config() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); - ensure_test_home(); + let home = ensure_test_home(); + + // 模拟 Claude 已安装/已初始化:存在 ~/.claude 目录 + fs::create_dir_all(home.join(".claude")).expect("create ~/.claude dir"); // 先创建一个启用 Claude 的 MCP 服务器 let state = support::create_test_state().expect("create test state"); @@ -400,3 +452,105 @@ fn import_mcp_from_gemini_sse_url_only_is_valid() { "Gemini url-only server should be normalized to type=sse in unified structure" ); } + +#[test] +fn enabling_gemini_mcp_skips_when_gemini_dir_missing() { + use support::create_test_state; + + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + // 确认 Gemini 配置目录不存在(模拟“未安装/未运行过 Gemini CLI”) + assert!( + !home.join(".gemini").exists(), + "~/.gemini should not exist in fresh test environment" + ); + + let state = create_test_state().expect("create test state"); + + // 先插入一个未启用 Gemini 的 MCP 服务器(避免 upsert 触发同步) + McpService::upsert_server( + &state, + McpServer { + id: "gemini-server".to_string(), + name: "Gemini Server".to_string(), + server: json!({ + "type": "sse", + "url": "https://example.com/sse" + }), + apps: McpApps { + claude: false, + codex: false, + gemini: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("insert server without syncing"); + + // 启用 Gemini:目录缺失时应跳过写入(不创建 ~/.gemini/settings.json) + McpService::toggle_app(&state, "gemini-server", AppType::Gemini, true) + .expect("toggle gemini should succeed even when ~/.gemini is missing"); + + assert!( + !home.join(".gemini").exists(), + "~/.gemini should still not exist after skipped sync" + ); +} + +#[test] +fn enabling_claude_mcp_skips_when_claude_config_absent() { + use support::create_test_state; + + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + // 确认 Claude 相关目录/文件都不存在(模拟“未安装/未运行过 Claude”) + assert!( + !home.join(".claude").exists(), + "~/.claude should not exist in fresh test environment" + ); + assert!( + !home.join(".claude.json").exists(), + "~/.claude.json should not exist in fresh test environment" + ); + + let state = create_test_state().expect("create test state"); + + // 先插入一个未启用 Claude 的 MCP 服务器(避免 upsert 触发同步) + McpService::upsert_server( + &state, + McpServer { + id: "claude-server".to_string(), + name: "Claude Server".to_string(), + server: json!({ + "type": "stdio", + "command": "echo" + }), + apps: McpApps { + claude: false, + codex: false, + gemini: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("insert server without syncing"); + + // 启用 Claude:配置缺失时应跳过写入(不创建 ~/.claude.json) + McpService::toggle_app(&state, "claude-server", AppType::Claude, true) + .expect("toggle claude should succeed even when ~/.claude is missing"); + + assert!( + !home.join(".claude.json").exists(), + "~/.claude.json should still not exist after skipped sync" + ); +}