mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(mcp): surface per-app failures when importing MCP servers from apps
import_mcp_from_apps swallowed every importer error with unwrap_or(0), so a corrupt config.toml surfaced as "imported 0 servers" with no hint that anything went wrong. Move the aggregation into McpService::import_from_all_apps: each app imports best-effort (one bad file doesn't block the rest), and failures are collected into a single error naming the failing apps alongside the count that did import. The frontend now refreshes the server list on settle rather than success, since a partial failure still means new servers were persisted.
This commit is contained in:
@@ -197,11 +197,5 @@ pub async fn toggle_mcp_app(
|
||||
/// 从所有应用导入 MCP 服务器(复用已有的导入逻辑)
|
||||
#[tauri::command]
|
||||
pub async fn import_mcp_from_apps(state: State<'_, AppState>) -> Result<usize, String> {
|
||||
let mut total = 0;
|
||||
total += McpService::import_from_claude(&state).unwrap_or(0);
|
||||
total += McpService::import_from_codex(&state).unwrap_or(0);
|
||||
total += McpService::import_from_gemini(&state).unwrap_or(0);
|
||||
total += McpService::import_from_opencode(&state).unwrap_or(0);
|
||||
total += McpService::import_from_hermes(&state).unwrap_or(0);
|
||||
Ok(total)
|
||||
McpService::import_from_all_apps(&state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -468,4 +468,41 @@ impl McpService {
|
||||
|
||||
Ok(new_count)
|
||||
}
|
||||
|
||||
/// 从所有支持 MCP 的应用导入服务器,返回新导入的数量。
|
||||
///
|
||||
/// Best-effort:单个应用导入失败(如坏 config.toml)不阻断其余应用;
|
||||
/// 全部跑完后若有失败,聚合成一个错误上报——历史实现逐应用
|
||||
/// `unwrap_or(0)` 吞错,坏文件只会表现为"导入成功 0 个",用户
|
||||
/// 无从得知哪个应用出了问题。
|
||||
pub fn import_from_all_apps(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut total = 0;
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
|
||||
let results: [(&str, Result<usize, AppError>); 5] = [
|
||||
("claude", Self::import_from_claude(state)),
|
||||
("codex", Self::import_from_codex(state)),
|
||||
("gemini", Self::import_from_gemini(state)),
|
||||
("opencode", Self::import_from_opencode(state)),
|
||||
("hermes", Self::import_from_hermes(state)),
|
||||
];
|
||||
for (app, result) in results {
|
||||
match result {
|
||||
Ok(count) => total += count,
|
||||
Err(err) => {
|
||||
log::warn!("从 {app} 导入 MCP 失败: {err}");
|
||||
failures.push(format!("{app}: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(total)
|
||||
} else {
|
||||
Err(AppError::Message(format!(
|
||||
"已导入 {total} 个,部分应用导入失败: {}",
|
||||
failures.join("; ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,56 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
);
|
||||
}
|
||||
|
||||
/// "从应用导入"是 best-effort:单个应用的坏配置文件不阻断其余应用的
|
||||
/// 导入,但失败必须聚合上报——历史实现逐应用 `unwrap_or(0)` 吞错,
|
||||
/// 坏 config.toml 只会表现为"导入成功 0 个",用户无从得知出了什么问题。
|
||||
#[test]
|
||||
fn import_from_all_apps_reports_broken_app_but_imports_the_rest() {
|
||||
use support::create_test_state;
|
||||
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
// 好的 ~/.claude.json:应正常导入
|
||||
let claude_json = json!({
|
||||
"mcpServers": {
|
||||
"alpha": { "type": "stdio", "command": "echo" }
|
||||
}
|
||||
});
|
||||
fs::write(
|
||||
get_claude_mcp_path(),
|
||||
serde_json::to_string_pretty(&claude_json).expect("serialize claude mcp"),
|
||||
)
|
||||
.expect("seed ~/.claude.json");
|
||||
|
||||
// 坏的 ~/.codex/config.toml:解析必然失败
|
||||
let codex_dir = home.join(".codex");
|
||||
fs::create_dir_all(&codex_dir).expect("create codex dir");
|
||||
fs::write(codex_dir.join("config.toml"), "not = = valid toml")
|
||||
.expect("seed broken codex config");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
let err = McpService::import_from_all_apps(&state)
|
||||
.expect_err("broken codex config must surface, not be swallowed as zero imports");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("codex"),
|
||||
"aggregated error should name the failing app, got: {message}"
|
||||
);
|
||||
|
||||
// Codex 的失败不阻断 Claude:alpha 应已入库并启用 Claude
|
||||
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||
let entry = servers
|
||||
.get("alpha")
|
||||
.expect("claude server imported despite codex failure");
|
||||
assert!(
|
||||
entry.apps.claude,
|
||||
"imported server should have Claude app enabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ export function useImportMcpFromApps() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => mcpApi.importFromApps(),
|
||||
onSuccess: () => {
|
||||
// 后端是 best-effort 导入:部分应用失败会返回错误,但其余应用的
|
||||
// 服务器已经入库,失败时也要刷新列表。
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user