use serde_json::json;
use std::time::Duration;
use tempfile::NamedTempFile;
use tokio::{
io::{AsyncWriteExt, BufReader},
process::Command,
time::timeout,
};
mod common;
use common::read_jsonrpc_until_id;
use warmplane::config::{save_config, AliasTarget, McpConfig};
#[tokio::test]
async fn test_mcp_passthrough_tools_listing_and_execution() {
let bin_path = env!("CARGO_BIN_EXE_warmplane");
let temp_config = NamedTempFile::new().unwrap();
let config_path = temp_config.path().to_str().unwrap().to_string();
let mut mcp_config = McpConfig::default();
mcp_config.capability_aliases.insert(
"search_docs".to_string(),
AliasTarget::Detailed {
target: "mock_fs.search".to_string(),
summary: Some("Search indexed documentation".to_string()),
description: Some("Search documentation using full text".to_string()),
passthrough: true,
},
);
mcp_config.capability_aliases.insert(
"hidden_internal".to_string(),
AliasTarget::Detailed {
target: "mock_fs.internal".to_string(),
summary: Some("Internal hidden operation".to_string()),
description: None,
passthrough: false,
},
);
save_config(&config_path, &mcp_config).unwrap();
let mut child = Command::new(bin_path)
.arg("mcp-server")
.arg("--config")
.arg(&config_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn warmplane mcp-server process");
let stdin = child.stdin.as_mut().expect("child stdin must be captured");
let stdout = child.stdout.take().expect("child stdout must be captured");
let mut reader = BufReader::new(stdout);
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0.0" }
}
});
stdin
.write_all(format!("{}\n", serde_json::to_string(&init_req).unwrap()).as_bytes())
.await
.unwrap();
stdin.flush().await.unwrap();
let init_resp = read_jsonrpc_until_id(&mut reader, 1).await;
assert_eq!(init_resp["id"], 1);
let initialized_notif = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized"
});
stdin
.write_all(format!("{}\n", serde_json::to_string(&initialized_notif).unwrap()).as_bytes())
.await
.unwrap();
stdin.flush().await.unwrap();
let tools_req = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
});
stdin
.write_all(format!("{}\n", serde_json::to_string(&tools_req).unwrap()).as_bytes())
.await
.unwrap();
stdin.flush().await.unwrap();
let tools_resp = read_jsonrpc_until_id(&mut reader, 2).await;
assert_eq!(tools_resp["id"], 2);
let tools = tools_resp["result"]["tools"]
.as_array()
.expect("tools array must exist");
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(tool_names.contains(&"capabilities_list"));
assert!(tool_names.contains(&"capability_call"));
assert!(
tool_names.contains(&"search_docs"),
"Passthrough tool 'search_docs' should be in tools/list"
);
assert!(
!tool_names.contains(&"hidden_internal"),
"Non-passthrough alias 'hidden_internal' should NOT be in tools/list"
);
let call_req = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_docs",
"arguments": {
"query": "architecture overview"
}
}
});
stdin
.write_all(format!("{}\n", serde_json::to_string(&call_req).unwrap()).as_bytes())
.await
.unwrap();
stdin.flush().await.unwrap();
let call_resp = read_jsonrpc_until_id(&mut reader, 3).await;
assert_eq!(call_resp["id"], 3);
assert!(
call_resp["result"]["structuredContent"].is_object()
|| call_resp["result"]["content"].is_array()
);
drop(child.stdin.take());
let exit_status = timeout(Duration::from_secs(5), child.wait())
.await
.expect("timeout waiting for child process exit")
.unwrap();
assert!(exit_status.success());
}