use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::server_config::{McpServerConfig, TransportConfig};
use echo_core::error::{McpError, ReactError, Result};
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct McpConfigFile {
#[serde(rename = "mcpServers", default)]
pub mcp_servers: HashMap<String, McpServerEntry>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct McpServerEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport: Option<String>,
#[serde(default)]
pub disabled: bool,
}
impl McpServerEntry {
pub fn to_server_config(&self, name: &str) -> Result<McpServerConfig> {
if self.disabled {
return Err(ReactError::Mcp(McpError::ConnectionFailed(format!(
"服务端 '{}' 已禁用(disabled: true)",
name
))));
}
if let Some(command) = &self.command {
let env: Vec<(String, String)> = self
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Ok(McpServerConfig {
name: name.to_string(),
transport: TransportConfig::Stdio {
command: command.clone(),
args: self.args.clone(),
env,
},
})
} else if let Some(url) = &self.url {
let transport = match self.transport.as_deref() {
Some("sse") => TransportConfig::Sse {
base_url: url.clone(),
headers: self.headers.clone(),
},
_ => TransportConfig::Http {
base_url: url.clone(),
headers: self.headers.clone(),
},
};
Ok(McpServerConfig {
name: name.to_string(),
transport,
})
} else {
Err(ReactError::Mcp(McpError::ConnectionFailed(format!(
"服务端 '{}' 配置无效:stdio 模式需提供 'command',HTTP 模式需提供 'url'",
name
))))
}
}
}
impl McpConfigFile {
pub fn parse(s: &str) -> Result<Self> {
serde_json::from_str(s).map_err(|e| {
ReactError::Mcp(McpError::ProtocolError(format!(
"mcp.json 格式解析失败: {}",
e
)))
})
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path).map_err(|e| {
ReactError::Mcp(McpError::ConnectionFailed(format!(
"读取配置文件失败 ({}): {}",
path.display(),
e
)))
})?;
Self::parse(&content)
}
pub fn to_server_configs(&self) -> Result<Vec<McpServerConfig>> {
let mut configs = Vec::new();
for (name, entry) in &self.mcp_servers {
if entry.disabled {
tracing::debug!("MCP: 跳过已禁用的服务端 '{}'", name);
continue;
}
configs.push(entry.to_server_config(name)?);
}
Ok(configs)
}
pub fn enabled_count(&self) -> usize {
self.mcp_servers.values().filter(|e| !e.disabled).count()
}
}