use anyhow::{Result, bail};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use super::args::ConvertArgs;
#[derive(Debug, Clone)]
pub enum McpConfigSource {
DirectUrl { url: String },
RemoteService {
name: String,
url: String,
protocol: Option<crate::client::protocol::McpProtocol>,
headers: HashMap<String, String>,
timeout: Option<u64>,
},
LocalCommand {
name: String,
command: String,
args: Vec<String>,
env: HashMap<String, String>,
},
}
#[derive(Deserialize, Debug)]
struct McpConfig {
#[serde(rename = "mcpServers")]
mcp_servers: HashMap<String, McpServerInnerConfig>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum McpServerInnerConfig {
Command(StdioConfig),
Url(UrlConfig),
}
#[derive(Deserialize, Debug, Clone)]
struct StdioConfig {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Debug, Clone)]
struct UrlConfig {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
rename = "baseUrl",
alias = "baseurl",
alias = "base_url"
)]
base_url: Option<String>,
#[serde(default, rename = "type", alias = "Type")]
r#type: Option<String>,
pub headers: Option<HashMap<String, String>>,
#[serde(default, alias = "authToken", alias = "auth_token")]
pub auth_token: Option<String>,
pub timeout: Option<u64>,
}
impl UrlConfig {
fn get_url(&self) -> Option<&str> {
self.url.as_deref().or(self.base_url.as_deref())
}
}
pub fn parse_convert_config(args: &ConvertArgs) -> Result<McpConfigSource> {
if let Some(ref url) = args.url {
return Ok(McpConfigSource::DirectUrl { url: url.clone() });
}
let json_str = if let Some(ref config) = args.config {
config.clone()
} else if let Some(ref path) = args.config_file {
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("读取配置文件失败: {}", e))?
} else {
bail!("必须提供 URL、--config 或 --config-file 参数之一");
};
let mcp_config: McpConfig = serde_json::from_str(&json_str).map_err(|e| {
anyhow::anyhow!(
"配置解析失败: {}。配置必须是标准 MCP 格式,包含 mcpServers 字段",
e
)
})?;
let servers = mcp_config.mcp_servers;
if servers.is_empty() {
bail!("配置中没有找到任何 MCP 服务");
}
let (name, inner_config) = if let Some(ref name) = args.name {
let config = servers.get(name).cloned().ok_or_else(|| {
anyhow::anyhow!(
"服务 '{}' 不存在。可用服务: {:?}",
name,
servers.keys().collect::<Vec<_>>()
)
})?;
(name.clone(), config)
} else if servers.len() == 1 {
servers
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("配置中没有找到任何 MCP 服务"))?
} else {
bail!(
"配置包含多个服务 {:?},请使用 --name 指定要使用的服务",
servers.keys().collect::<Vec<_>>()
);
};
match inner_config {
McpServerInnerConfig::Command(stdio) => Ok(McpConfigSource::LocalCommand {
name,
command: stdio.command,
args: stdio.args.unwrap_or_default(),
env: stdio.env.unwrap_or_default(),
}),
McpServerInnerConfig::Url(url_config) => {
let url = url_config
.get_url()
.ok_or_else(|| anyhow::anyhow!("URL 配置缺少 url 或 baseUrl 字段"))?
.to_string();
let protocol =
url_config
.r#type
.as_ref()
.and_then(|t| match t.to_ascii_lowercase().as_str() {
"sse" => Some(crate::client::protocol::McpProtocol::Sse),
"http" | "stream" | "streamablehttp" | "streamable-http"
| "streamable_http" => Some(crate::client::protocol::McpProtocol::Stream),
_ => None,
});
let headers = merge_config_headers_checked(
url_config.headers.clone().unwrap_or_default(),
url_config.auth_token.as_deref(),
)?;
Ok(McpConfigSource::RemoteService {
name,
url,
protocol,
headers,
timeout: url_config.timeout,
})
}
}
}
pub fn merge_headers(
config_headers: HashMap<String, String>,
cli_headers: &[(String, String)],
cli_auth: Option<&String>,
) -> HashMap<String, String> {
let mut config_entries = config_headers.into_iter().collect::<Vec<_>>();
config_entries.sort_by(|(left, _), (right, _)| {
left.to_ascii_lowercase()
.cmp(&right.to_ascii_lowercase())
.then_with(|| left.cmp(right))
});
let mut merged = HashMap::new();
for (key, value) in config_entries {
insert_header_case_insensitive(&mut merged, key, value);
}
for (key, value) in cli_headers {
insert_header_case_insensitive(&mut merged, key.clone(), value.clone());
}
if let Some(auth_value) = cli_auth {
insert_header_case_insensitive(
&mut merged,
"Authorization".to_string(),
auth_value.clone(),
);
}
merged
}
pub(crate) fn merge_headers_checked(
config_headers: HashMap<String, String>,
cli_headers: &[(String, String)],
cli_auth: Option<&String>,
) -> Result<HashMap<String, String>> {
validate_unique_header_names(
"configured headers",
config_headers.keys().map(String::as_str),
)?;
validate_unique_header_names(
"CLI headers",
cli_headers.iter().map(|(name, _)| name.as_str()),
)?;
Ok(merge_headers(config_headers, cli_headers, cli_auth))
}
pub(crate) fn merge_config_headers_checked(
config_headers: HashMap<String, String>,
auth_token: Option<&str>,
) -> Result<HashMap<String, String>> {
validate_unique_header_names(
"configured headers",
config_headers.keys().map(String::as_str),
)?;
let mut merged = merge_headers(config_headers, &[], None);
if let Some(token) = auth_token {
insert_header_case_insensitive(
&mut merged,
"Authorization".to_string(),
normalize_authorization(token),
);
}
Ok(merged)
}
fn validate_unique_header_names<'a>(
source: &str,
names: impl IntoIterator<Item = &'a str>,
) -> Result<()> {
let mut seen = HashSet::new();
for name in names {
let normalized = name.to_ascii_lowercase();
if !seen.insert(normalized) {
bail!("{source} contains duplicate header name ignoring ASCII case: {name}");
}
}
Ok(())
}
fn insert_header_case_insensitive(
headers: &mut HashMap<String, String>,
key: String,
value: String,
) {
if let Some(existing) = headers
.keys()
.find(|existing| existing.eq_ignore_ascii_case(&key))
.cloned()
{
headers.remove(&existing);
}
headers.insert(key, value);
}
pub fn normalize_authorization(value: &str) -> String {
if value.starts_with("Bearer ") {
value.to_string()
} else {
format!("Bearer {}", value)
}
}
#[cfg(test)]
mod header_tests {
use super::*;
#[test]
fn auth_token_is_the_only_source_that_adds_bearer() {
let mut configured = HashMap::new();
configured.insert("X-Test".to_string(), "configured".to_string());
let merged =
merge_config_headers_checked(configured, Some("secret")).expect("valid headers");
assert_eq!(
merged.get("Authorization").map(String::as_str),
Some("Bearer secret")
);
}
#[test]
fn complete_authorization_values_are_preserved_and_override_case_insensitively() {
let mut configured = HashMap::new();
configured.insert("authorization".to_string(), "Bearer old".to_string());
let cli_headers = vec![(
"AUTHORIZATION".to_string(),
"ApiKey from-header".to_string(),
)];
let cli_auth = "Basic final".to_string();
let merged = merge_headers_checked(configured, &cli_headers, Some(&cli_auth))
.expect("valid headers");
assert_eq!(merged.len(), 1);
assert_eq!(
merged.get("Authorization").map(String::as_str),
Some("Basic final")
);
}
#[test]
fn duplicate_header_names_in_one_source_fail_fast() {
let cli_headers = vec![
("Authorization".to_string(), "Bearer one".to_string()),
("authorization".to_string(), "Bearer two".to_string()),
];
let error = merge_headers_checked(HashMap::new(), &cli_headers, None)
.expect_err("case-insensitive duplicates must fail");
assert!(error.to_string().contains("duplicate header name"));
}
}