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,
}
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())
}
pub fn read_mcp_json() -> Result<Option<String>, AppError> {
let path = user_config_path();
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?;
Ok(Some(content))
}
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> = std::collections::HashMap::new();
let Some(obj) = root.get("mcpServers").and_then(|v| v.as_object()) else {
return Ok(servers);
};
for (id, raw_spec) in obj {
let mut spec = raw_spec.clone();
if let Some(spec_obj) = spec.as_object_mut() {
if let Some(http_url_value) = spec_obj.remove("httpUrl") {
spec_obj.insert("url".to_string(), http_url_value);
spec_obj.insert("type".to_string(), Value::String("http".to_string()));
}
let has_type = spec_obj.get("type").and_then(|v| v.as_str()).is_some();
if !has_type {
if spec_obj.get("command").and_then(|v| v.as_str()).is_some() {
spec_obj.insert("type".to_string(), Value::String("stdio".to_string()));
} else if spec_obj.get("url").and_then(|v| v.as_str()).is_some() {
spec_obj.insert("type".to_string(), Value::String("sse".to_string()));
}
}
}
servers.insert(id.clone(), spec);
}
Ok(servers)
}
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!({})
};
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}' 不是对象"
)));
};
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;
}
let transport_type = obj.get("type").and_then(|v| v.as_str());
if transport_type == Some("http") {
if let Some(url_value) = obj.remove("url") {
obj.insert("httpUrl".to_string(), url_value);
}
}
const DEFAULT_STARTUP_MS: u64 = 10_000;
const DEFAULT_TOOL_MS: u64 = 60_000;
let existing_timeout_ms = obj
.get("timeout")
.and_then(|val| val.as_u64().or_else(|| val.as_f64().map(|f| f as u64)));
let extract_timeout =
|obj: &mut Map<String, Value>, key: &str, multiplier: u64| -> Option<u64> {
obj.remove(key).and_then(|val| {
val.as_u64()
.map(|n| n.saturating_mul(multiplier))
.or_else(|| val.as_f64().map(|f| (f * multiplier as f64) as u64))
})
};
let startup_ms = extract_timeout(&mut obj, "startup_timeout_sec", 1000)
.or_else(|| extract_timeout(&mut obj, "startup_timeout_ms", 1))
.unwrap_or(DEFAULT_STARTUP_MS);
let tool_ms = extract_timeout(&mut obj, "tool_timeout_sec", 1000)
.or_else(|| extract_timeout(&mut obj, "tool_timeout_ms", 1))
.unwrap_or(DEFAULT_TOOL_MS);
let derived_timeout_ms = startup_ms.max(tool_ms);
let final_timeout_ms = existing_timeout_ms.unwrap_or(0).max(derived_timeout_ms);
obj.insert(
"timeout".to_string(),
Value::Number(final_timeout_ms.into()),
);
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(())
}