fix(mcp): stop cross-app failures from blocking MCP re-projection

sync_all_enabled iterated AppType::all() with `?`, so one app's corrupt
live file (e.g. a broken ~/.claude.json, which passes the existence
gate but fails to parse) blocked every app behind it in the iteration
order -- and bubbled the error into whatever operation triggered the
sync:

- switch/save had just rewritten only the target app's live file, yet a
  broken unrelated file failed the whole operation after DB and live
  were already updated, reporting a false "switch failed" to the user.
  Both now project only the target app (sync_enabled_for_app) and
  degrade projection failure to a warning: the primary operation has
  already taken effect, and the projection self-heals on the next
  switch or MCP toggle.
- sync_current_provider_for_app_to_live syncs a single app; it now
  projects that app only, keeping failures (which can only concern the
  target app) as errors.
- sync_current_to_live (config import / cloud-sync restore) keeps the
  all-apps sweep, but sync_all_enabled is now best-effort: it projects
  every app, collects failures, and reports them aggregated. Its error
  is held until after skill sync so an MCP failure no longer skips it.
This commit is contained in:
Jason
2026-07-08 22:21:18 +08:00
parent 1f36f0cfed
commit 11c173c730
4 changed files with 192 additions and 15 deletions
+21 -6
View File
@@ -176,21 +176,36 @@ impl McpService {
Ok(())
}
/// 手动同步所有启用的 MCP 服务器到对应的应用
/// 手动同步所有启用的 MCP 服务器到对应的应用
///
/// Best-effort:单个应用投影失败(如 ~/.claude.json 坏 JSON)不阻断
/// 其余应用——各应用的 live 文件互相独立,一处损坏没有理由让其他
/// 应用的 MCP 状态陈旧。全部跑完后若有失败,聚合成一个错误上报,
/// 保留调用方的可见性。
pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> {
let servers = Self::get_all_servers(state)?;
let mut failures: Vec<String> = Vec::new();
for app in AppType::all() {
Self::project_servers_to_app(state, &servers, &app)?;
if let Err(err) = Self::project_servers_to_app(state, &servers, &app) {
log::warn!("同步 MCP 到 {app:?} 失败: {err}");
failures.push(format!("{}: {err}", app.as_str()));
}
}
Ok(())
if failures.is_empty() {
Ok(())
} else {
Err(AppError::Message(format!(
"部分应用 MCP 同步失败: {}",
failures.join("; ")
)))
}
}
/// 只把启用状态投影到单个应用。某个应用的 live 被整体重写后用它做
/// 定向重投影sync_all_enabled 按 AppType::all() 顺序逐应用短路,
/// 排在前面的无关应用 live 损坏(如 ~/.claude.json 坏 JSON)会阻断
/// 后面应用的投影。
/// 定向重投影,避免把无关应用的失败面(如 ~/.claude.json 坏 JSON
/// 牵连进目标应用的关键路径。
pub fn sync_enabled_for_app(state: &AppState, app: &AppType) -> Result<(), AppError> {
let servers = Self::get_all_servers(state)?;
Self::project_servers_to_app(state, &servers, app)
+9 -4
View File
@@ -948,7 +948,10 @@ pub(crate) fn sync_current_provider_for_app_to_live(
}
}
McpService::sync_all_enabled(state)?;
// 本函数语义是"把这个应用同步到 live",MCP 重投影也只针对该应用;
// 全量 sync_all_enabled 会把无关应用的 live 损坏牵连进来。投影失败
// 上抛(不降级):这里没有已变更的 DB 状态需要保护,调用方重试即可。
McpService::sync_enabled_for_app(state, app_type)?;
Ok(())
}
@@ -1016,8 +1019,10 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
}
}
// MCP sync
McpService::sync_all_enabled(state)?;
// MCP syncbest-effort 逐应用投影,内部已聚合失败)。错误暂存到
// Skill 同步之后再返回:MCP 的失败不该跳过 Skill 同步,但调用方
//(配置导入 / 云同步恢复)需要知道结果不完整。
let mcp_result = McpService::sync_all_enabled(state);
// Skill sync
for app_type in AppType::all() {
@@ -1027,7 +1032,7 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
}
}
Ok(())
mcp_result
}
/// Read current live settings for an app type
+20 -4
View File
@@ -2134,8 +2134,16 @@ impl ProviderService {
}
} else {
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
// Sync MCP
McpService::sync_all_enabled(state)?;
// 重写 live 后只重投影本应用的 MCP:全量 sync_all_enabled 会把
// 无关应用的 live 损坏(如 ~/.claude.json 坏 JSON)牵连进保存
// 流程。走到这里 DB 与 live 都已按新配置落盘,保存事实上已
// 成功;投影失败降级为警告,避免制造"保存失败"假象(MCP
// 投影可自愈:下次切换 / 任一 MCP 启停都会重新投影)。
if let Err(err) = McpService::sync_enabled_for_app(state, &app_type) {
log::warn!(
"保存供应商后重投影 {app_type:?} MCP 失败(将在下次同步时自愈): {err}"
);
}
}
}
@@ -2509,8 +2517,16 @@ impl ProviderService {
}
}
// Sync MCP
McpService::sync_all_enabled(state)?;
// 切换重写了目标应用的 live,只重投影该应用的 MCP(Codex 的
// [mcp_servers] 与 live 同文件,整体替换后必须补回;其余应用的
// MCP 文件独立于 live,投影是幂等维护)。不用全量 sync_all_enabled
// 无关应用的 live 损坏(如 ~/.claude.json 坏 JSON)不该阻断切换。
// 走到这里 DB is_current 与 live 都已落盘,切换事实上已成功;
// 投影失败上抛会让前端报"切换失败"制造分裂假象,故降级为警告
// (MCP 投影可自愈:下次切换 / 任一 MCP 启停都会重新投影)。
if let Err(err) = McpService::sync_enabled_for_app(state, &app_type) {
log::warn!("切换供应商后重投影 {app_type:?} MCP 失败(将在下次同步时自愈): {err}");
}
Ok(result)
}