use std::collections::HashMap;
use std::time::Duration;
use agent_client_protocol as acp;
use zeph_config::McpTrustLevel;
use zeph_mcp::{McpTransport, ServerEntry};
const DEFAULT_MCP_TIMEOUT_SECS: u64 = 30;
#[must_use]
pub fn acp_mcp_servers_to_entries(
servers: &[acp::schema::v1::McpServer],
elicitation_default_timeout_secs: u64,
) -> Vec<ServerEntry> {
servers
.iter()
.filter_map(|s| match s {
acp::schema::v1::McpServer::Stdio(stdio) => {
let env: HashMap<String, String> = stdio
.env
.iter()
.filter(|e| !is_dangerous_env_var(&e.name))
.map(|e| (e.name.clone(), e.value.clone()))
.collect();
Some(ServerEntry {
id: stdio.name.clone(),
transport: McpTransport::Stdio {
command: stdio.command.display().to_string(),
args: stdio.args.clone(),
env,
},
timeout: Duration::from_secs(DEFAULT_MCP_TIMEOUT_SECS),
trust_level: McpTrustLevel::Untrusted,
tool_allowlist: None,
allow_untrusted_without_allowlist: false,
expected_tools: Vec::new(),
roots: Vec::new(),
tool_metadata: HashMap::new(),
elicitation_enabled: elicitation_from_meta(stdio.meta.as_ref()),
elicitation_timeout_secs: elicitation_timeout_from_meta(
stdio.meta.as_ref(),
elicitation_default_timeout_secs,
),
env_isolation: false,
media_passthrough: false,
})
}
acp::schema::v1::McpServer::Http(http) => Some(ServerEntry {
id: http.name.clone(),
transport: McpTransport::Http {
url: http.url.clone(),
headers: std::collections::HashMap::new(),
},
timeout: Duration::from_secs(DEFAULT_MCP_TIMEOUT_SECS),
trust_level: McpTrustLevel::Untrusted,
tool_allowlist: None,
allow_untrusted_without_allowlist: false,
expected_tools: Vec::new(),
roots: Vec::new(),
tool_metadata: HashMap::new(),
elicitation_enabled: elicitation_from_meta(http.meta.as_ref()),
elicitation_timeout_secs: elicitation_timeout_from_meta(
http.meta.as_ref(),
elicitation_default_timeout_secs,
),
env_isolation: false,
media_passthrough: false,
}),
acp::schema::v1::McpServer::Sse(sse) => {
Some(ServerEntry {
id: sse.name.clone(),
transport: McpTransport::Http {
url: sse.url.clone(),
headers: std::collections::HashMap::new(),
},
timeout: Duration::from_secs(DEFAULT_MCP_TIMEOUT_SECS),
trust_level: McpTrustLevel::Untrusted,
tool_allowlist: None,
allow_untrusted_without_allowlist: false,
expected_tools: Vec::new(),
roots: Vec::new(),
tool_metadata: HashMap::new(),
elicitation_enabled: elicitation_from_meta(sse.meta.as_ref()),
elicitation_timeout_secs: elicitation_timeout_from_meta(
sse.meta.as_ref(),
elicitation_default_timeout_secs,
),
env_isolation: false,
media_passthrough: false,
})
}
_ => {
tracing::warn!("skipping unknown MCP server transport — not supported");
None
}
})
.collect()
}
fn elicitation_from_meta(meta: Option<&serde_json::Map<String, serde_json::Value>>) -> bool {
meta.and_then(|m| m.get("elicitation_enabled"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
}
pub(crate) fn elicitation_timeout_from_meta(
meta: Option<&serde_json::Map<String, serde_json::Value>>,
default_secs: u64,
) -> u64 {
meta.and_then(|m| m.get("elicitation_timeout_secs"))
.and_then(serde_json::Value::as_u64)
.unwrap_or(default_secs)
}
fn is_dangerous_env_var(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
matches!(
upper.as_str(),
"LD_PRELOAD"
| "LD_LIBRARY_PATH"
| "DYLD_INSERT_LIBRARIES"
| "DYLD_LIBRARY_PATH"
| "DYLD_FRAMEWORK_PATH"
| "DYLD_FALLBACK_LIBRARY_PATH"
| "PATH"
| "HTTP_PROXY"
| "HTTPS_PROXY"
| "ALL_PROXY"
| "NO_PROXY"
| "BASH_ENV"
| "ENV"
| "PYTHONPATH"
| "NODE_PATH"
| "RUBYLIB"
)
}
#[cfg(test)]
mod elicitation_tests {
use super::elicitation_from_meta;
#[test]
fn absent_meta_returns_false() {
assert!(!elicitation_from_meta(None));
}
#[test]
fn missing_key_returns_false() {
let mut map = serde_json::Map::new();
map.insert("other_key".to_owned(), serde_json::Value::Bool(true));
assert!(!elicitation_from_meta(Some(&map)));
}
#[test]
fn key_true_returns_true() {
let mut map = serde_json::Map::new();
map.insert(
"elicitation_enabled".to_owned(),
serde_json::Value::Bool(true),
);
assert!(elicitation_from_meta(Some(&map)));
}
#[test]
fn key_false_returns_false() {
let mut map = serde_json::Map::new();
map.insert(
"elicitation_enabled".to_owned(),
serde_json::Value::Bool(false),
);
assert!(!elicitation_from_meta(Some(&map)));
}
#[test]
fn non_bool_value_returns_false() {
let mut map = serde_json::Map::new();
map.insert(
"elicitation_enabled".to_owned(),
serde_json::Value::String("true".to_owned()),
);
assert!(!elicitation_from_meta(Some(&map)));
}
}
#[cfg(test)]
mod elicitation_timeout_tests {
use super::elicitation_timeout_from_meta;
#[test]
fn absent_meta_returns_default() {
assert_eq!(elicitation_timeout_from_meta(None, 120), 120);
}
#[test]
fn missing_key_returns_default() {
let mut map = serde_json::Map::new();
map.insert("other_key".to_owned(), serde_json::Value::Bool(true));
assert_eq!(elicitation_timeout_from_meta(Some(&map), 120), 120);
}
#[test]
fn valid_u64_value_returned() {
let mut map = serde_json::Map::new();
map.insert(
"elicitation_timeout_secs".to_owned(),
serde_json::Value::Number(60.into()),
);
assert_eq!(elicitation_timeout_from_meta(Some(&map), 120), 60);
}
#[test]
fn non_numeric_value_returns_default() {
let mut map = serde_json::Map::new();
map.insert(
"elicitation_timeout_secs".to_owned(),
serde_json::Value::String("60".to_owned()),
);
assert_eq!(elicitation_timeout_from_meta(Some(&map), 120), 120);
}
#[test]
fn negative_value_returns_default() {
let mut map = serde_json::Map::new();
map.insert("elicitation_timeout_secs".to_owned(), serde_json::json!(-1));
assert_eq!(elicitation_timeout_from_meta(Some(&map), 120), 120);
}
#[test]
fn custom_default_is_used_when_meta_absent() {
assert_eq!(elicitation_timeout_from_meta(None, 300), 300);
}
}