#![cfg(all(feature = "mcp", feature = "test-support"))]
use radkit::agent::LlmWorker;
use radkit::macros::LLMOutput;
use radkit::models::{Content, ContentPart, LlmResponse, TokenUsage};
use radkit::test_support::FakeLlm;
use radkit::tools::{
BaseToolset, DefaultExecutionState, MCPConnectionParams, MCPToolset, ToolCall, ToolContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Serialize, Deserialize, LLMOutput, JsonSchema)]
struct WeatherReport {
location: String,
temperature: f64,
condition: String,
humidity: Option<f64>,
}
fn create_mcp_weather_toolset() -> Arc<MCPToolset> {
let mcp_connection = MCPConnectionParams::Http {
url: "https://mcp-servers.microagents.io/weather".to_string(),
timeout: Duration::from_secs(30),
headers: Default::default(),
};
Arc::new(MCPToolset::new(mcp_connection))
}
fn tool_call_response(tool_name: &str, arguments: serde_json::Value) -> LlmResponse {
let tool_call = ToolCall::new("call-1", tool_name, arguments);
LlmResponse::new(
Content::from_parts(vec![ContentPart::ToolCall(tool_call)]),
TokenUsage::empty(),
)
}
fn structured_response<T: Serialize>(value: &T) -> LlmResponse {
let json_str = serde_json::to_string(value).unwrap();
LlmResponse::new(Content::from_text(json_str), TokenUsage::empty())
}
#[tokio::test]
#[ignore = "requires network access to MCP weather server"]
async fn test_mcp_agent_single_tool_call() {
println!("๐งช Testing MCP Agent with Single Tool Call");
let mcp_toolset = create_mcp_weather_toolset();
let tools = mcp_toolset.get_tools().await;
assert!(!tools.is_empty(), "No tools discovered from MCP server");
println!("๐ Discovered {} MCP tools", tools.len());
for tool in &tools {
println!(" - {}: {}", tool.name(), tool.description());
}
let weather_tool_name = tools
.iter()
.find(|t| {
let name_lower = t.name().to_lowercase();
name_lower.contains("weather") || name_lower.contains("forecast")
})
.map(|t| t.name())
.expect("No weather tool found");
println!("โ Using weather tool: {}", weather_tool_name);
let tool_call_resp = tool_call_response(
weather_tool_name,
json!({
"location": "San Francisco"
}),
);
let final_report = WeatherReport {
location: "San Francisco".to_string(),
temperature: 65.0,
condition: "Partly Cloudy".to_string(),
humidity: Some(70.0),
};
let structured_resp = structured_response(&final_report);
let fake_llm = FakeLlm::with_responses(
"fake-mcp-llm",
vec![Ok(tool_call_resp), Ok(structured_resp)],
);
let weather_agent = LlmWorker::<WeatherReport>::builder(fake_llm.clone())
.with_system_instructions(
"You are a weather assistant. Use the weather tool to fetch current conditions.",
)
.with_toolset(mcp_toolset.clone())
.with_max_iterations(5)
.build();
println!("๐ค Executing agent query: 'What's the weather in San Francisco?'");
let result = weather_agent
.run("What's the weather in San Francisco?")
.await;
assert!(result.is_ok(), "Agent execution failed: {:?}", result.err());
let report = result.unwrap();
println!("๐ Weather Report:");
println!(" Location: {}", report.location);
println!(" Temperature: {}ยฐF", report.temperature);
println!(" Condition: {}", report.condition);
if let Some(humidity) = report.humidity {
println!(" Humidity: {}%", humidity);
}
let llm_calls = fake_llm.calls();
assert!(
!llm_calls.is_empty(),
"FakeLlm was not called during execution"
);
println!("โ LLM called {} times", llm_calls.len());
let last_thread = &llm_calls[llm_calls.len() - 1];
let has_tool_result = last_thread.events().iter().any(|event| {
event
.content()
.parts()
.iter()
.any(|part| matches!(part, ContentPart::ToolResponse(_)))
});
assert!(
has_tool_result,
"No tool result found in LLM thread - tool was not executed"
);
println!("โ MCP tool was executed successfully");
mcp_toolset.close().await;
println!("โ
MCP agent single tool call test passed");
}
#[tokio::test]
#[ignore = "requires network access to MCP weather server"]
async fn test_mcp_agent_multi_turn_conversation() {
println!("๐งช Testing MCP Agent Multi-Turn Conversation");
let mcp_toolset = create_mcp_weather_toolset();
let tools = mcp_toolset.get_tools().await;
assert!(!tools.is_empty(), "No tools discovered");
let weather_tool_name = tools
.iter()
.find(|t| {
let name_lower = t.name().to_lowercase();
name_lower.contains("weather") || name_lower.contains("forecast")
})
.map(|t| t.name())
.expect("No weather tool found");
println!("โ Using weather tool: {}", weather_tool_name);
let tool_call_1 = tool_call_response(
weather_tool_name,
json!({
"location": "New York"
}),
);
let tool_call_2 = tool_call_response(
weather_tool_name,
json!({
"location": "Los Angeles"
}),
);
#[derive(Debug, Serialize, Deserialize, LLMOutput, JsonSchema)]
struct Comparison {
city1: String,
city2: String,
summary: String,
}
let comparison = Comparison {
city1: "New York".to_string(),
city2: "Los Angeles".to_string(),
summary: "New York is colder than Los Angeles".to_string(),
};
let structured_resp = structured_response(&comparison);
let fake_llm = FakeLlm::with_responses(
"fake-mcp-llm-multiturn",
vec![Ok(tool_call_1), Ok(tool_call_2), Ok(structured_resp)],
);
let comparison_agent = LlmWorker::<Comparison>::builder(fake_llm.clone())
.with_system_instructions(
"You are a weather assistant. Compare weather between cities using the weather tool.",
)
.with_toolset(mcp_toolset.clone())
.with_max_iterations(10)
.build();
println!("๐ค Executing: 'Compare weather between New York and Los Angeles'");
let result = comparison_agent
.run("Compare weather between New York and Los Angeles")
.await;
assert!(result.is_ok(), "Agent execution failed: {:?}", result.err());
let comparison = result.unwrap();
println!("๐ Comparison Result:");
println!(" City 1: {}", comparison.city1);
println!(" City 2: {}", comparison.city2);
println!(" Summary: {}", comparison.summary);
let llm_calls = fake_llm.calls();
assert!(
llm_calls.len() >= 2,
"Expected at least 2 LLM calls, got {}",
llm_calls.len()
);
println!("โ LLM called {} times for multi-turn", llm_calls.len());
let tool_result_count = llm_calls
.iter()
.flat_map(|thread| thread.events())
.filter(|event| {
event
.content()
.parts()
.iter()
.any(|part| matches!(part, ContentPart::ToolResponse(_)))
})
.count();
assert!(
tool_result_count >= 1,
"Expected at least 1 tool execution, got {}",
tool_result_count
);
println!("โ {} tool execution(s) completed", tool_result_count);
mcp_toolset.close().await;
println!("โ
MCP agent multi-turn test passed");
}
#[tokio::test]
#[ignore = "requires network access to MCP weather server"]
async fn test_mcp_tool_error_handling() {
println!("๐งช Testing MCP Tool Error Handling");
let mcp_toolset = create_mcp_weather_toolset();
let tools = mcp_toolset.get_tools().await;
assert!(!tools.is_empty(), "No tools discovered");
let weather_tool = tools
.iter()
.find(|t| {
let name_lower = t.name().to_lowercase();
name_lower.contains("weather") || name_lower.contains("forecast")
})
.expect("No weather tool found");
println!("โ Testing error handling with: {}", weather_tool.name());
let mut invalid_args = std::collections::HashMap::new();
invalid_args.insert("invalid_param".to_string(), json!("test"));
let state = DefaultExecutionState::new();
let tool_context = ToolContext::builder()
.with_state(&state)
.build()
.expect("Failed to create ToolContext");
println!("๐ง Calling tool with invalid arguments...");
let result = weather_tool.run_async(invalid_args, &tool_context).await;
println!("๐ Tool result: {:?}", result);
println!("โ Tool handled invalid arguments without panic");
mcp_toolset.close().await;
println!("โ
MCP error handling test passed");
}
#[tokio::test]
#[ignore = "requires network access to MCP weather server"]
async fn test_mcp_toolset_session_reuse() {
println!("๐งช Testing MCP Session Reuse");
let mcp_toolset = create_mcp_weather_toolset();
println!("๐ Calling get_tools() first time...");
let tools1 = mcp_toolset.get_tools().await;
assert!(!tools1.is_empty(), "No tools discovered");
println!("๐ Calling get_tools() second time (should reuse session)...");
let tools2 = mcp_toolset.get_tools().await;
assert!(!tools2.is_empty(), "No tools discovered");
assert_eq!(
tools1.len(),
tools2.len(),
"Tool count differs between calls"
);
println!(
"โ Both calls returned {} tools (session reused)",
tools1.len()
);
mcp_toolset.close().await;
println!("โ
MCP session reuse test passed");
}