use mcp_multiplexer::{cache::Cache, config::Config, upstream::Upstreams};
fn isolate_cache() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let dir = std::env::temp_dir().join(format!("mcpmux-test-cache-{}", std::process::id()));
unsafe { std::env::set_var("XDG_CACHE_HOME", &dir) };
});
}
fn test_config() -> Config {
let bin = env!("CARGO_BIN_EXE_mcp-mock");
let text = format!(r#"{{"mcpServers":{{"mock":{{"command":{bin:?}}}}}}}"#);
serde_json::from_str(&text).unwrap()
}
#[tokio::test]
async fn lazy_connect_and_call() {
isolate_cache();
let ups = Upstreams::new(test_config(), Cache::default(), 0);
assert_eq!(ups.status("mock"), "cold");
let tools = ups.tools("mock").await.unwrap();
let names: Vec<_> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(
names.contains(&"echo") && names.contains(&"add"),
"{names:?}"
);
assert_eq!(ups.status("mock"), "connected");
assert_eq!(
ups.instructions("mock").as_deref(),
Some("mock upstream for tests")
);
let mut args = serde_json::Map::new();
args.insert("message".into(), "hi".into());
let res = ups.call("mock", "echo", Some(args)).await.unwrap();
let text = res.content[0].as_text().unwrap().text.clone();
assert_eq!(text, "echo: hi");
}
#[tokio::test]
async fn unknown_tool_rerefreshes_and_errors() {
isolate_cache();
let ups = Upstreams::new(test_config(), Cache::default(), 0);
let err = ups
.call("mock", "nonexistent", None)
.await
.unwrap_err()
.to_string();
assert!(!err.is_empty());
assert_eq!(ups.status("mock"), "connected");
}
#[tokio::test]
async fn denied_tool_blocked() {
isolate_cache();
let bin = env!("CARGO_BIN_EXE_mcp-mock");
let text = format!(r#"{{"mcpServers":{{"mock":{{"command":{bin:?},"deny":["echo"]}}}}}}"#);
let cfg: Config = serde_json::from_str(&text).unwrap();
let ups = Upstreams::new(cfg, Cache::default(), 0);
let err = ups
.call("mock", "echo", None)
.await
.unwrap_err()
.to_string();
assert!(err.contains("blocked by config"), "{err}");
let names: Vec<_> = ups
.tools("mock")
.await
.unwrap()
.iter()
.map(|t| t.name.clone())
.collect();
assert!(!names.contains(&"echo".to_string()));
}
#[tokio::test]
async fn allow_filtered_tool_blocked() {
isolate_cache();
let bin = env!("CARGO_BIN_EXE_mcp-mock");
let text = format!(r#"{{"mcpServers":{{"mock":{{"command":{bin:?},"allow":["add"]}}}}}}"#);
let cfg: Config = serde_json::from_str(&text).unwrap();
let ups = Upstreams::new(cfg, Cache::default(), 0);
let err = ups
.call("mock", "echo", None)
.await
.unwrap_err()
.to_string();
assert!(err.contains("blocked by config"), "{err}");
let names: Vec<_> = ups
.tools("mock")
.await
.unwrap()
.iter()
.map(|t| t.name.clone())
.collect();
assert!(!names.contains(&"echo".to_string()));
}
#[tokio::test]
async fn dead_upstream_reconnects_and_retries() {
isolate_cache();
let bin = env!("CARGO_BIN_EXE_mcp-mock");
let text = format!(
r#"{{"mcpServers":{{"mock":{{"command":{bin:?},"env":{{"MOCK_DIE_AFTER_CALL":"1"}}}}}}}}"#
);
let cfg: Config = serde_json::from_str(&text).unwrap();
let ups = Upstreams::new(cfg, Cache::default(), 0);
let mut args = serde_json::Map::new();
args.insert("message".into(), "one".into());
let r = ups.call("mock", "echo", Some(args)).await.unwrap();
assert_eq!(r.content[0].as_text().unwrap().text, "echo: one");
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let mut args = serde_json::Map::new();
args.insert("message".into(), "two".into());
let r = ups.call("mock", "echo", Some(args)).await.unwrap();
assert_eq!(r.content[0].as_text().unwrap().text, "echo: two");
assert_eq!(ups.status("mock"), "connected");
}
#[tokio::test]
async fn unspawnable_server_errors_without_wedging() {
isolate_cache();
let text = r#"{"mcpServers":{"dead":{"command":"true"}}}"#;
let cfg: Config = serde_json::from_str(text).unwrap();
let ups = Upstreams::new(cfg, Cache::default(), 0);
let err = ups.call("dead", "x", None).await.unwrap_err().to_string();
assert!(!err.is_empty());
assert_eq!(ups.status("dead"), "unavailable");
let err = ups.call("dead", "x", None).await.unwrap_err().to_string();
assert!(!err.is_empty());
assert_eq!(ups.status("dead"), "unavailable");
}