use std::collections::BTreeMap;
use agent_client_protocol::schema::v1::{McpServer, McpServerHttp, McpServerStdio};
use crate::tools::mcp::{
config::{
InvalidMcpServer, McpConfig, McpSamplingPolicy, McpServerConfig, McpToolFilter,
McpTransport,
},
parse_remote_url, validate_identity, validate_literal_headers, validate_stdio_environment,
};
pub(super) fn overlay_host_mcp(process: &mut McpConfig, servers: &[McpServer]) {
let host = host_mcp_config(servers);
for invalid in &host.invalid_servers {
if process.servers.remove(&invalid.identity).is_some() {
tracing::warn!(server = %invalid.identity, "host MCP entry is invalid; not connecting the configured server it replaces");
}
}
process.merge(host);
}
fn host_mcp_config(servers: &[McpServer]) -> McpConfig {
let mut config = McpConfig::default();
for server in servers {
let (name, converted) = match server {
McpServer::Stdio(stdio) => (stdio.name.as_str(), stdio_server(stdio)),
McpServer::Http(http) => (http.name.as_str(), http_server(http)),
McpServer::Sse(sse) => {
tracing::warn!(server = %sse.name, "skipping host MCP server: SSE transport is not supported");
continue;
}
_ => {
tracing::warn!("skipping host MCP server with an unsupported transport");
continue;
}
};
let converted =
validate_identity(name).and_then(|()| converted.map(|server| (name, server)));
let replaced_valid = config.servers.remove(name).is_some();
let before = config.invalid_servers.len();
config
.invalid_servers
.retain(|invalid| invalid.identity != name);
if replaced_valid || config.invalid_servers.len() != before {
tracing::warn!(server = %name, "host MCP server listed twice; keeping the last entry");
}
match converted {
Ok((name, server)) => {
config.servers.insert(name.to_string(), server);
}
Err(error) => config.invalid_servers.push(InvalidMcpServer {
identity: name.to_string(),
error: format!("{error:#}"),
}),
}
}
config
}
fn stdio_server(server: &McpServerStdio) -> anyhow::Result<McpServerConfig> {
let command = server.command.to_string_lossy().into_owned();
if command.trim().is_empty() {
anyhow::bail!("stdio command must not be empty");
}
let mut env = BTreeMap::new();
for variable in &server.env {
if env
.insert(variable.name.clone(), variable.value.clone())
.is_some()
{
anyhow::bail!("stdio env repeats child variable `{}`", variable.name);
}
}
let env_from_env = BTreeMap::new();
validate_stdio_environment(&env, &env_from_env)?;
Ok(host_server(McpTransport::Stdio {
command,
args: server.args.clone(),
cwd: None,
env,
env_from_env,
}))
}
fn http_server(server: &McpServerHttp) -> anyhow::Result<McpServerConfig> {
parse_remote_url(&server.url)?;
let mut headers = BTreeMap::new();
for header in &server.headers {
if headers
.insert(header.name.clone(), header.value.clone())
.is_some()
{
anyhow::bail!("header `{}` is listed twice", header.name);
}
}
validate_literal_headers(&headers)?;
Ok(host_server(McpTransport::StreamableHttp {
url: server.url.clone(),
headers,
headers_from_env: BTreeMap::new(),
oauth: None,
allow_insecure_http: false,
}))
}
fn host_server(transport: McpTransport) -> McpServerConfig {
McpServerConfig {
enabled: true,
tools: McpToolFilter::default(),
log_level: None,
sampling: McpSamplingPolicy::Deny,
transport,
filesystem: None,
}
}
#[cfg(test)]
#[path = "host_mcp_tests.rs"]
mod tests;