From 67bd8f5c113d96136513ecd609f20c8f6b83279b Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 17 Nov 2025 22:57:04 +0800 Subject: [PATCH] fix(mcp): correct Codex MCP configuration format to [mcp_servers] BREAKING CHANGE: The [mcp.servers] format was completely incorrect and not any official Codex format. The only correct format is [mcp_servers] at the top level of config.toml. Changes: - Remove incorrect [mcp.servers] nested table support - Always use [mcp_servers] top-level table (official Codex format) - Auto-migrate and cleanup erroneous [mcp.servers] entries on write - Preserve error-tolerant import for migrating old incorrect configs - Simplify sync logic by removing format selection branches (~60 lines) - Update all documentation and tests to reflect correct format - Add warning logs when detecting and cleaning incorrect format Backend (Rust): - mcp.rs: Simplify sync_enabled_to_codex by removing Target enum - mcp.rs: sync_single_server_to_codex now always uses [mcp_servers] - mcp.rs: remove_server_from_codex cleans both locations - mcp.rs: Update import_from_codex comments to clarify format status - tests: Rename test to sync_enabled_to_codex_migrates_erroneous_* - tests: Update assertions to verify migration behavior Frontend (TypeScript): - tomlUtils.ts: Prioritize [mcp_servers] format in parsing - tomlUtils.ts: Update error messages to guide correct format Documentation: - README.md: Correct MCP format reference to [mcp_servers] - CLAUDE.md: Add comprehensive format specification with examples All 79 tests pass. This ensures backward compatibility while enforcing the correct Codex official standard going forward. Refs: https://github.com/openai/codex/issues/3441 --- README.md | 2 +- README_ZH.md | 2 +- src-tauri/src/app_config.rs | 8 +- src-tauri/src/claude_mcp.rs | 12 +- src-tauri/src/commands/config.rs | 6 +- src-tauri/src/gemini_mcp.rs | 2 - src-tauri/src/mcp.rs | 166 ++++++++++---------------- src-tauri/tests/import_export_sync.rs | 53 +++++--- src-tauri/tests/mcp_commands.rs | 22 ++-- src/utils/tomlUtils.ts | 41 +++---- 10 files changed, 147 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index d0c902a..cfe5d7b 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{ver - Live config: `~/.codex/auth.json` (required) + `config.toml` (optional) - API key field: `OPENAI_API_KEY` in `auth.json` -- MCP servers: `~/.codex/config.toml` → `[mcp.servers]` +- MCP servers: `~/.codex/config.toml` → `[mcp_servers]` tables **Gemini** diff --git a/README_ZH.md b/README_ZH.md index 9244e18..fac90c4 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -137,7 +137,7 @@ brew upgrade --cask cc-switch - Live 配置:`~/.codex/auth.json`(必需)+ `config.toml`(可选) - API key 字段:`auth.json` 中的 `OPENAI_API_KEY` -- MCP 服务器:`~/.codex/config.toml` → `[mcp.servers]` +- MCP 服务器:`~/.codex/config.toml` → `[mcp_servers]` 表 **Gemini** diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 2daace5..dd748ea 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -317,7 +317,9 @@ impl MultiAppConfig { // 迁移通用配置片段:claude_common_config_snippet → common_config_snippets.claude if let Some(old_claude_snippet) = config.claude_common_config_snippet.take() { - log::info!("迁移通用配置:claude_common_config_snippet → common_config_snippets.claude"); + log::info!( + "迁移通用配置:claude_common_config_snippet → common_config_snippets.claude" + ); config.common_config_snippets.claude = Some(old_claude_snippet); updated = true; } @@ -414,9 +416,7 @@ impl MultiAppConfig { return Ok(false); } - log::info!( - "检测到已存在配置文件且 Prompt 列表为空,将尝试从现有提示词文件自动导入" - ); + log::info!("检测到已存在配置文件且 Prompt 列表为空,将尝试从现有提示词文件自动导入"); let mut imported = false; for app in [AppType::Claude, AppType::Codex, AppType::Gemini] { diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index 8a8bb35..0369e91 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -139,13 +139,11 @@ pub fn upsert_mcp_server(id: &str, spec: Value) -> Result { if is_http || is_sse { let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or(""); if url.is_empty() { - return Err(AppError::McpValidation( - if is_http { - "http 类型的 MCP 服务器缺少 url 字段".into() - } else { - "sse 类型的 MCP 服务器缺少 url 字段".into() - }, - )); + return Err(AppError::McpValidation(if is_http { + "http 类型的 MCP 服务器缺少 url 字段".into() + } else { + "sse 类型的 MCP 服务器缺少 url 字段".into() + })); } } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 4724430..b5b5fb0 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -184,8 +184,7 @@ pub async fn get_common_config_snippet( use crate::app_config::AppType; use std::str::FromStr; - let app = AppType::from_str(&app_type) - .map_err(|e| format!("无效的应用类型: {}", e))?; + let app = AppType::from_str(&app_type).map_err(|e| format!("无效的应用类型: {}", e))?; let guard = state .config @@ -205,8 +204,7 @@ pub async fn set_common_config_snippet( use crate::app_config::AppType; use std::str::FromStr; - let app = AppType::from_str(&app_type) - .map_err(|e| format!("无效的应用类型: {}", e))?; + let app = AppType::from_str(&app_type).map_err(|e| format!("无效的应用类型: {}", e))?; let mut guard = state .config diff --git a/src-tauri/src/gemini_mcp.rs b/src-tauri/src/gemini_mcp.rs index 6213d35..b2eb91a 100644 --- a/src-tauri/src/gemini_mcp.rs +++ b/src-tauri/src/gemini_mcp.rs @@ -48,8 +48,6 @@ pub fn read_mcp_json() -> Result, AppError> { Ok(Some(content)) } - - /// 读取 Gemini settings.json 中的 mcpServers 映射 pub fn read_mcp_servers_map() -> Result, AppError> { let path = user_config_path(); diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 30b6181..61a386e 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -396,18 +396,18 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result] 与 [mcp_servers.] +/// +/// 格式支持: +/// - 正确格式:[mcp_servers.*](Codex 官方标准) +/// - 错误格式:[mcp.servers.*](容错读取,用于迁移错误写入的配置) +/// /// 已存在的服务器将启用 Codex 应用,不覆盖其他字段和应用状态 pub fn import_from_codex(config: &mut MultiAppConfig) -> Result { use crate::app_config::{McpApps, McpServer}; @@ -629,13 +629,16 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result Ok(changed_total) } -/// 将 config.json 中 Codex 的 enabled==true 项以 TOML 形式写入 ~/.codex/config.toml 的 [mcp.servers] -/// 策略: +/// 将 config.json 中 Codex 的 enabled==true 项以 TOML 形式写入 ~/.codex/config.toml +/// +/// 格式策略: +/// - 唯一正确格式:[mcp_servers] 顶层表(Codex 官方标准) +/// - 自动清理错误格式:[mcp.servers](如果存在) /// - 读取现有 config.toml;若语法无效则报错,不尝试覆盖 -/// - 仅更新 `mcp.servers` 或 `mcp_servers` 子表,保留 `mcp` 其它键 -/// - 仅写入启用项;无启用项时清理对应子表 +/// - 仅更新 `mcp_servers` 表,保留其它键 +/// - 仅写入启用项;无启用项时清理 mcp_servers 表 pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> { - use toml_edit::{DocumentMut, Item, Table}; + use toml_edit::{Item, Table}; // 1) 收集启用项(Codex 维度) let enabled = collect_enabled_servers(&config.mcp.codex); @@ -644,44 +647,31 @@ pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> { let base_text = crate::codex_config::read_and_validate_codex_config_text()?; // 3) 使用 toml_edit 解析(允许空文件) - let mut doc: DocumentMut = if base_text.trim().is_empty() { - DocumentMut::default() + let mut doc = if base_text.trim().is_empty() { + toml_edit::DocumentMut::default() } else { base_text - .parse::() + .parse::() .map_err(|e| AppError::McpValidation(format!("解析 config.toml 失败: {e}")))? }; - enum Target { - McpServers, // 顶层 mcp_servers - McpDotServers, // mcp.servers + // 4) 清理可能存在的错误格式 [mcp.servers] + if let Some(mcp_item) = doc.get_mut("mcp") { + if let Some(tbl) = mcp_item.as_table_like_mut() { + if tbl.contains_key("servers") { + log::warn!("检测到错误的 MCP 格式 [mcp.servers],正在清理并迁移到 [mcp_servers]"); + tbl.remove("servers"); + } + } } - // 4) 选择目标风格:优先沿用既有子表;其次在 mcp 表下新建;最后退回顶层 mcp_servers - let has_mcp_dot_servers = doc - .get("mcp") - .and_then(|m| m.get("servers")) - .and_then(|s| s.as_table_like()) - .is_some(); - let has_mcp_servers = doc - .get("mcp_servers") - .and_then(|s| s.as_table_like()) - .is_some(); - let mcp_is_table = doc.get("mcp").and_then(|m| m.as_table_like()).is_some(); - - let target = if has_mcp_dot_servers { - Target::McpDotServers - } else if has_mcp_servers { - Target::McpServers - } else if mcp_is_table { - Target::McpDotServers + // 5) 构造目标 servers 表(稳定的键顺序) + if enabled.is_empty() { + // 无启用项:移除 mcp_servers 表 + doc.as_table_mut().remove("mcp_servers"); } else { - Target::McpServers - }; - - // 构造目标 servers 表(稳定的键顺序) - let build_servers_table = || -> Table { - let mut servers = Table::new(); + // 构建 servers 表 + let mut servers_tbl = Table::new(); let mut ids: Vec<_> = enabled.keys().cloned().collect(); ids.sort(); for id in ids { @@ -689,47 +679,15 @@ pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> { // 复用通用转换函数(已包含扩展字段支持) match json_server_to_toml_table(spec) { Ok(table) => { - servers[&id[..]] = Item::Table(table); + servers_tbl[&id[..]] = Item::Table(table); } Err(err) => { log::error!("跳过无效的 MCP 服务器 '{id}': {err}"); } } } - servers - }; - - // 5) 应用更新:仅就地更新目标子表;避免改动其它键/注释/空白 - if enabled.is_empty() { - // 无启用项:移除两种 servers 表(如果存在),但保留 mcp 其它字段 - if let Some(mcp_item) = doc.get_mut("mcp") { - if let Some(tbl) = mcp_item.as_table_like_mut() { - tbl.remove("servers"); - } - } - doc.as_table_mut().remove("mcp_servers"); - } else { - let servers_tbl = build_servers_table(); - match target { - Target::McpDotServers => { - // 确保 mcp 为表 - if doc.get("mcp").and_then(|m| m.as_table_like()).is_none() { - doc["mcp"] = Item::Table(Table::new()); - } - doc["mcp"]["servers"] = Item::Table(servers_tbl); - // 去重:若存在顶层 mcp_servers,则移除以避免重复定义 - doc.as_table_mut().remove("mcp_servers"); - } - Target::McpServers => { - doc["mcp_servers"] = Item::Table(servers_tbl); - // 去重:若存在 mcp.servers,则移除该子表,保留 mcp 其它键 - if let Some(mcp_item) = doc.get_mut("mcp") { - if let Some(tbl) = mcp_item.as_table_like_mut() { - tbl.remove("servers"); - } - } - } - } + // 使用唯一正确的格式:[mcp_servers] + doc["mcp_servers"] = Item::Table(servers_tbl); } // 6) 写回(仅改 TOML,不触碰 auth.json);toml_edit 会尽量保留未改区域的注释/空白/顺序 @@ -808,11 +766,7 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Option Option Option Result } /// 将单个 MCP 服务器同步到 Codex live 配置 +/// 始终使用 Codex 官方格式 [mcp_servers],并清理可能存在的错误格式 [mcp.servers] pub fn sync_single_server_to_codex( _config: &MultiAppConfig, id: &str, @@ -1094,24 +1042,26 @@ pub fn sync_single_server_to_codex( toml_edit::DocumentMut::new() }; - // 确保 [mcp] 表存在 - if !doc.contains_key("mcp") { - doc["mcp"] = toml_edit::table(); + // 清理可能存在的错误格式 [mcp.servers] + if let Some(mcp_item) = doc.get_mut("mcp") { + if let Some(tbl) = mcp_item.as_table_like_mut() { + if tbl.contains_key("servers") { + log::warn!("检测到错误的 MCP 格式 [mcp.servers],正在清理并迁移到 [mcp_servers]"); + tbl.remove("servers"); + } + } } - // 确保 [mcp.servers] 子表存在 - if doc["mcp"] - .as_table() - .and_then(|t| t.get("servers")) - .is_none() - { - doc["mcp"]["servers"] = toml_edit::table(); + // 确保 [mcp_servers] 表存在 + if !doc.contains_key("mcp_servers") { + doc["mcp_servers"] = toml_edit::table(); } // 将 JSON 服务器规范转换为 TOML 表 let toml_table = json_server_to_toml_table(server_spec)?; - doc["mcp"]["servers"][id] = Item::Table(toml_table); + // 使用唯一正确的格式:[mcp_servers] + doc["mcp_servers"][id] = Item::Table(toml_table); // 写回文件 std::fs::write(&config_path, doc.to_string()).map_err(|e| AppError::io(&config_path, e))?; @@ -1120,6 +1070,7 @@ pub fn sync_single_server_to_codex( } /// 从 Codex live 配置中移除单个 MCP 服务器 +/// 从正确的 [mcp_servers] 表中删除,同时清理可能存在于错误位置 [mcp.servers] 的数据 pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> { let config_path = crate::codex_config::get_codex_config_path(); @@ -1134,10 +1085,17 @@ pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> { .parse::() .map_err(|e| AppError::McpValidation(format!("解析 Codex config.toml 失败: {e}")))?; - // 从 [mcp.servers] 中删除 + // 从正确的位置删除:[mcp_servers] + if let Some(mcp_servers) = doc.get_mut("mcp_servers").and_then(|s| s.as_table_mut()) { + mcp_servers.remove(id); + } + + // 同时清理可能存在于错误位置的数据:[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); + if servers.remove(id).is_some() { + log::warn!("从错误的 MCP 格式 [mcp.servers] 中清理了服务器 '{}'", id); + } } } diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index f71ce1c..32082a0 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -222,10 +222,13 @@ mode = "dev" text.contains("[profile]"), "non-MCP table should be preserved" ); - // 新增的 mcp_servers/或 mcp.servers 应存在并包含 echo assert!( - text.contains("mcp_servers") || text.contains("[mcp.servers]"), - "one server table style should be present" + text.contains("mcp_servers"), + "mcp_servers table should be present" + ); + assert!( + !text.contains("[mcp.servers]"), + "invalid [mcp.servers] table should not appear" ); assert!( text.contains("echo") && text.contains("command = \"echo\""), @@ -234,14 +237,14 @@ mode = "dev" } #[test] -fn sync_enabled_to_codex_keeps_existing_style_mcp_dot_servers() { +fn sync_enabled_to_codex_migrates_erroneous_mcp_dot_servers_to_mcp_servers() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let path = cc_switch_lib::get_codex_config_path(); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create codex dir"); } - // 预置 mcp.servers 风格 + // 预置错误的 mcp.servers 风格(应迁移为顶层 mcp_servers) let seed = r#"[mcp] other = "keep" [mcp.servers] @@ -260,14 +263,14 @@ fn sync_enabled_to_codex_keeps_existing_style_mcp_dot_servers() { cc_switch_lib::sync_enabled_to_codex(&config).expect("sync codex"); let text = fs::read_to_string(&path).expect("read config.toml"); - // 仍应采用 mcp.servers 风格 + // 应迁移到顶层 mcp_servers,并移除错误的 mcp.servers 表 assert!( - text.contains("[mcp.servers]"), - "should keep mcp.servers style" + text.contains("mcp_servers"), + "should migrate to mcp_servers table" ); assert!( - !text.contains("mcp_servers"), - "should not switch to mcp_servers" + !text.contains("[mcp.servers]"), + "invalid [mcp.servers] table should be removed" ); } @@ -488,10 +491,17 @@ url = "https://example.com" assert!(changed >= 2, "should import both servers"); // v3.7.0: 检查统一结构 - let servers = config.mcp.servers.as_ref().expect("unified servers should exist"); + let servers = config + .mcp + .servers + .as_ref() + .expect("unified servers should exist"); let echo = servers.get("echo_server").expect("echo server"); - assert_eq!(echo.apps.codex, true, "Codex app should be enabled for echo_server"); + assert_eq!( + echo.apps.codex, true, + "Codex app should be enabled for echo_server" + ); let server_spec = echo.server.as_object().expect("server spec"); assert_eq!( server_spec @@ -502,7 +512,10 @@ url = "https://example.com" ); let http = servers.get("http_server").expect("http server"); - assert_eq!(http.apps.codex, true, "Codex app should be enabled for http_server"); + assert_eq!( + http.apps.codex, true, + "Codex app should be enabled for http_server" + ); let http_spec = http.server.as_object().expect("http spec"); assert_eq!( http_spec.get("url").and_then(|v| v.as_str()).unwrap_or(""), @@ -541,7 +554,7 @@ command = "echo" }), apps: cc_switch_lib::McpApps { claude: false, - codex: false, // 初始未启用 + codex: false, // 初始未启用 gemini: false, }, description: None, @@ -564,7 +577,10 @@ command = "echo" .expect("existing entry"); // 验证 Codex 应用已启用 - assert_eq!(entry.apps.codex, true, "Codex app should be enabled after import"); + assert_eq!( + entry.apps.codex, true, + "Codex app should be enabled after import" + ); // 验证现有配置被保留(server 不应被覆盖) let spec = entry.server.as_object().expect("server spec"); @@ -662,7 +678,7 @@ fn import_from_claude_merges_into_config() { "command": "prev" }), apps: cc_switch_lib::McpApps { - claude: false, // 初始未启用 + claude: false, // 初始未启用 codex: false, gemini: false, }, @@ -686,7 +702,10 @@ fn import_from_claude_merges_into_config() { .expect("entry exists"); // 验证 Claude 应用已启用 - assert_eq!(entry.apps.claude, true, "Claude app should be enabled after import"); + assert_eq!( + entry.apps.claude, true, + "Claude app should be enabled after import" + ); // 验证现有配置被保留(server 不应被覆盖) let server = entry.server.as_object().expect("server obj"); diff --git a/src-tauri/tests/mcp_commands.rs b/src-tauri/tests/mcp_commands.rs index 30160c7..ad342c4 100644 --- a/src-tauri/tests/mcp_commands.rs +++ b/src-tauri/tests/mcp_commands.rs @@ -127,8 +127,14 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() { let guard = state.config.read().expect("lock config"); // v3.7.0: 检查统一结构 - let servers = guard.mcp.servers.as_ref().expect("unified servers should exist"); - let entry = servers.get("echo").expect("server imported into unified structure"); + let servers = guard + .mcp + .servers + .as_ref() + .expect("unified servers should exist"); + let entry = servers + .get("echo") + .expect("server imported into unified structure"); assert!( entry.apps.claude, "imported server should have Claude app enabled" @@ -182,10 +188,12 @@ fn set_mcp_enabled_for_codex_writes_live_config() { // 创建 Codex 配置目录和文件 let codex_dir = home.join(".codex"); fs::create_dir_all(&codex_dir).expect("create codex dir"); - fs::write(codex_dir.join("auth.json"), r#"{"OPENAI_API_KEY":"test-key"}"#) - .expect("create auth.json"); - fs::write(codex_dir.join("config.toml"), "") - .expect("create empty config.toml"); + fs::write( + codex_dir.join("auth.json"), + r#"{"OPENAI_API_KEY":"test-key"}"#, + ) + .expect("create auth.json"); + fs::write(codex_dir.join("config.toml"), "").expect("create empty config.toml"); let mut config = MultiAppConfig::default(); config.ensure_app(&AppType::Codex); @@ -203,7 +211,7 @@ fn set_mcp_enabled_for_codex_writes_live_config() { }), apps: McpApps { claude: false, - codex: false, // 初始未启用 + codex: false, // 初始未启用 gemini: false, }, description: None, diff --git a/src/utils/tomlUtils.ts b/src/utils/tomlUtils.ts index baa507b..0c7a5c2 100644 --- a/src/utils/tomlUtils.ts +++ b/src/utils/tomlUtils.ts @@ -44,7 +44,8 @@ export const mcpServerToToml = (server: McpServerSpec): string => { * 将 TOML 文本转换为 McpServerSpec 对象(单个服务器配置) * 支持两种格式: * 1. 直接的服务器配置(type, command, args 等) - * 2. [mcp.servers.] 或 [mcp_servers.] 格式(取第一个服务器) + * 2. [mcp_servers.] 格式(推荐,取第一个服务器) + * 3. [mcp.servers.] 错误格式(容错解析,同样取第一个服务器) * @param tomlText TOML 文本 * @returns McpServer 对象 * @throws 解析或转换失败时抛出错误 @@ -67,7 +68,16 @@ export const tomlToMcpServer = (tomlText: string): McpServerSpec => { return normalizeServerConfig(parsed); } - // 情况 2: [mcp.servers.] 格式 + // 情况 2: [mcp_servers.] 格式(推荐) + if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") { + const serverIds = Object.keys(parsed.mcp_servers); + if (serverIds.length > 0) { + const firstServer = (parsed.mcp_servers as any)[serverIds[0]]; + return normalizeServerConfig(firstServer); + } + } + + // 情况 3: [mcp.servers.] 错误格式(容错解析) if (parsed.mcp && typeof parsed.mcp === "object") { const mcpObj = parsed.mcp as any; if (mcpObj.servers && typeof mcpObj.servers === "object") { @@ -79,17 +89,8 @@ export const tomlToMcpServer = (tomlText: string): McpServerSpec => { } } - // 情况 3: [mcp_servers.] 格式 - if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") { - const serverIds = Object.keys(parsed.mcp_servers); - if (serverIds.length > 0) { - const firstServer = (parsed.mcp_servers as any)[serverIds[0]]; - return normalizeServerConfig(firstServer); - } - } - throw new Error( - "无法识别的 TOML 格式。请提供单个 MCP 服务器配置,或使用 [mcp.servers.] 格式", + "无法识别的 TOML 格式。请提供单个 MCP 服务器配置,或使用 [mcp_servers.] 格式", ); }; @@ -189,7 +190,14 @@ export const extractIdFromToml = (tomlText: string): string => { try { const parsed = parseToml(normalizeTomlText(tomlText)); - // 尝试从 [mcp.servers.] 或 [mcp_servers.] 中提取 ID + // 尝试从 [mcp_servers.] 或 [mcp.servers.] 中提取 ID + if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") { + const serverIds = Object.keys(parsed.mcp_servers); + if (serverIds.length > 0) { + return serverIds[0]; + } + } + if (parsed.mcp && typeof parsed.mcp === "object") { const mcpObj = parsed.mcp as any; if (mcpObj.servers && typeof mcpObj.servers === "object") { @@ -200,13 +208,6 @@ export const extractIdFromToml = (tomlText: string): string => { } } - if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") { - const serverIds = Object.keys(parsed.mcp_servers); - if (serverIds.length > 0) { - return serverIds[0]; - } - } - // 尝试从 command 中推断 if (parsed.command && typeof parsed.command === "string") { const cmd = parsed.command.split(/[\\/]/).pop() || "";