feat(mcp): implement unified MCP management for v3.7.0
BREAKING CHANGE: Migrate from app-specific MCP storage to unified structure ## Phase 1: Data Structure Migration - Add McpApps struct with claude/codex/gemini boolean fields - Add McpServer struct for unified server definition - Add migration logic in MultiAppConfig::migrate_mcp_to_unified() - Integrate automatic migration into MultiAppConfig::load() - Support backward compatibility through Optional fields ## Phase 2: Backend Services Refactor - Completely rewrite services/mcp.rs for unified management: * get_all_servers() - retrieve all MCP servers * upsert_server() - add/update unified server * delete_server() - remove server * toggle_app() - enable/disable server for specific app * sync_all_enabled() - sync to all live configs - Add single-server sync functions to mcp.rs: * sync_single_server_to_claude/codex/gemini() * remove_server_from_claude/codex/gemini() - Add read_mcp_servers_map() to claude_mcp.rs and gemini_mcp.rs - Add new Tauri commands to commands/mcp.rs: * get_mcp_servers, upsert_mcp_server, delete_mcp_server * toggle_mcp_app, sync_all_mcp_servers - Register new commands in lib.rs ## Migration Strategy - Detects old structure (mcp.claude/codex/gemini.servers) - Merges into unified mcp.servers with apps markers - Handles conflicts by merging enabled apps - Clears old structures after migration - Saves migrated config automatically ## Known Issues - Old commands still need compatibility layer (WIP) - toml_edit type conversion needs fixing (WIP) - Frontend not yet implemented (Phase 3 pending) Related: v3.6.2 -> v3.7.0
This commit is contained in:
@@ -2,7 +2,75 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// MCP 配置:单客户端维度(claude 或 codex 下的一组服务器)
|
||||
/// MCP 服务器应用状态(标记应用到哪些客户端)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct McpApps {
|
||||
#[serde(default)]
|
||||
pub claude: bool,
|
||||
#[serde(default)]
|
||||
pub codex: bool,
|
||||
#[serde(default)]
|
||||
pub gemini: bool,
|
||||
}
|
||||
|
||||
impl McpApps {
|
||||
/// 检查指定应用是否启用
|
||||
pub fn is_enabled_for(&self, app: &AppType) -> bool {
|
||||
match app {
|
||||
AppType::Claude => self.claude,
|
||||
AppType::Codex => self.codex,
|
||||
AppType::Gemini => self.gemini,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置指定应用的启用状态
|
||||
pub fn set_enabled_for(&mut self, app: &AppType, enabled: bool) {
|
||||
match app {
|
||||
AppType::Claude => self.claude = enabled,
|
||||
AppType::Codex => self.codex = enabled,
|
||||
AppType::Gemini => self.gemini = enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有启用的应用列表
|
||||
pub fn enabled_apps(&self) -> Vec<AppType> {
|
||||
let mut apps = Vec::new();
|
||||
if self.claude {
|
||||
apps.push(AppType::Claude);
|
||||
}
|
||||
if self.codex {
|
||||
apps.push(AppType::Codex);
|
||||
}
|
||||
if self.gemini {
|
||||
apps.push(AppType::Gemini);
|
||||
}
|
||||
apps
|
||||
}
|
||||
|
||||
/// 检查是否所有应用都未启用
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.claude && !self.codex && !self.gemini
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP 服务器定义(v3.7.0 统一结构)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpServer {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub server: serde_json::Value,
|
||||
pub apps: McpApps,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub homepage: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// MCP 配置:单客户端维度(v3.6.x 及以前,保留用于向后兼容)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct McpConfig {
|
||||
/// 以 id 为键的服务器定义(宽松 JSON 对象,包含 enabled/source 等 UI 辅助字段)
|
||||
@@ -10,15 +78,27 @@ pub struct McpConfig {
|
||||
pub servers: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// MCP 根:按客户端分开维护(无历史兼容压力,直接以 v2 结构落地)
|
||||
impl McpConfig {
|
||||
/// 检查配置是否为空
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.servers.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP 根配置(v3.7.0 新旧结构并存)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct McpRoot {
|
||||
#[serde(default)]
|
||||
/// 统一的 MCP 服务器存储(v3.7.0+)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub servers: Option<HashMap<String, McpServer>>,
|
||||
|
||||
/// 旧的分应用存储(v3.6.x 及以前,保留用于迁移)
|
||||
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
|
||||
pub claude: McpConfig,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
|
||||
pub codex: McpConfig,
|
||||
#[serde(default)]
|
||||
pub gemini: McpConfig, // Gemini MCP 配置(预留)
|
||||
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
|
||||
pub gemini: McpConfig,
|
||||
}
|
||||
|
||||
/// Prompt 配置:单客户端维度
|
||||
@@ -169,6 +249,13 @@ impl MultiAppConfig {
|
||||
.insert("gemini".to_string(), ProviderManager::default());
|
||||
}
|
||||
|
||||
// 执行 MCP 迁移(v3.6.x → v3.7.0)
|
||||
let migrated = config.migrate_mcp_to_unified()?;
|
||||
if migrated {
|
||||
log::info!("MCP 配置已迁移到 v3.7.0 统一结构,保存配置...");
|
||||
config.save()?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -296,6 +383,137 @@ impl MultiAppConfig {
|
||||
log::info!("自动导入完成: {}", app.as_str());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 v3.6.x 的分应用 MCP 结构迁移到 v3.7.0 的统一结构
|
||||
///
|
||||
/// 迁移策略:
|
||||
/// 1. 检查是否已经迁移(mcp.servers 是否存在)
|
||||
/// 2. 收集所有应用的 MCP,按 ID 去重合并
|
||||
/// 3. 生成统一的 McpServer 结构,标记应用到哪些客户端
|
||||
/// 4. 清空旧的分应用配置
|
||||
pub fn migrate_mcp_to_unified(&mut self) -> Result<bool, AppError> {
|
||||
// 检查是否已经是新结构
|
||||
if self.mcp.servers.is_some() {
|
||||
log::debug!("MCP 配置已是统一结构,跳过迁移");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
log::info!("检测到旧版 MCP 配置格式,开始迁移到 v3.7.0 统一结构...");
|
||||
|
||||
let mut unified_servers: HashMap<String, McpServer> = HashMap::new();
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
// 收集所有应用的 MCP
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
let old_servers = match app {
|
||||
AppType::Claude => &self.mcp.claude.servers,
|
||||
AppType::Codex => &self.mcp.codex.servers,
|
||||
AppType::Gemini => &self.mcp.gemini.servers,
|
||||
};
|
||||
|
||||
for (id, entry) in old_servers {
|
||||
let enabled = entry
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
if let Some(existing) = unified_servers.get_mut(id) {
|
||||
// 该 ID 已存在,合并 apps 字段
|
||||
existing.apps.set_enabled_for(&app, enabled);
|
||||
|
||||
// 检测配置冲突(同 ID 但配置不同)
|
||||
if existing.server != *entry.get("server").unwrap_or(&serde_json::json!({})) {
|
||||
conflicts.push(format!(
|
||||
"MCP '{id}' 在 {} 和之前的应用中配置不同,将使用首次遇到的配置",
|
||||
app.as_str()
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// 首次遇到该 MCP,创建新条目
|
||||
let name = entry
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(id)
|
||||
.to_string();
|
||||
|
||||
let server = entry
|
||||
.get("server")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
|
||||
let description = entry
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let homepage = entry
|
||||
.get("homepage")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let docs = entry
|
||||
.get("docs")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let tags = entry
|
||||
.get("tags")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut apps = McpApps::default();
|
||||
apps.set_enabled_for(&app, enabled);
|
||||
|
||||
unified_servers.insert(
|
||||
id.clone(),
|
||||
McpServer {
|
||||
id: id.clone(),
|
||||
name,
|
||||
server,
|
||||
apps,
|
||||
description,
|
||||
homepage,
|
||||
docs,
|
||||
tags,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录冲突警告
|
||||
if !conflicts.is_empty() {
|
||||
log::warn!("MCP 迁移过程中检测到配置冲突:");
|
||||
for conflict in &conflicts {
|
||||
log::warn!(" - {conflict}");
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"MCP 迁移完成,共迁移 {} 个服务器{}",
|
||||
unified_servers.len(),
|
||||
if !conflicts.is_empty() {
|
||||
format!("(存在 {} 个冲突)", conflicts.len())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
);
|
||||
|
||||
// 替换为新结构
|
||||
self.mcp.servers = Some(unified_servers);
|
||||
|
||||
// 清空旧的分应用配置
|
||||
self.mcp.claude = McpConfig::default();
|
||||
self.mcp.codex = McpConfig::default();
|
||||
self.mcp.gemini = McpConfig::default();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -231,6 +231,27 @@ pub fn validate_command_in_path(cmd: &str) -> Result<bool, AppError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 读取 ~/.claude.json 中的 mcpServers 映射
|
||||
pub fn read_mcp_servers_map() -> Result<std::collections::HashMap<String, Value>, AppError> {
|
||||
let path = user_config_path();
|
||||
if !path.exists() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let root = read_json_value(&path)?;
|
||||
let servers = root
|
||||
.get("mcpServers")
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|obj| {
|
||||
obj.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
/// 将给定的启用 MCP 服务器映射写入到用户级 ~/.claude.json 的 mcpServers 字段
|
||||
/// 仅覆盖 mcpServers,其他字段保持不变
|
||||
pub fn set_mcp_servers_map(
|
||||
|
||||
@@ -143,3 +143,53 @@ pub async fn sync_enabled_mcp_to_gemini(state: State<'_, AppState>) -> Result<bo
|
||||
pub async fn import_mcp_from_gemini(state: State<'_, AppState>) -> Result<usize, String> {
|
||||
McpService::import_from_gemini(&state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// v3.7.0 新增:统一 MCP 管理命令
|
||||
// ============================================================================
|
||||
|
||||
use crate::app_config::McpServer;
|
||||
|
||||
/// 获取所有 MCP 服务器(统一结构)
|
||||
#[tauri::command]
|
||||
pub async fn get_mcp_servers(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<HashMap<String, McpServer>, String> {
|
||||
McpService::get_all_servers(&state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 添加或更新 MCP 服务器
|
||||
#[tauri::command]
|
||||
pub async fn upsert_mcp_server(
|
||||
state: State<'_, AppState>,
|
||||
server: McpServer,
|
||||
) -> Result<(), String> {
|
||||
McpService::upsert_server(&state, server).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 删除 MCP 服务器
|
||||
#[tauri::command]
|
||||
pub async fn delete_mcp_server(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<bool, String> {
|
||||
McpService::delete_server(&state, &id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 切换 MCP 服务器在指定应用的启用状态
|
||||
#[tauri::command]
|
||||
pub async fn toggle_mcp_app(
|
||||
state: State<'_, AppState>,
|
||||
server_id: String,
|
||||
app: String,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let app_ty = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||
McpService::toggle_app(&state, &server_id, app_ty, enabled).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 手动同步所有启用的 MCP 服务器到对应的应用
|
||||
#[tauri::command]
|
||||
pub async fn sync_all_mcp_servers(state: State<'_, AppState>) -> Result<(), String> {
|
||||
McpService::sync_all_enabled(&state).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -157,6 +157,27 @@ pub fn delete_mcp_server(id: &str) -> Result<bool, AppError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 读取 Gemini settings.json 中的 mcpServers 映射
|
||||
pub fn read_mcp_servers_map() -> Result<std::collections::HashMap<String, Value>, AppError> {
|
||||
let path = user_config_path();
|
||||
if !path.exists() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let root = read_json_value(&path)?;
|
||||
let servers = root
|
||||
.get("mcpServers")
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|obj| {
|
||||
obj.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
/// 将给定的启用 MCP 服务器映射写入到 Gemini settings.json 的 mcpServers 字段
|
||||
/// 仅覆盖 mcpServers,其他字段保持不变
|
||||
pub fn set_mcp_servers_map(
|
||||
|
||||
@@ -18,14 +18,16 @@ mod settings;
|
||||
mod store;
|
||||
mod usage_script;
|
||||
|
||||
pub use app_config::{AppType, MultiAppConfig};
|
||||
pub use app_config::{AppType, McpServer, MultiAppConfig};
|
||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||
pub use commands::*;
|
||||
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||
pub use error::AppError;
|
||||
pub use mcp::{
|
||||
import_from_claude, import_from_codex, import_from_gemini, sync_enabled_to_claude,
|
||||
sync_enabled_to_codex, sync_enabled_to_gemini,
|
||||
import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude,
|
||||
remove_server_from_codex, remove_server_from_gemini, sync_enabled_to_claude,
|
||||
sync_enabled_to_codex, sync_enabled_to_gemini, sync_single_server_to_claude,
|
||||
sync_single_server_to_codex, sync_single_server_to_gemini,
|
||||
};
|
||||
pub use provider::{Provider, ProviderMeta};
|
||||
pub use services::{
|
||||
@@ -545,6 +547,12 @@ pub fn run() {
|
||||
commands::import_mcp_from_claude,
|
||||
commands::import_mcp_from_codex,
|
||||
commands::import_mcp_from_gemini,
|
||||
// v3.7.0: Unified MCP management
|
||||
commands::get_mcp_servers,
|
||||
commands::upsert_mcp_server,
|
||||
commands::delete_mcp_server,
|
||||
commands::toggle_mcp_app,
|
||||
commands::sync_all_mcp_servers,
|
||||
// Prompt management
|
||||
commands::get_prompts,
|
||||
commands::upsert_prompt,
|
||||
|
||||
@@ -791,3 +791,140 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result<usize, AppError
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// v3.7.0 新增:单个服务器同步和删除函数
|
||||
// ============================================================================
|
||||
|
||||
/// 将单个 MCP 服务器同步到 Claude live 配置
|
||||
pub fn sync_single_server_to_claude(
|
||||
_config: &MultiAppConfig,
|
||||
id: &str,
|
||||
server_spec: &Value,
|
||||
) -> Result<(), AppError> {
|
||||
// 读取现有的 MCP 配置
|
||||
let current = crate::claude_mcp::read_mcp_servers_map()?;
|
||||
|
||||
// 创建新的 HashMap,包含现有的所有服务器 + 当前要同步的服务器
|
||||
let mut updated = current;
|
||||
updated.insert(id.to_string(), server_spec.clone());
|
||||
|
||||
// 写回
|
||||
crate::claude_mcp::set_mcp_servers_map(&updated)
|
||||
}
|
||||
|
||||
/// 从 Claude live 配置中移除单个 MCP 服务器
|
||||
pub fn remove_server_from_claude(id: &str) -> Result<(), AppError> {
|
||||
// 读取现有的 MCP 配置
|
||||
let mut current = crate::claude_mcp::read_mcp_servers_map()?;
|
||||
|
||||
// 移除指定服务器
|
||||
current.remove(id);
|
||||
|
||||
// 写回
|
||||
crate::claude_mcp::set_mcp_servers_map(¤t)
|
||||
}
|
||||
|
||||
/// 将单个 MCP 服务器同步到 Codex live 配置
|
||||
pub fn sync_single_server_to_codex(
|
||||
_config: &MultiAppConfig,
|
||||
id: &str,
|
||||
server_spec: &Value,
|
||||
) -> Result<(), AppError> {
|
||||
// 读取现有的 config.toml
|
||||
let config_path = crate::codex_config::get_codex_config_path()?;
|
||||
|
||||
let mut doc = if config_path.exists() {
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
content
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.map_err(|e| AppError::McpValidation(format!("解析 Codex config.toml 失败: {e}")))?
|
||||
} else {
|
||||
toml_edit::DocumentMut::new()
|
||||
};
|
||||
|
||||
// 确保 [mcp] 表存在
|
||||
if !doc.contains_key("mcp") {
|
||||
doc["mcp"] = toml_edit::table();
|
||||
}
|
||||
|
||||
// 确保 [mcp.servers] 子表存在
|
||||
if !doc["mcp"]
|
||||
.as_table()
|
||||
.and_then(|t| t.get("servers"))
|
||||
.is_some()
|
||||
{
|
||||
doc["mcp"]["servers"] = toml_edit::table();
|
||||
}
|
||||
|
||||
// 将服务器转换为 TOML 格式并插入
|
||||
let toml_value = serde_json::from_value::<toml_edit::Value>(server_spec.clone())
|
||||
.map_err(|e| AppError::McpValidation(format!("无法将 MCP 服务器转换为 TOML: {e}")))?;
|
||||
|
||||
doc["mcp"]["servers"][id] = toml_edit::value(toml_value);
|
||||
|
||||
// 写回文件
|
||||
std::fs::write(&config_path, doc.to_string())
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从 Codex live 配置中移除单个 MCP 服务器
|
||||
pub fn remove_server_from_codex(id: &str) -> Result<(), AppError> {
|
||||
let config_path = crate::codex_config::get_codex_config_path()?;
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(()); // 文件不存在,无需删除
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
|
||||
let mut doc = content
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.map_err(|e| AppError::McpValidation(format!("解析 Codex config.toml 失败: {e}")))?;
|
||||
|
||||
// 从 [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);
|
||||
}
|
||||
}
|
||||
|
||||
// 写回文件
|
||||
std::fs::write(&config_path, doc.to_string())
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将单个 MCP 服务器同步到 Gemini live 配置
|
||||
pub fn sync_single_server_to_gemini(
|
||||
_config: &MultiAppConfig,
|
||||
id: &str,
|
||||
server_spec: &Value,
|
||||
) -> Result<(), AppError> {
|
||||
// 读取现有的 MCP 配置
|
||||
let current = crate::gemini_mcp::read_mcp_servers_map()?;
|
||||
|
||||
// 创建新的 HashMap,包含现有的所有服务器 + 当前要同步的服务器
|
||||
let mut updated = current;
|
||||
updated.insert(id.to_string(), server_spec.clone());
|
||||
|
||||
// 写回
|
||||
crate::gemini_mcp::set_mcp_servers_map(&updated)
|
||||
}
|
||||
|
||||
/// 从 Gemini live 配置中移除单个 MCP 服务器
|
||||
pub fn remove_server_from_gemini(id: &str) -> Result<(), AppError> {
|
||||
// 读取现有的 MCP 配置
|
||||
let mut current = crate::gemini_mcp::read_mcp_servers_map()?;
|
||||
|
||||
// 移除指定服务器
|
||||
current.remove(id);
|
||||
|
||||
// 写回
|
||||
crate::gemini_mcp::set_mcp_servers_map(¤t)
|
||||
}
|
||||
|
||||
@@ -1,213 +1,189 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::app_config::{AppType, MultiAppConfig};
|
||||
use crate::app_config::{AppType, McpServer, MultiAppConfig};
|
||||
use crate::error::AppError;
|
||||
use crate::mcp;
|
||||
use crate::store::AppState;
|
||||
|
||||
/// MCP 相关业务逻辑
|
||||
/// MCP 相关业务逻辑(v3.7.0 统一结构)
|
||||
pub struct McpService;
|
||||
|
||||
impl McpService {
|
||||
/// 获取指定应用的 MCP 服务器快照,并在必要时回写归一化后的配置。
|
||||
pub fn get_servers(state: &AppState, app: AppType) -> Result<HashMap<String, Value>, AppError> {
|
||||
/// 获取所有 MCP 服务器(统一结构)
|
||||
pub fn get_all_servers(state: &AppState) -> Result<HashMap<String, McpServer>, AppError> {
|
||||
let cfg = state.config.read()?;
|
||||
|
||||
// 如果是新结构,直接返回
|
||||
if let Some(servers) = &cfg.mcp.servers {
|
||||
return Ok(servers.clone());
|
||||
}
|
||||
|
||||
// 理论上不应该走到这里,因为 load 时会自动迁移
|
||||
Err(AppError::localized(
|
||||
"mcp.old_structure",
|
||||
"检测到旧版 MCP 结构,请重启应用完成迁移",
|
||||
"Old MCP structure detected, please restart app to complete migration",
|
||||
))
|
||||
}
|
||||
|
||||
/// 添加或更新 MCP 服务器
|
||||
pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
|
||||
{
|
||||
let mut cfg = state.config.write()?;
|
||||
let (snapshot, normalized) = mcp::get_servers_snapshot_for(&mut cfg, &app);
|
||||
drop(cfg);
|
||||
if normalized > 0 {
|
||||
|
||||
// 确保 servers 字段存在
|
||||
if cfg.mcp.servers.is_none() {
|
||||
cfg.mcp.servers = Some(HashMap::new());
|
||||
}
|
||||
|
||||
let servers = cfg.mcp.servers.as_mut().unwrap();
|
||||
let id = server.id.clone();
|
||||
|
||||
// 插入或更新
|
||||
servers.insert(id, server.clone());
|
||||
}
|
||||
|
||||
state.save()?;
|
||||
}
|
||||
Ok(snapshot)
|
||||
|
||||
// 同步到各个启用的应用
|
||||
Self::sync_server_to_apps(state, &server)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 在 config.json 中新增或更新指定 MCP 服务器,并按需同步到对应客户端。
|
||||
pub fn upsert_server(
|
||||
state: &AppState,
|
||||
app: AppType,
|
||||
id: &str,
|
||||
spec: Value,
|
||||
sync_other_side: bool,
|
||||
) -> Result<bool, AppError> {
|
||||
let (changed, snapshot, sync_claude, sync_codex, sync_gemini): (
|
||||
bool,
|
||||
Option<MultiAppConfig>,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
) = {
|
||||
/// 删除 MCP 服务器
|
||||
pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
|
||||
let server = {
|
||||
let mut cfg = state.config.write()?;
|
||||
let changed = mcp::upsert_in_config_for(&mut cfg, &app, id, spec)?;
|
||||
|
||||
// 修复:默认启用(unwrap_or(true))
|
||||
// 新增的 MCP 如果缺少 enabled 字段,应该默认为启用状态
|
||||
let enabled = cfg
|
||||
.mcp_for(&app)
|
||||
.servers
|
||||
.get(id)
|
||||
.and_then(|entry| entry.get("enabled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
let mut sync_claude = matches!(app, AppType::Claude) && enabled;
|
||||
let mut sync_codex = matches!(app, AppType::Codex) && enabled;
|
||||
let mut sync_gemini = matches!(app, AppType::Gemini) && enabled;
|
||||
|
||||
// 修复:sync_other_side=true 时,先将 MCP 复制到另一侧,然后强制同步
|
||||
// 这才是"同步到另一侧"的正确语义:将 MCP 跨应用复制
|
||||
if sync_other_side && app != AppType::Gemini {
|
||||
// Gemini 暂不支持跨应用复制,直接跳过
|
||||
// 获取当前 MCP 条目的克隆(刚刚插入的,不可能失败)
|
||||
let current_entry = cfg
|
||||
.mcp_for(&app)
|
||||
.servers
|
||||
.get(id)
|
||||
.cloned()
|
||||
.expect("刚刚插入的 MCP 条目必定存在");
|
||||
|
||||
// 将该 MCP 复制到另一侧的 servers
|
||||
let other_app = match app {
|
||||
AppType::Claude => AppType::Codex,
|
||||
AppType::Codex => AppType::Claude,
|
||||
AppType::Gemini => unreachable!("Gemini 已在外层 if 中跳过"),
|
||||
};
|
||||
|
||||
cfg.mcp_for_mut(&other_app)
|
||||
.servers
|
||||
.insert(id.to_string(), current_entry);
|
||||
|
||||
// 强制同步另一侧
|
||||
match app {
|
||||
AppType::Claude => sync_codex = true,
|
||||
AppType::Codex => sync_claude = true,
|
||||
AppType::Gemini => unreachable!("Gemini 已在外层 if 中跳过"),
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = if sync_claude || sync_codex || sync_gemini {
|
||||
Some(cfg.clone())
|
||||
if let Some(servers) = &mut cfg.mcp.servers {
|
||||
servers.remove(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
(changed, snapshot, sync_claude, sync_codex, sync_gemini)
|
||||
};
|
||||
|
||||
// 保持原有行为:始终尝试持久化,避免遗漏 normalize 带来的隐式变更
|
||||
if let Some(server) = server {
|
||||
state.save()?;
|
||||
|
||||
if let Some(snapshot) = snapshot {
|
||||
if sync_claude {
|
||||
mcp::sync_enabled_to_claude(&snapshot)?;
|
||||
}
|
||||
if sync_codex {
|
||||
mcp::sync_enabled_to_codex(&snapshot)?;
|
||||
}
|
||||
if sync_gemini {
|
||||
mcp::sync_enabled_to_gemini(&snapshot)?;
|
||||
// 从所有应用的 live 配置中移除
|
||||
Self::remove_server_from_all_apps(state, id, &server)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// 删除 config.json 中的 MCP 服务器条目,并同步客户端配置。
|
||||
pub fn delete_server(state: &AppState, app: AppType, id: &str) -> Result<bool, AppError> {
|
||||
let (existed, snapshot): (bool, Option<MultiAppConfig>) = {
|
||||
let mut cfg = state.config.write()?;
|
||||
let existed = mcp::delete_in_config_for(&mut cfg, &app, id)?;
|
||||
let snapshot = if existed { Some(cfg.clone()) } else { None };
|
||||
(existed, snapshot)
|
||||
};
|
||||
if existed {
|
||||
state.save()?;
|
||||
if let Some(snapshot) = snapshot {
|
||||
match app {
|
||||
AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?,
|
||||
AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?,
|
||||
AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(existed)
|
||||
}
|
||||
|
||||
/// 设置 MCP 启用状态,并同步到客户端配置。
|
||||
pub fn set_enabled(
|
||||
/// 切换指定应用的启用状态
|
||||
pub fn toggle_app(
|
||||
state: &AppState,
|
||||
server_id: &str,
|
||||
app: AppType,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
) -> Result<bool, AppError> {
|
||||
let (existed, snapshot): (bool, Option<MultiAppConfig>) = {
|
||||
) -> Result<(), AppError> {
|
||||
let server = {
|
||||
let mut cfg = state.config.write()?;
|
||||
let existed = mcp::set_enabled_flag_for(&mut cfg, &app, id, enabled)?;
|
||||
let snapshot = if existed { Some(cfg.clone()) } else { None };
|
||||
(existed, snapshot)
|
||||
|
||||
if let Some(servers) = &mut cfg.mcp.servers {
|
||||
if let Some(server) = servers.get_mut(server_id) {
|
||||
server.apps.set_enabled_for(&app, enabled);
|
||||
Some(server.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if existed {
|
||||
if let Some(server) = server {
|
||||
state.save()?;
|
||||
if let Some(snapshot) = snapshot {
|
||||
match app {
|
||||
AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?,
|
||||
AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?,
|
||||
AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?,
|
||||
|
||||
// 同步到对应应用
|
||||
if enabled {
|
||||
Self::sync_server_to_app(state, &server, &app)?;
|
||||
} else {
|
||||
Self::remove_server_from_app(state, server_id, &app)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(existed)
|
||||
}
|
||||
|
||||
/// 手动同步已启用的 MCP 服务器到客户端配置。
|
||||
pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> {
|
||||
let (snapshot, normalized): (MultiAppConfig, usize) = {
|
||||
let mut cfg = state.config.write()?;
|
||||
let normalized = mcp::normalize_servers_for(&mut cfg, &app);
|
||||
(cfg.clone(), normalized)
|
||||
};
|
||||
if normalized > 0 {
|
||||
state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 MCP 服务器同步到所有启用的应用
|
||||
fn sync_server_to_apps(state: &AppState, server: &McpServer) -> Result<(), AppError> {
|
||||
let cfg = state.config.read()?;
|
||||
|
||||
for app in server.apps.enabled_apps() {
|
||||
Self::sync_server_to_app_internal(&cfg, server, &app)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 MCP 服务器同步到指定应用
|
||||
fn sync_server_to_app(
|
||||
state: &AppState,
|
||||
server: &McpServer,
|
||||
app: &AppType,
|
||||
) -> Result<(), AppError> {
|
||||
let cfg = state.config.read()?;
|
||||
Self::sync_server_to_app_internal(&cfg, server, app)
|
||||
}
|
||||
|
||||
fn sync_server_to_app_internal(
|
||||
cfg: &MultiAppConfig,
|
||||
server: &McpServer,
|
||||
app: &AppType,
|
||||
) -> Result<(), AppError> {
|
||||
match app {
|
||||
AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?,
|
||||
AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?,
|
||||
AppType::Gemini => mcp::sync_enabled_to_gemini(&snapshot)?,
|
||||
AppType::Claude => {
|
||||
mcp::sync_single_server_to_claude(cfg, &server.id, &server.server)?;
|
||||
}
|
||||
AppType::Codex => {
|
||||
mcp::sync_single_server_to_codex(cfg, &server.id, &server.server)?;
|
||||
}
|
||||
AppType::Gemini => {
|
||||
mcp::sync_single_server_to_gemini(cfg, &server.id, &server.server)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从 Claude 客户端配置导入 MCP 定义。
|
||||
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let changed = mcp::import_from_claude(&mut cfg)?;
|
||||
drop(cfg);
|
||||
if changed > 0 {
|
||||
state.save()?;
|
||||
/// 从所有曾启用过该服务器的应用中移除
|
||||
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(changed)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从 Codex 客户端配置导入 MCP 定义。
|
||||
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let changed = mcp::import_from_codex(&mut cfg)?;
|
||||
drop(cfg);
|
||||
if changed > 0 {
|
||||
state.save()?;
|
||||
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)?,
|
||||
}
|
||||
Ok(changed)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从 Gemini 客户端配置导入 MCP 定义。
|
||||
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let changed = mcp::import_from_gemini(&mut cfg)?;
|
||||
drop(cfg);
|
||||
if changed > 0 {
|
||||
state.save()?;
|
||||
/// 手动同步所有启用的 MCP 服务器到对应的应用
|
||||
pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> {
|
||||
let servers = Self::get_all_servers(state)?;
|
||||
|
||||
for server in servers.values() {
|
||||
Self::sync_server_to_apps(state, server)?;
|
||||
}
|
||||
Ok(changed)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user