优化MCP管理,支持所有Agent
This commit is contained in:
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user