fix(mcp): migrate import functions to unified v3.7.0 structure
- Rewrite import_from_claude/codex/gemini to write directly to mcp.servers - Implement skip-on-error strategy for fault tolerance (single invalid item no longer aborts entire batch) - Smart merge logic: existing servers only enable corresponding app, preserve other configs - Remove deprecated markers from service layer - Export McpApps type for test usage - Update mcp_commands tests to use unified structure Fixes runtime import issue where data was written to legacy structure (mcp.claude/codex.servers) but unified panel reads from new structure (mcp.servers), causing "imported but invisible" bug.
This commit is contained in:
@@ -18,7 +18,7 @@ mod settings;
|
|||||||
mod store;
|
mod store;
|
||||||
mod usage_script;
|
mod usage_script;
|
||||||
|
|
||||||
pub use app_config::{AppType, McpServer, MultiAppConfig};
|
pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
|
||||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||||
pub use commands::*;
|
pub use commands::*;
|
||||||
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||||
|
|||||||
@@ -324,92 +324,101 @@ pub fn sync_enabled_to_claude(config: &MultiAppConfig) -> Result<(), AppError> {
|
|||||||
crate::claude_mcp::set_mcp_servers_map(&enabled)
|
crate::claude_mcp::set_mcp_servers_map(&enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 ~/.claude.json 导入 mcpServers 到 config.json(设为 enabled=true)。
|
/// 从 ~/.claude.json 导入 mcpServers 到统一结构(v3.7.0+)
|
||||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
/// 已存在的服务器将启用 Claude 应用,不覆盖其他字段和应用状态
|
||||||
pub fn import_from_claude(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
pub fn import_from_claude(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||||
|
use crate::app_config::{McpApps, McpServer};
|
||||||
|
|
||||||
let text_opt = crate::claude_mcp::read_mcp_json()?;
|
let text_opt = crate::claude_mcp::read_mcp_json()?;
|
||||||
let Some(text) = text_opt else { return Ok(0) };
|
let Some(text) = text_opt else { return Ok(0) };
|
||||||
let mut changed = normalize_servers_for(config, &AppType::Claude);
|
|
||||||
let v: Value = serde_json::from_str(&text)
|
let v: Value = serde_json::from_str(&text)
|
||||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.claude.json 失败: {e}")))?;
|
.map_err(|e| AppError::McpValidation(format!("解析 ~/.claude.json 失败: {e}")))?;
|
||||||
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
||||||
return Ok(changed);
|
return Ok(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保新结构存在
|
||||||
|
if config.mcp.servers.is_none() {
|
||||||
|
config.mcp.servers = Some(HashMap::new());
|
||||||
|
}
|
||||||
|
let servers = config.mcp.servers.as_mut().unwrap();
|
||||||
|
|
||||||
|
let mut changed = 0;
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
for (id, spec) in map.iter() {
|
for (id, spec) in map.iter() {
|
||||||
// 校验目标 spec
|
// 校验:单项失败不中止,收集错误继续处理
|
||||||
validate_server_spec(spec)?;
|
if let Err(e) = validate_server_spec(spec) {
|
||||||
|
log::warn!("跳过无效 MCP 服务器 '{id}': {e}");
|
||||||
|
errors.push(format!("{id}: {e}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let entry = config
|
if let Some(existing) = servers.get_mut(id) {
|
||||||
.mcp_for_mut(&AppType::Claude)
|
// 已存在:仅启用 Claude 应用
|
||||||
.servers
|
if !existing.apps.claude {
|
||||||
.entry(id.clone());
|
existing.apps.claude = true;
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
match entry {
|
|
||||||
Entry::Vacant(vac) => {
|
|
||||||
let mut obj = serde_json::Map::new();
|
|
||||||
obj.insert(String::from("id"), json!(id));
|
|
||||||
obj.insert(String::from("name"), json!(id));
|
|
||||||
obj.insert(String::from("server"), spec.clone());
|
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
|
||||||
vac.insert(Value::Object(obj));
|
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
log::info!("MCP 服务器 '{id}' 已启用 Claude 应用");
|
||||||
}
|
}
|
||||||
Entry::Occupied(mut occ) => {
|
} else {
|
||||||
let value = occ.get_mut();
|
// 新建服务器:默认仅启用 Claude
|
||||||
let Some(existing) = value.as_object_mut() else {
|
servers.insert(
|
||||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
id.clone(),
|
||||||
let mut obj = serde_json::Map::new();
|
McpServer {
|
||||||
obj.insert(String::from("id"), json!(id));
|
id: id.clone(),
|
||||||
obj.insert(String::from("name"), json!(id));
|
name: id.clone(),
|
||||||
obj.insert(String::from("server"), spec.clone());
|
server: spec.clone(),
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
apps: McpApps {
|
||||||
occ.insert(Value::Object(obj));
|
claude: true,
|
||||||
changed += 1;
|
codex: false,
|
||||||
continue;
|
gemini: false,
|
||||||
};
|
},
|
||||||
|
description: None,
|
||||||
let mut modified = false;
|
homepage: None,
|
||||||
let prev_enabled = existing
|
docs: None,
|
||||||
.get("enabled")
|
tags: Vec::new(),
|
||||||
.and_then(|b| b.as_bool())
|
},
|
||||||
.unwrap_or(false);
|
);
|
||||||
if !prev_enabled {
|
changed += 1;
|
||||||
existing.insert(String::from("enabled"), json!(true));
|
log::info!("导入新 MCP 服务器 '{id}'");
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("server").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
|
||||||
existing.insert(String::from("server"), spec.clone());
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("id").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
|
||||||
existing.insert(String::from("id"), json!(id));
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if modified {
|
|
||||||
changed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
log::warn!(
|
||||||
|
"导入完成,但有 {} 项失败: {:?}",
|
||||||
|
errors.len(),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 ~/.codex/config.toml 导入 MCP 到 config.json(Codex 作用域),并将导入项设为 enabled=true。
|
/// 从 ~/.codex/config.toml 导入 MCP 到统一结构(v3.7.0+)
|
||||||
/// 支持两种 schema:[mcp.servers.<id>] 与 [mcp_servers.<id>]。
|
/// 支持两种 schema:[mcp.servers.<id>] 与 [mcp_servers.<id>]
|
||||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
/// 已存在的服务器将启用 Codex 应用,不覆盖其他字段和应用状态
|
||||||
pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||||
|
use crate::app_config::{McpApps, McpServer};
|
||||||
|
|
||||||
let text = crate::codex_config::read_and_validate_codex_config_text()?;
|
let text = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||||
if text.trim().is_empty() {
|
if text.trim().is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
let mut changed_total = normalize_servers_for(config, &AppType::Codex);
|
|
||||||
|
|
||||||
let root: toml::Table = toml::from_str(&text)
|
let root: toml::Table = toml::from_str(&text)
|
||||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.codex/config.toml 失败: {e}")))?;
|
.map_err(|e| AppError::McpValidation(format!("解析 ~/.codex/config.toml 失败: {e}")))?;
|
||||||
|
|
||||||
|
// 确保新结构存在
|
||||||
|
if config.mcp.servers.is_none() {
|
||||||
|
config.mcp.servers = Some(HashMap::new());
|
||||||
|
}
|
||||||
|
let servers = config.mcp.servers.as_mut().unwrap();
|
||||||
|
|
||||||
|
let mut changed_total = 0usize;
|
||||||
|
|
||||||
// helper:处理一组 servers 表
|
// helper:处理一组 servers 表
|
||||||
let mut import_servers_tbl = |servers_tbl: &toml::value::Table| {
|
let mut import_servers_tbl = |servers_tbl: &toml::value::Table| {
|
||||||
let mut changed = 0usize;
|
let mut changed = 0usize;
|
||||||
@@ -476,70 +485,48 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {
|
||||||
|
log::warn!("跳过未知类型 '{typ}' 的 Codex MCP 项 '{id}'");
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let spec_v = serde_json::Value::Object(spec);
|
let spec_v = serde_json::Value::Object(spec);
|
||||||
|
|
||||||
// 校验
|
// 校验:单项失败继续处理
|
||||||
if let Err(e) = validate_server_spec(&spec_v) {
|
if let Err(e) = validate_server_spec(&spec_v) {
|
||||||
log::warn!("跳过无效 Codex MCP 项 '{id}': {e}");
|
log::warn!("跳过无效 Codex MCP 项 '{id}': {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并:仅强制 enabled=true
|
if let Some(existing) = servers.get_mut(id) {
|
||||||
use std::collections::hash_map::Entry;
|
// 已存在:仅启用 Codex 应用
|
||||||
let entry = config
|
if !existing.apps.codex {
|
||||||
.mcp_for_mut(&AppType::Codex)
|
existing.apps.codex = true;
|
||||||
.servers
|
|
||||||
.entry(id.clone());
|
|
||||||
match entry {
|
|
||||||
Entry::Vacant(vac) => {
|
|
||||||
let mut obj = serde_json::Map::new();
|
|
||||||
obj.insert(String::from("id"), json!(id));
|
|
||||||
obj.insert(String::from("name"), json!(id));
|
|
||||||
obj.insert(String::from("server"), spec_v.clone());
|
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
|
||||||
vac.insert(serde_json::Value::Object(obj));
|
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
log::info!("MCP 服务器 '{id}' 已启用 Codex 应用");
|
||||||
}
|
}
|
||||||
Entry::Occupied(mut occ) => {
|
} else {
|
||||||
let value = occ.get_mut();
|
// 新建服务器:默认仅启用 Codex
|
||||||
let Some(existing) = value.as_object_mut() else {
|
servers.insert(
|
||||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
id.clone(),
|
||||||
let mut obj = serde_json::Map::new();
|
McpServer {
|
||||||
obj.insert(String::from("id"), json!(id));
|
id: id.clone(),
|
||||||
obj.insert(String::from("name"), json!(id));
|
name: id.clone(),
|
||||||
obj.insert(String::from("server"), spec_v.clone());
|
server: spec_v,
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
apps: McpApps {
|
||||||
occ.insert(serde_json::Value::Object(obj));
|
claude: false,
|
||||||
changed += 1;
|
codex: true,
|
||||||
continue;
|
gemini: false,
|
||||||
};
|
},
|
||||||
|
description: None,
|
||||||
let mut modified = false;
|
homepage: None,
|
||||||
let prev = existing
|
docs: None,
|
||||||
.get("enabled")
|
tags: Vec::new(),
|
||||||
.and_then(|b| b.as_bool())
|
},
|
||||||
.unwrap_or(false);
|
);
|
||||||
if !prev {
|
changed += 1;
|
||||||
existing.insert(String::from("enabled"), json!(true));
|
log::info!("导入新 MCP 服务器 '{id}'");
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("server").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
|
||||||
existing.insert(String::from("server"), spec_v.clone());
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("id").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
|
||||||
existing.insert(String::from("id"), json!(id));
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if modified {
|
|
||||||
changed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
@@ -724,76 +711,76 @@ pub fn sync_enabled_to_gemini(config: &MultiAppConfig) -> Result<(), AppError> {
|
|||||||
crate::gemini_mcp::set_mcp_servers_map(&enabled)
|
crate::gemini_mcp::set_mcp_servers_map(&enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 ~/.gemini/settings.json 导入 mcpServers 到 config.json(设为 enabled=true)。
|
/// 从 ~/.gemini/settings.json 导入 mcpServers 到统一结构(v3.7.0+)
|
||||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
/// 已存在的服务器将启用 Gemini 应用,不覆盖其他字段和应用状态
|
||||||
pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||||
|
use crate::app_config::{McpApps, McpServer};
|
||||||
|
|
||||||
let text_opt = crate::gemini_mcp::read_mcp_json()?;
|
let text_opt = crate::gemini_mcp::read_mcp_json()?;
|
||||||
let Some(text) = text_opt else { return Ok(0) };
|
let Some(text) = text_opt else { return Ok(0) };
|
||||||
let mut changed = normalize_servers_for(config, &AppType::Gemini);
|
|
||||||
let v: Value = serde_json::from_str(&text)
|
let v: Value = serde_json::from_str(&text)
|
||||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.gemini/settings.json 失败: {e}")))?;
|
.map_err(|e| AppError::McpValidation(format!("解析 ~/.gemini/settings.json 失败: {e}")))?;
|
||||||
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
||||||
return Ok(changed);
|
return Ok(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保新结构存在
|
||||||
|
if config.mcp.servers.is_none() {
|
||||||
|
config.mcp.servers = Some(HashMap::new());
|
||||||
|
}
|
||||||
|
let servers = config.mcp.servers.as_mut().unwrap();
|
||||||
|
|
||||||
|
let mut changed = 0;
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
for (id, spec) in map.iter() {
|
for (id, spec) in map.iter() {
|
||||||
// 校验目标 spec
|
// 校验:单项失败不中止,收集错误继续处理
|
||||||
validate_server_spec(spec)?;
|
if let Err(e) = validate_server_spec(spec) {
|
||||||
|
log::warn!("跳过无效 MCP 服务器 '{id}': {e}");
|
||||||
|
errors.push(format!("{id}: {e}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let entry = config
|
if let Some(existing) = servers.get_mut(id) {
|
||||||
.mcp_for_mut(&AppType::Gemini)
|
// 已存在:仅启用 Gemini 应用
|
||||||
.servers
|
if !existing.apps.gemini {
|
||||||
.entry(id.clone());
|
existing.apps.gemini = true;
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
match entry {
|
|
||||||
Entry::Vacant(vac) => {
|
|
||||||
let mut obj = serde_json::Map::new();
|
|
||||||
obj.insert(String::from("id"), json!(id));
|
|
||||||
obj.insert(String::from("name"), json!(id));
|
|
||||||
obj.insert(String::from("server"), spec.clone());
|
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
|
||||||
vac.insert(Value::Object(obj));
|
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
log::info!("MCP 服务器 '{id}' 已启用 Gemini 应用");
|
||||||
}
|
}
|
||||||
Entry::Occupied(mut occ) => {
|
} else {
|
||||||
let value = occ.get_mut();
|
// 新建服务器:默认仅启用 Gemini
|
||||||
let Some(existing) = value.as_object_mut() else {
|
servers.insert(
|
||||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
id.clone(),
|
||||||
let mut obj = serde_json::Map::new();
|
McpServer {
|
||||||
obj.insert(String::from("id"), json!(id));
|
id: id.clone(),
|
||||||
obj.insert(String::from("name"), json!(id));
|
name: id.clone(),
|
||||||
obj.insert(String::from("server"), spec.clone());
|
server: spec.clone(),
|
||||||
obj.insert(String::from("enabled"), json!(true));
|
apps: McpApps {
|
||||||
occ.insert(Value::Object(obj));
|
claude: false,
|
||||||
changed += 1;
|
codex: false,
|
||||||
continue;
|
gemini: true,
|
||||||
};
|
},
|
||||||
|
description: None,
|
||||||
let mut modified = false;
|
homepage: None,
|
||||||
let prev_enabled = existing
|
docs: None,
|
||||||
.get("enabled")
|
tags: Vec::new(),
|
||||||
.and_then(|b| b.as_bool())
|
},
|
||||||
.unwrap_or(false);
|
);
|
||||||
if !prev_enabled {
|
changed += 1;
|
||||||
existing.insert(String::from("enabled"), json!(true));
|
log::info!("导入新 MCP 服务器 '{id}'");
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("server").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
|
||||||
existing.insert(String::from("server"), spec.clone());
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if existing.get("id").is_none() {
|
|
||||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
|
||||||
existing.insert(String::from("id"), json!(id));
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
if modified {
|
|
||||||
changed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
log::warn!(
|
||||||
|
"导入完成,但有 {} 项失败: {:?}",
|
||||||
|
errors.len(),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,11 +231,7 @@ impl McpService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [已废弃] 从 Claude 导入 MCP(兼容旧 API)
|
/// 从 Claude 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
#[deprecated(
|
|
||||||
since = "3.7.0",
|
|
||||||
note = "Import will be handled differently in unified structure"
|
|
||||||
)]
|
|
||||||
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
let mut cfg = state.config.write()?;
|
||||||
let count = mcp::import_from_claude(&mut cfg)?;
|
let count = mcp::import_from_claude(&mut cfg)?;
|
||||||
@@ -244,11 +240,7 @@ impl McpService {
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [已废弃] 从 Codex 导入 MCP(兼容旧 API)
|
/// 从 Codex 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
#[deprecated(
|
|
||||||
since = "3.7.0",
|
|
||||||
note = "Import will be handled differently in unified structure"
|
|
||||||
)]
|
|
||||||
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
let mut cfg = state.config.write()?;
|
||||||
let count = mcp::import_from_codex(&mut cfg)?;
|
let count = mcp::import_from_codex(&mut cfg)?;
|
||||||
@@ -257,11 +249,7 @@ impl McpService {
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [已废弃] 从 Gemini 导入 MCP(兼容旧 API)
|
/// 从 Gemini 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
#[deprecated(
|
|
||||||
since = "3.7.0",
|
|
||||||
note = "Import will be handled differently in unified structure"
|
|
||||||
)]
|
|
||||||
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
let mut cfg = state.config.write()?;
|
||||||
let count = mcp::import_from_gemini(&mut cfg)?;
|
let count = mcp::import_from_gemini(&mut cfg)?;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::{fs, sync::RwLock};
|
use std::{collections::HashMap, fs, sync::RwLock};
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use cc_switch_lib::{
|
use cc_switch_lib::{
|
||||||
get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError,
|
get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError,
|
||||||
AppState, AppType, McpService, MultiAppConfig,
|
AppState, AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[path = "support.rs"]
|
#[path = "support.rs"]
|
||||||
@@ -126,16 +126,12 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let guard = state.config.read().expect("lock config");
|
let guard = state.config.read().expect("lock config");
|
||||||
let claude_servers = &guard.mcp.claude.servers;
|
// v3.7.0: 检查统一结构
|
||||||
let entry = claude_servers
|
let servers = guard.mcp.servers.as_ref().expect("unified servers should exist");
|
||||||
.get("echo")
|
let entry = servers.get("echo").expect("server imported into unified structure");
|
||||||
.expect("server imported into config.json");
|
|
||||||
assert!(
|
assert!(
|
||||||
entry
|
entry.apps.claude,
|
||||||
.get("enabled")
|
"imported server should have Claude app enabled"
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false),
|
|
||||||
"imported server should be marked enabled"
|
|
||||||
);
|
);
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
@@ -181,43 +177,61 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
|||||||
fn set_mcp_enabled_for_codex_writes_live_config() {
|
fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
reset_test_fs();
|
reset_test_fs();
|
||||||
ensure_test_home();
|
let home = ensure_test_home();
|
||||||
|
|
||||||
|
// 创建 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");
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
let mut config = MultiAppConfig::default();
|
||||||
config.ensure_app(&AppType::Codex);
|
config.ensure_app(&AppType::Codex);
|
||||||
config.mcp.codex.servers.insert(
|
|
||||||
|
// v3.7.0: 使用统一结构
|
||||||
|
config.mcp.servers = Some(HashMap::new());
|
||||||
|
config.mcp.servers.as_mut().unwrap().insert(
|
||||||
"codex-server".into(),
|
"codex-server".into(),
|
||||||
json!({
|
McpServer {
|
||||||
"id": "codex-server",
|
id: "codex-server".to_string(),
|
||||||
"name": "Codex Server",
|
name: "Codex Server".to_string(),
|
||||||
"server": {
|
server: json!({
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "echo"
|
"command": "echo"
|
||||||
|
}),
|
||||||
|
apps: McpApps {
|
||||||
|
claude: false,
|
||||||
|
codex: false, // 初始未启用
|
||||||
|
gemini: false,
|
||||||
},
|
},
|
||||||
"enabled": false
|
description: None,
|
||||||
}),
|
homepage: None,
|
||||||
|
docs: None,
|
||||||
|
tags: Vec::new(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
};
|
};
|
||||||
|
|
||||||
McpService::set_enabled(&state, AppType::Codex, "codex-server", true)
|
// v3.7.0: 使用 toggle_app 替代 set_enabled
|
||||||
.expect("set enabled should succeed");
|
McpService::toggle_app(&state, "codex-server", AppType::Codex, true)
|
||||||
|
.expect("toggle_app should succeed");
|
||||||
|
|
||||||
let guard = state.config.read().expect("lock config");
|
let guard = state.config.read().expect("lock config");
|
||||||
let entry = guard
|
let entry = guard
|
||||||
.mcp
|
.mcp
|
||||||
.codex
|
|
||||||
.servers
|
.servers
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
.get("codex-server")
|
.get("codex-server")
|
||||||
.expect("codex server exists");
|
.expect("codex server exists");
|
||||||
assert!(
|
assert!(
|
||||||
entry
|
entry.apps.codex,
|
||||||
.get("enabled")
|
"server should have Codex app enabled after toggle"
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false),
|
|
||||||
"server should be marked enabled after command"
|
|
||||||
);
|
);
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user