优化MCP管理,支持所有Agent

This commit is contained in:
xintaofei
2026-03-31 22:15:48 +08:00
parent fafe970309
commit 296b0c7806
3 changed files with 316 additions and 3 deletions

View File

@@ -42,7 +42,10 @@ fn mcp_network(message: impl Into<String>) -> AppCommandError {
pub enum McpAppType { pub enum McpAppType {
ClaudeCode, ClaudeCode,
Codex, Codex,
Gemini,
OpenClaw,
OpenCode, OpenCode,
Cline,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -350,7 +353,10 @@ pub async fn mcp_upsert_local_server(
let all_apps = [ let all_apps = [
McpAppType::ClaudeCode, McpAppType::ClaudeCode,
McpAppType::Codex, McpAppType::Codex,
McpAppType::Gemini,
McpAppType::OpenClaw,
McpAppType::OpenCode, McpAppType::OpenCode,
McpAppType::Cline,
]; ];
for app in all_apps { for app in all_apps {
@@ -401,7 +407,10 @@ pub async fn mcp_remove_server(
None => vec![ None => vec![
McpAppType::ClaudeCode, McpAppType::ClaudeCode,
McpAppType::Codex, McpAppType::Codex,
McpAppType::Gemini,
McpAppType::OpenClaw,
McpAppType::OpenCode, McpAppType::OpenCode,
McpAppType::Cline,
], ],
}; };
@@ -537,6 +546,24 @@ fn opencode_config_path() -> PathBuf {
.join("config.json") .join("config.json")
} }
fn gemini_config_path() -> PathBuf {
home_dir_or_default().join(".gemini").join("settings.json")
}
fn openclaw_config_path() -> PathBuf {
home_dir_or_default()
.join(".openclaw")
.join("openclaw.json")
}
fn cline_config_path() -> PathBuf {
home_dir_or_default()
.join(".cline")
.join("data")
.join("settings")
.join("cline_mcp_settings.json")
}
fn read_json_file(path: &Path) -> Result<Value, AppCommandError> { fn read_json_file(path: &Path) -> Result<Value, AppCommandError> {
if !path.exists() { if !path.exists() {
return Ok(json!({})); return Ok(json!({}));
@@ -1584,6 +1611,253 @@ fn remove_opencode_server(id: &str) -> Result<bool, AppCommandError> {
Ok(removed) Ok(removed)
} }
// ---------------------------------------------------------------------------
// Gemini CLI (~/.gemini/settings.json → mcpServers)
// ---------------------------------------------------------------------------
fn read_gemini_servers() -> Result<BTreeMap<String, Value>, AppCommandError> {
let path = gemini_config_path();
let root = read_json_file(&path)?;
let mut out = BTreeMap::new();
let Some(servers) = root.get("mcpServers").and_then(Value::as_object) else {
return Ok(out);
};
for (id, spec) in servers {
match canonicalize_spec(spec, "Gemini config") {
Ok(normalized) => {
out.insert(id.to_string(), normalized);
}
Err(err) => {
eprintln!("[MCP] skip invalid Gemini MCP entry id={id}: {err}");
}
}
}
Ok(out)
}
fn upsert_gemini_server(id: &str, spec: &Value) -> Result<(), AppCommandError> {
let path = gemini_config_path();
let mut root = read_json_file(&path)?;
if !root.is_object() {
root = json!({});
}
let canonical = canonicalize_spec(spec, "Gemini write")?;
let obj = root.as_object_mut().ok_or_else(|| {
mcp_configuration_invalid(format!("invalid JSON root in {}", path.display()))
})?;
if !obj.get("mcpServers").map(Value::is_object).unwrap_or(false) {
obj.insert("mcpServers".to_string(), Value::Object(Map::new()));
}
let map = obj
.get_mut("mcpServers")
.and_then(Value::as_object_mut)
.ok_or_else(|| {
mcp_configuration_invalid(format!("invalid mcpServers in {}", path.display()))
})?;
map.insert(id.to_string(), canonical);
write_json_file(&path, &root)
}
fn remove_gemini_server(id: &str) -> Result<bool, AppCommandError> {
let path = gemini_config_path();
if !path.exists() {
return Ok(false);
}
let mut root = read_json_file(&path)?;
let Some(obj) = root.as_object_mut() else {
return Ok(false);
};
let Some(servers) = obj.get_mut("mcpServers").and_then(Value::as_object_mut) else {
return Ok(false);
};
let removed = servers.remove(id).is_some();
if removed {
write_json_file(&path, &root)?;
}
Ok(removed)
}
// ---------------------------------------------------------------------------
// OpenClaw (~/.openclaw/openclaw.json → mcp.servers)
// ---------------------------------------------------------------------------
fn read_openclaw_servers() -> Result<BTreeMap<String, Value>, AppCommandError> {
let path = openclaw_config_path();
let root = read_json_file(&path)?;
let mut out = BTreeMap::new();
let Some(mcp) = root.get("mcp").and_then(Value::as_object) else {
return Ok(out);
};
let Some(servers) = mcp.get("servers").and_then(Value::as_object) else {
return Ok(out);
};
for (id, spec) in servers {
match canonicalize_spec(spec, "OpenClaw config") {
Ok(normalized) => {
out.insert(id.to_string(), normalized);
}
Err(err) => {
eprintln!("[MCP] skip invalid OpenClaw MCP entry id={id}: {err}");
}
}
}
Ok(out)
}
fn upsert_openclaw_server(id: &str, spec: &Value) -> Result<(), AppCommandError> {
let path = openclaw_config_path();
let mut root = read_json_file(&path)?;
if !root.is_object() {
root = json!({});
}
let canonical = canonicalize_spec(spec, "OpenClaw write")?;
let obj = root.as_object_mut().ok_or_else(|| {
mcp_configuration_invalid(format!("invalid JSON root in {}", path.display()))
})?;
if !obj.get("mcp").map(Value::is_object).unwrap_or(false) {
obj.insert("mcp".to_string(), json!({}));
}
let mcp = obj
.get_mut("mcp")
.and_then(Value::as_object_mut)
.ok_or_else(|| {
mcp_configuration_invalid(format!("invalid mcp in {}", path.display()))
})?;
if !mcp.get("servers").map(Value::is_object).unwrap_or(false) {
mcp.insert("servers".to_string(), Value::Object(Map::new()));
}
let servers = mcp
.get_mut("servers")
.and_then(Value::as_object_mut)
.ok_or_else(|| {
mcp_configuration_invalid(format!("invalid mcp.servers in {}", path.display()))
})?;
servers.insert(id.to_string(), canonical);
write_json_file(&path, &root)
}
fn remove_openclaw_server(id: &str) -> Result<bool, AppCommandError> {
let path = openclaw_config_path();
if !path.exists() {
return Ok(false);
}
let mut root = read_json_file(&path)?;
let Some(obj) = root.as_object_mut() else {
return Ok(false);
};
let Some(mcp) = obj.get_mut("mcp").and_then(Value::as_object_mut) else {
return Ok(false);
};
let Some(servers) = mcp.get_mut("servers").and_then(Value::as_object_mut) else {
return Ok(false);
};
let removed = servers.remove(id).is_some();
if removed {
if servers.is_empty() {
mcp.remove("servers");
}
if mcp.is_empty() {
obj.remove("mcp");
}
write_json_file(&path, &root)?;
}
Ok(removed)
}
// ---------------------------------------------------------------------------
// Cline (~/.cline/data/settings/cline_mcp_settings.json → mcpServers)
// ---------------------------------------------------------------------------
fn read_cline_servers() -> Result<BTreeMap<String, Value>, AppCommandError> {
let path = cline_config_path();
let root = read_json_file(&path)?;
let mut out = BTreeMap::new();
let Some(servers) = root.get("mcpServers").and_then(Value::as_object) else {
return Ok(out);
};
for (id, spec) in servers {
match canonicalize_spec(spec, "Cline config") {
Ok(normalized) => {
out.insert(id.to_string(), normalized);
}
Err(err) => {
eprintln!("[MCP] skip invalid Cline MCP entry id={id}: {err}");
}
}
}
Ok(out)
}
fn upsert_cline_server(id: &str, spec: &Value) -> Result<(), AppCommandError> {
let path = cline_config_path();
let mut root = read_json_file(&path)?;
if !root.is_object() {
root = json!({});
}
let canonical = canonicalize_spec(spec, "Cline write")?;
let obj = root.as_object_mut().ok_or_else(|| {
mcp_configuration_invalid(format!("invalid JSON root in {}", path.display()))
})?;
if !obj.get("mcpServers").map(Value::is_object).unwrap_or(false) {
obj.insert("mcpServers".to_string(), Value::Object(Map::new()));
}
let map = obj
.get_mut("mcpServers")
.and_then(Value::as_object_mut)
.ok_or_else(|| {
mcp_configuration_invalid(format!("invalid mcpServers in {}", path.display()))
})?;
map.insert(id.to_string(), canonical);
write_json_file(&path, &root)
}
fn remove_cline_server(id: &str) -> Result<bool, AppCommandError> {
let path = cline_config_path();
if !path.exists() {
return Ok(false);
}
let mut root = read_json_file(&path)?;
let Some(obj) = root.as_object_mut() else {
return Ok(false);
};
let Some(servers) = obj.get_mut("mcpServers").and_then(Value::as_object_mut) else {
return Ok(false);
};
let removed = servers.remove(id).is_some();
if removed {
write_json_file(&path, &root)?;
}
Ok(removed)
}
fn scan_local_servers() -> Result<Vec<LocalMcpServer>, AppCommandError> { fn scan_local_servers() -> Result<Vec<LocalMcpServer>, AppCommandError> {
let mut merged: BTreeMap<String, (Value, BTreeSet<McpAppType>)> = BTreeMap::new(); let mut merged: BTreeMap<String, (Value, BTreeSet<McpAppType>)> = BTreeMap::new();
@@ -1608,6 +1882,27 @@ fn scan_local_servers() -> Result<Vec<LocalMcpServer>, AppCommandError> {
entry.1.insert(McpAppType::OpenCode); entry.1.insert(McpAppType::OpenCode);
} }
for (id, spec) in read_gemini_servers()? {
let entry = merged
.entry(id)
.or_insert_with(|| (spec.clone(), BTreeSet::new()));
entry.1.insert(McpAppType::Gemini);
}
for (id, spec) in read_openclaw_servers()? {
let entry = merged
.entry(id)
.or_insert_with(|| (spec.clone(), BTreeSet::new()));
entry.1.insert(McpAppType::OpenClaw);
}
for (id, spec) in read_cline_servers()? {
let entry = merged
.entry(id)
.or_insert_with(|| (spec.clone(), BTreeSet::new()));
entry.1.insert(McpAppType::Cline);
}
Ok(merged Ok(merged
.into_iter() .into_iter()
.map(|(id, (spec, apps))| LocalMcpServer { .map(|(id, (spec, apps))| LocalMcpServer {
@@ -1628,6 +1923,9 @@ fn upsert_server_for_app(app: McpAppType, id: &str, spec: &Value) -> Result<(),
McpAppType::ClaudeCode => upsert_claude_server(id, spec), McpAppType::ClaudeCode => upsert_claude_server(id, spec),
McpAppType::Codex => upsert_codex_server(id, spec), McpAppType::Codex => upsert_codex_server(id, spec),
McpAppType::OpenCode => upsert_opencode_server(id, spec), McpAppType::OpenCode => upsert_opencode_server(id, spec),
McpAppType::Gemini => upsert_gemini_server(id, spec),
McpAppType::OpenClaw => upsert_openclaw_server(id, spec),
McpAppType::Cline => upsert_cline_server(id, spec),
} }
} }
@@ -1636,6 +1934,9 @@ fn remove_server_for_app(app: McpAppType, id: &str) -> Result<bool, AppCommandEr
McpAppType::ClaudeCode => remove_claude_server(id), McpAppType::ClaudeCode => remove_claude_server(id),
McpAppType::Codex => remove_codex_server(id), McpAppType::Codex => remove_codex_server(id),
McpAppType::OpenCode => remove_opencode_server(id), McpAppType::OpenCode => remove_opencode_server(id),
McpAppType::Gemini => remove_gemini_server(id),
McpAppType::OpenClaw => remove_openclaw_server(id),
McpAppType::Cline => remove_cline_server(id),
} }
} }

View File

@@ -69,9 +69,12 @@ type McpTranslator = (
) => string ) => string
const APP_OPTIONS: { value: McpAppType; label: string }[] = [ const APP_OPTIONS: { value: McpAppType; label: string }[] = [
{ value: "claude_code", label: "Claude" }, { value: "claude_code", label: "Claude Code" },
{ value: "codex", label: "Codex" }, { value: "codex", label: "Codex CLI" },
{ value: "gemini", label: "Gemini CLI" },
{ value: "open_claw", label: "OpenClaw" },
{ value: "open_code", label: "OpenCode" }, { value: "open_code", label: "OpenCode" },
{ value: "cline", label: "Cline" },
] ]
function isObject(value: unknown): value is Record<string, unknown> { function isObject(value: unknown): value is Record<string, unknown> {
@@ -228,7 +231,10 @@ function appsToDraft(apps: McpAppType[]): Record<McpAppType, boolean> {
return { return {
claude_code: appSet.has("claude_code"), claude_code: appSet.has("claude_code"),
codex: appSet.has("codex"), codex: appSet.has("codex"),
gemini: appSet.has("gemini"),
open_claw: appSet.has("open_claw"),
open_code: appSet.has("open_code"), open_code: appSet.has("open_code"),
cline: appSet.has("cline"),
} }
} }

View File

@@ -573,7 +573,13 @@ export interface GitHubTokenValidation {
message: string | null message: string | null
} }
export type McpAppType = "claude_code" | "codex" | "open_code" export type McpAppType =
| "claude_code"
| "codex"
| "gemini"
| "open_claw"
| "open_code"
| "cline"
export interface LocalMcpServer { export interface LocalMcpServer {
id: string id: string