Files
CC-Switch/src-tauri/src/gemini_mcp.rs
T
Jason fa33330b3b fix(mcp): improve upsert and import robustness
- Remove server from live config when app is disabled during upsert
- Merge enabled flags instead of overwriting when importing from multiple apps
- Normalize Gemini MCP type field (url-only → sse, command → stdio)
- Use atomic write for Codex config updates
- Add tests for disable-removal, multi-app merge, and Gemini SSE import
2025-12-18 15:14:37 +08:00

150 lines
5.3 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 serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::fs;
use std::path::{Path, PathBuf};
use crate::config::atomic_write;
use crate::error::AppError;
use crate::gemini_config::get_gemini_settings_path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpStatus {
pub user_config_path: String,
pub user_config_exists: bool,
pub server_count: usize,
}
/// 获取 Gemini MCP 配置文件路径(~/.gemini/settings.json
fn user_config_path() -> PathBuf {
get_gemini_settings_path()
}
fn read_json_value(path: &Path) -> Result<Value, AppError> {
if !path.exists() {
return Ok(serde_json::json!({}));
}
let content = fs::read_to_string(path).map_err(|e| AppError::io(path, e))?;
let value: Value = serde_json::from_str(&content).map_err(|e| AppError::json(path, e))?;
Ok(value)
}
fn write_json_value(path: &Path, value: &Value) -> Result<(), AppError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let json =
serde_json::to_string_pretty(value).map_err(|e| AppError::JsonSerialize { source: e })?;
atomic_write(path, json.as_bytes())
}
/// 读取 Gemini settings.json 中的 mcpServers 映射
///
/// 执行反向格式转换以保持与统一 MCP 结构的兼容性:
/// - httpUrl → url + type: "http"
/// - 仅有 url 字段 → 补齐 type: "sse"Gemini 以字段名推断传输类型)
/// - 仅有 command 字段 → 补齐 type: "stdio"
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 mut servers: std::collections::HashMap<String, Value> = root
.get("mcpServers")
.and_then(|v| v.as_object())
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
// 反向格式转换:Gemini 特有格式 → 统一 MCP 格式
for (_, spec) in servers.iter_mut() {
if let Some(obj) = spec.as_object_mut() {
// httpUrl → url + type: "http"
if let Some(http_url) = obj.remove("httpUrl") {
obj.insert("url".to_string(), http_url);
obj.insert("type".to_string(), Value::String("http".to_string()));
}
// Gemini CLI 不使用 type 字段:这里补齐成统一结构,便于校验与导入
if obj.get("type").is_none() {
if obj.contains_key("command") {
obj.insert("type".to_string(), Value::String("stdio".to_string()));
} else if obj.contains_key("url") {
obj.insert("type".to_string(), Value::String("sse".to_string()));
}
}
}
}
Ok(servers)
}
/// 将给定的启用 MCP 服务器映射写入到 Gemini settings.json 的 mcpServers 字段
/// 仅覆盖 mcpServers,其他字段保持不变
pub fn set_mcp_servers_map(
servers: &std::collections::HashMap<String, Value>,
) -> Result<(), AppError> {
let path = user_config_path();
let mut root = if path.exists() {
read_json_value(&path)?
} else {
serde_json::json!({})
};
// 构建 mcpServers 对象:移除 UI 辅助字段(enabled/source),仅保留实际 MCP 规范
let mut out: Map<String, Value> = Map::new();
for (id, spec) in servers.iter() {
let mut obj = if let Some(map) = spec.as_object() {
map.clone()
} else {
return Err(AppError::McpValidation(format!(
"MCP 服务器 '{id}' 不是对象"
)));
};
// 提取 server 字段(如果存在)
if let Some(server_val) = obj.remove("server") {
let server_obj = server_val.as_object().cloned().ok_or_else(|| {
AppError::McpValidation(format!("MCP 服务器 '{id}' server 字段不是对象"))
})?;
obj = server_obj;
}
// Gemini CLI 格式转换:
// - Gemini 不使用 "type" 字段(从字段名推断传输类型)
// - HTTP 使用 "httpUrl" 字段,SSE 使用 "url" 字段
let transport_type = obj.get("type").and_then(|v| v.as_str());
if transport_type == Some("http") {
// HTTP streaming: 将 "url" 重命名为 "httpUrl"
if let Some(url_value) = obj.remove("url") {
obj.insert("httpUrl".to_string(), url_value);
}
}
// SSE 保持 "url" 字段不变
// 移除 UI 辅助字段和 type 字段(Gemini 不需要)
obj.remove("type");
obj.remove("enabled");
obj.remove("source");
obj.remove("id");
obj.remove("name");
obj.remove("description");
obj.remove("tags");
obj.remove("homepage");
obj.remove("docs");
out.insert(id.clone(), Value::Object(obj));
}
{
let obj = root
.as_object_mut()
.ok_or_else(|| AppError::Config("~/.gemini/settings.json 根必须是对象".into()))?;
obj.insert("mcpServers".into(), Value::Object(out));
}
write_json_value(&path, &root)?;
Ok(())
}