Files
CC-Switch/src-tauri/src/services/mcp.rs
T
Jason 7097a0d710 fix: replace implicit app inference with explicit selection for Skills import and sync
Skills import previously inferred app enablement from filesystem presence,
causing incorrect multi-app activation when the same skill directory existed
under multiple app paths. Now the frontend submits explicit app selections
via ImportSkillSelection, and schema migration preserves a snapshot of
legacy app mappings to avoid lossy reconstruction.

Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks)
and MCP sync_all_enabled (removes disabled servers from live config).
2026-03-14 23:41:36 +08:00

385 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use indexmap::IndexMap;
use std::collections::HashMap;
use crate::app_config::{AppType, McpServer};
use crate::error::AppError;
use crate::mcp;
use crate::store::AppState;
/// MCP 相关业务逻辑(v3.7.0 统一结构)
pub struct McpService;
impl McpService {
/// 获取所有 MCP 服务器(统一结构)
pub fn get_all_servers(state: &AppState) -> Result<IndexMap<String, McpServer>, AppError> {
state.db.get_all_mcp_servers()
}
/// 添加或更新 MCP 服务器
pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
// 读取旧状态:用于处理“编辑时取消勾选某个应用”的场景(需要从对应 live 配置中移除)
let prev_apps = state
.db
.get_all_mcp_servers()?
.get(&server.id)
.map(|s| s.apps.clone())
.unwrap_or_default();
state.db.save_mcp_server(&server)?;
// 处理禁用:若旧版本启用但新版本取消,则需要从该应用的 live 配置移除
if prev_apps.claude && !server.apps.claude {
Self::remove_server_from_app(state, &server.id, &AppType::Claude)?;
}
if prev_apps.codex && !server.apps.codex {
Self::remove_server_from_app(state, &server.id, &AppType::Codex)?;
}
if prev_apps.gemini && !server.apps.gemini {
Self::remove_server_from_app(state, &server.id, &AppType::Gemini)?;
}
if prev_apps.opencode && !server.apps.opencode {
Self::remove_server_from_app(state, &server.id, &AppType::OpenCode)?;
}
// 同步到各个启用的应用
Self::sync_server_to_apps(state, &server)?;
Ok(())
}
/// 删除 MCP 服务器
pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
let server = state.db.get_all_mcp_servers()?.shift_remove(id);
if let Some(server) = server {
state.db.delete_mcp_server(id)?;
// 从所有应用的 live 配置中移除
Self::remove_server_from_all_apps(state, id, &server)?;
Ok(true)
} else {
Ok(false)
}
}
/// 切换指定应用的启用状态
pub fn toggle_app(
state: &AppState,
server_id: &str,
app: AppType,
enabled: bool,
) -> Result<(), AppError> {
let mut servers = state.db.get_all_mcp_servers()?;
if let Some(server) = servers.get_mut(server_id) {
server.apps.set_enabled_for(&app, enabled);
state.db.save_mcp_server(server)?;
// 同步到对应应用
if enabled {
Self::sync_server_to_app(state, server, &app)?;
} else {
Self::remove_server_from_app(state, server_id, &app)?;
}
}
Ok(())
}
/// 将 MCP 服务器同步到所有启用的应用
fn sync_server_to_apps(_state: &AppState, server: &McpServer) -> Result<(), AppError> {
for app in server.apps.enabled_apps() {
Self::sync_server_to_app_no_config(server, &app)?;
}
Ok(())
}
/// 将 MCP 服务器同步到指定应用
fn sync_server_to_app(
_state: &AppState,
server: &McpServer,
app: &AppType,
) -> Result<(), AppError> {
Self::sync_server_to_app_no_config(server, app)
}
fn sync_server_to_app_no_config(server: &McpServer, app: &AppType) -> Result<(), AppError> {
match app {
AppType::Claude => {
mcp::sync_single_server_to_claude(&Default::default(), &server.id, &server.server)?;
}
AppType::Codex => {
// Codex uses TOML format, must use the correct function
mcp::sync_single_server_to_codex(&Default::default(), &server.id, &server.server)?;
}
AppType::Gemini => {
mcp::sync_single_server_to_gemini(&Default::default(), &server.id, &server.server)?;
}
AppType::OpenCode => {
mcp::sync_single_server_to_opencode(
&Default::default(),
&server.id,
&server.server,
)?;
}
AppType::OpenClaw => {
// OpenClaw MCP support is still in development (Issue #4834)
// Skip for now
log::debug!("OpenClaw MCP support is still in development, skipping sync");
}
}
Ok(())
}
/// 从所有曾启用过该服务器的应用中移除
fn remove_server_from_all_apps(
state: &AppState,
id: &str,
server: &McpServer,
) -> Result<(), AppError> {
// 从所有曾启用的应用中移除
for app in server.apps.enabled_apps() {
Self::remove_server_from_app(state, id, &app)?;
}
Ok(())
}
fn remove_server_from_app(_state: &AppState, id: &str, app: &AppType) -> Result<(), AppError> {
match app {
AppType::Claude => mcp::remove_server_from_claude(id)?,
AppType::Codex => mcp::remove_server_from_codex(id)?,
AppType::Gemini => mcp::remove_server_from_gemini(id)?,
AppType::OpenCode => {
mcp::remove_server_from_opencode(id)?;
}
AppType::OpenClaw => {
// OpenClaw MCP support is still in development
log::debug!("OpenClaw MCP support is still in development, skipping remove");
}
}
Ok(())
}
/// 手动同步所有启用的 MCP 服务器到对应的应用
pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> {
let servers = Self::get_all_servers(state)?;
for app in AppType::all() {
if matches!(app, AppType::OpenClaw) {
continue;
}
for server in servers.values() {
if server.apps.is_enabled_for(&app) {
Self::sync_server_to_app(state, server, &app)?;
} else {
Self::remove_server_from_app(state, &server.id, &app)?;
}
}
}
Ok(())
}
// ========================================================================
// 兼容层:支持旧的 v3.6.x 命令(已废弃,将在 v4.0 移除)
// ========================================================================
/// [已废弃] 获取指定应用的 MCP 服务器(兼容旧 API)
#[deprecated(since = "3.7.0", note = "Use get_all_servers instead")]
pub fn get_servers(
state: &AppState,
app: AppType,
) -> Result<HashMap<String, serde_json::Value>, AppError> {
let all_servers = Self::get_all_servers(state)?;
let mut result = HashMap::new();
for (id, server) in all_servers {
if server.apps.is_enabled_for(&app) {
result.insert(id, server.server);
}
}
Ok(result)
}
/// [已废弃] 设置 MCP 服务器在指定应用的启用状态(兼容旧 API)
#[deprecated(since = "3.7.0", note = "Use toggle_app instead")]
pub fn set_enabled(
state: &AppState,
app: AppType,
id: &str,
enabled: bool,
) -> Result<bool, AppError> {
Self::toggle_app(state, id, app, enabled)?;
Ok(true)
}
/// [已废弃] 同步启用的 MCP 到指定应用(兼容旧 API)
#[deprecated(since = "3.7.0", note = "Use sync_all_enabled instead")]
pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> {
let servers = Self::get_all_servers(state)?;
for server in servers.values() {
if server.apps.is_enabled_for(&app) {
Self::sync_server_to_app(state, server, &app)?;
}
}
Ok(())
}
/// 从 Claude 导入 MCPv3.7.0 已更新为统一结构)
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
// 创建临时 MultiAppConfig 用于导入
let mut temp_config = crate::app_config::MultiAppConfig::default();
// 调用原有的导入逻辑(从 mcp.rs)
let count = crate::mcp::import_from_claude(&mut temp_config)?;
let mut new_count = 0;
// 如果有导入的服务器,保存到数据库
if count > 0 {
if let Some(servers) = &temp_config.mcp.servers {
let mut existing = state.db.get_all_mcp_servers()?;
for server in servers.values() {
// 已存在:仅启用 Claude,不覆盖其他字段(与导入模块语义保持一致)
let to_save = if let Some(existing_server) = existing.get(&server.id) {
let mut merged = existing_server.clone();
merged.apps.claude = true;
merged
} else {
// 真正的新服务器
new_count += 1;
server.clone()
};
state.db.save_mcp_server(&to_save)?;
existing.insert(to_save.id.clone(), to_save.clone());
// 同步到对应应用 live 配置
Self::sync_server_to_apps(state, &to_save)?;
}
}
}
Ok(new_count)
}
/// 从 Codex 导入 MCPv3.7.0 已更新为统一结构)
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
// 创建临时 MultiAppConfig 用于导入
let mut temp_config = crate::app_config::MultiAppConfig::default();
// 调用原有的导入逻辑(从 mcp.rs)
let count = crate::mcp::import_from_codex(&mut temp_config)?;
let mut new_count = 0;
// 如果有导入的服务器,保存到数据库
if count > 0 {
if let Some(servers) = &temp_config.mcp.servers {
let mut existing = state.db.get_all_mcp_servers()?;
for server in servers.values() {
// 已存在:仅启用 Codex,不覆盖其他字段(与导入模块语义保持一致)
let to_save = if let Some(existing_server) = existing.get(&server.id) {
let mut merged = existing_server.clone();
merged.apps.codex = true;
merged
} else {
// 真正的新服务器
new_count += 1;
server.clone()
};
state.db.save_mcp_server(&to_save)?;
existing.insert(to_save.id.clone(), to_save.clone());
// 同步到对应应用 live 配置
Self::sync_server_to_apps(state, &to_save)?;
}
}
}
Ok(new_count)
}
/// 从 Gemini 导入 MCPv3.7.0 已更新为统一结构)
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
// 创建临时 MultiAppConfig 用于导入
let mut temp_config = crate::app_config::MultiAppConfig::default();
// 调用原有的导入逻辑(从 mcp.rs)
let count = crate::mcp::import_from_gemini(&mut temp_config)?;
let mut new_count = 0;
// 如果有导入的服务器,保存到数据库
if count > 0 {
if let Some(servers) = &temp_config.mcp.servers {
let mut existing = state.db.get_all_mcp_servers()?;
for server in servers.values() {
// 已存在:仅启用 Gemini,不覆盖其他字段(与导入模块语义保持一致)
let to_save = if let Some(existing_server) = existing.get(&server.id) {
let mut merged = existing_server.clone();
merged.apps.gemini = true;
merged
} else {
// 真正的新服务器
new_count += 1;
server.clone()
};
state.db.save_mcp_server(&to_save)?;
existing.insert(to_save.id.clone(), to_save.clone());
// 同步到对应应用 live 配置
Self::sync_server_to_apps(state, &to_save)?;
}
}
}
Ok(new_count)
}
/// 从 OpenCode 导入 MCPv3.9.2+ 新增)
pub fn import_from_opencode(state: &AppState) -> Result<usize, AppError> {
// 创建临时 MultiAppConfig 用于导入
let mut temp_config = crate::app_config::MultiAppConfig::default();
// 调用原有的导入逻辑(从 mcp/opencode.rs
let count = crate::mcp::import_from_opencode(&mut temp_config)?;
let mut new_count = 0;
// 如果有导入的服务器,保存到数据库
if count > 0 {
if let Some(servers) = &temp_config.mcp.servers {
let mut existing = state.db.get_all_mcp_servers()?;
for server in servers.values() {
// 已存在:仅启用 OpenCode,不覆盖其他字段(与导入模块语义保持一致)
let to_save = if let Some(existing_server) = existing.get(&server.id) {
let mut merged = existing_server.clone();
merged.apps.opencode = true;
merged
} else {
// 真正的新服务器
new_count += 1;
server.clone()
};
state.db.save_mcp_server(&to_save)?;
existing.insert(to_save.id.clone(), to_save.clone());
// 同步到对应应用 live 配置
Self::sync_server_to_apps(state, &to_save)?;
}
}
}
Ok(new_count)
}
}