use crate::config::McpConnectionType;
use crate::log_debug;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::{IsTerminal, Write};
use std::sync::{Arc, RwLock};
use uuid;
pub mod hint_accumulator;
pub mod tool_map;
lazy_static::lazy_static! {
static ref INTERNAL_FUNCTION_CACHE: Arc<RwLock<std::collections::HashMap<String, Vec<McpFunction>>>> =
Arc::new(RwLock::new(std::collections::HashMap::new()));
}
pub mod oauth;
pub mod agent;
pub mod dev;
pub mod fs;
pub mod health_monitor;
pub mod process;
pub mod server;
pub mod shared_utils;
pub mod web;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCall {
pub tool_name: String,
pub parameters: Value,
#[serde(default)]
pub tool_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolResult {
pub tool_name: String,
pub result: Value,
#[serde(default)]
pub tool_id: String,
}
impl McpToolResult {
pub fn success(tool_name: String, tool_id: String, content: String) -> Self {
Self {
tool_name,
tool_id,
result: json!({
"content": [
{
"type": "text",
"text": content
}
],
"isError": false
}),
}
}
pub fn success_with_metadata(
tool_name: String,
tool_id: String,
content: String,
metadata: serde_json::Value,
) -> Self {
Self {
tool_name,
tool_id,
result: json!({
"content": [
{
"type": "text",
"text": content
}
],
"isError": false,
"metadata": metadata
}),
}
}
pub fn error(tool_name: String, tool_id: String, error_message: String) -> Self {
Self {
tool_name,
tool_id,
result: json!({
"content": [
{
"type": "text",
"text": error_message
}
],
"isError": true
}),
}
}
pub fn is_error(&self) -> bool {
self.result
.get("isError")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
}
pub fn extract_mcp_content(result: &Value) -> String {
if let Some(content_array) = result.get("content") {
if let Some(content_items) = content_array.as_array() {
let main_content = content_items
.iter()
.filter_map(|item| {
if item.get("type").and_then(|t| t.as_str()) == Some("text") {
item.get("text").and_then(|t| t.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n");
if let Some(metadata) = result.get("metadata") {
if !metadata.is_null() {
return format!(
"{}\n\n[Metadata: {}]",
main_content,
serde_json::to_string_pretty(metadata).unwrap_or_default()
);
}
}
return main_content;
}
}
if let Some(output) = result.get("output") {
if let Some(output_str) = output.as_str() {
return output_str.to_string();
}
}
serde_json::to_string_pretty(result).unwrap_or_default()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpFunction {
pub name: String,
pub description: String,
pub parameters: Value,
}
pub fn guess_tool_category(tool_name: &str) -> &'static str {
match tool_name {
"core" => "system",
"text_editor" => "developer",
"list_files" => "filesystem",
"web_search" | "read_html" => "web",
"shell" | "ast_grep" | "plan" => "developer",
name if name.contains("file") || name.contains("editor") => "developer",
name if name.contains("search") || name.contains("find") => "search",
name if name.contains("image") || name.contains("photo") => "media",
name if name.contains("web") || name.contains("http") => "web",
name if name.contains("db") || name.contains("database") => "database",
name if name.contains("browser") => "browser",
name if name.contains("terminal") => "terminal",
name if name.contains("video") => "video",
name if name.contains("audio") => "audio",
name if name.contains("location") || name.contains("map") => "location",
name if name.contains("google") => "google",
name if name.contains("weather") => "weather",
name if name.contains("calculator") || name.contains("math") => "math",
name if name.contains("news") => "news",
name if name.contains("email") => "email",
name if name.contains("calendar") => "calendar",
name if name.contains("translate") => "translation",
name if name.contains("github") => "github",
name if name.contains("git") => "git",
_ => "external",
}
}
pub fn parse_tool_calls(_content: &str) -> Vec<McpToolCall> {
Vec::new()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResponseMessage {
pub role: String,
pub tool_call_id: String,
pub name: String,
pub content: String,
}
pub fn tool_results_to_messages(
results: &[McpToolResult],
config: &crate::config::Config,
) -> Vec<ToolResponseMessage> {
let mut messages = Vec::new();
for result in results {
let content_str = serde_json::to_string(&result.result).unwrap_or_default();
let (final_content, was_truncated) = crate::utils::truncation::truncate_mcp_response_global(
&content_str,
config.mcp_response_tokens_threshold,
);
if was_truncated {
use colored::Colorize;
eprintln!(
"{}",
format!(
"⚠️ Tool '{}' response truncated to {} tokens (mcp_response_tokens_threshold)",
result.tool_name, config.mcp_response_tokens_threshold
)
.bright_yellow()
);
}
messages.push(ToolResponseMessage {
role: "tool".to_string(),
tool_call_id: result.tool_id.clone(),
name: result.tool_name.clone(),
content: final_content,
});
}
messages
}
pub fn ensure_tool_call_ids(calls: &mut [McpToolCall]) {
for call in calls.iter_mut() {
if call.tool_id.is_empty() {
call.tool_id = format!("tool_{}", uuid::Uuid::new_v4().simple());
}
}
}
pub async fn initialize_servers_for_role(config: &crate::config::Config) -> Result<()> {
initialize_servers_for_role_with_callback(config, None).await
}
pub async fn initialize_servers_for_role_with_callback(
config: &crate::config::Config,
progress_callback: Option<&dyn Fn(&str)>,
) -> Result<()> {
if config.mcp.servers.is_empty() {
crate::log_debug!("No MCP servers enabled for this role");
return Ok(());
}
crate::log_debug!(
"Initializing {} MCP servers for role",
config.mcp.servers.len()
);
for server in &config.mcp.servers {
if let McpConnectionType::Http | McpConnectionType::Stdin = server.connection_type() {
crate::log_debug!("Initializing external server: {}", server.name());
if let Some(callback) = progress_callback {
callback(server.name());
}
if server::is_server_already_running_with_config(server) {
crate::log_debug!(
"Server '{}' is already running - skipping initialization",
server.name()
);
continue;
}
match server::get_server_functions(server).await {
Ok(functions) => {
crate::log_debug!(
"Successfully initialized server '{}' with {} functions",
server.name(),
functions.len()
);
for func in &functions {
crate::log_debug!(" - Available: {}", func.name);
}
}
Err(e) => {
crate::log_debug!(
"Failed to initialize server '{}': {} (will retry on first use)",
server.name(),
e
);
}
}
} else {
crate::log_debug!(
"Skipping initialization for internal server: {} ({:?})",
server.name(),
server.connection_type()
);
}
}
let config_arc = std::sync::Arc::new(config.clone());
if let Err(e) = health_monitor::start_health_monitor(config_arc).await {
crate::log_debug!("Failed to start health monitor: {}", e);
}
crate::log_debug!("MCP server initialization completed");
Ok(())
}
pub async fn initialize_mcp_for_role(role: &str, config: &crate::config::Config) -> Result<()> {
initialize_mcp_for_role_with_callback(role, config, None).await
}
pub async fn initialize_mcp_for_role_with_callback(
role: &str,
config: &crate::config::Config,
progress_callback: Option<&dyn Fn(&str)>,
) -> Result<()> {
let config_for_role = config.get_merged_config_for_role(role);
if let Err(e) =
initialize_servers_for_role_with_callback(&config_for_role, progress_callback).await
{
crate::log_debug!("Warning: Failed to initialize MCP servers: {}", e);
}
if let Err(e) = tool_map::initialize_tool_map(&config_for_role).await {
crate::log_debug!("Warning: Failed to initialize tool map: {}", e);
}
Ok(())
}
pub async fn get_available_functions(config: &crate::config::Config) -> Vec<McpFunction> {
let mut functions = Vec::new();
if config.mcp.servers.is_empty() {
crate::log_debug!("MCP has no servers configured, no functions available");
return functions;
}
let enabled_servers: Vec<crate::config::McpServerConfig> = config.mcp.servers.to_vec();
for server in enabled_servers {
match server.connection_type() {
McpConnectionType::Builtin => {
match server.name() {
"developer" => {
let server_functions =
get_filtered_server_functions("developer", server.tools(), || {
dev::get_all_functions()
});
functions.extend(server_functions);
}
"filesystem" => {
let server_functions =
get_filtered_server_functions("filesystem", server.tools(), || {
fs::get_all_functions()
});
functions.extend(server_functions);
}
"agent" => {
let server_functions = agent::get_all_functions(config);
let filtered_functions =
filter_tools_by_patterns(server_functions, server.tools());
functions.extend(filtered_functions);
}
"web" => {
let server_functions =
get_filtered_server_functions("web", server.tools(), || {
web::get_all_functions()
});
functions.extend(server_functions);
}
_ => {
crate::log_debug!("Unknown builtin server: {}", server.name());
}
}
}
McpConnectionType::Http | McpConnectionType::Stdin => {
match server::get_server_functions_cached(&server).await {
Ok(server_functions) => {
let filtered_functions =
filter_tools_by_patterns(server_functions, server.tools());
functions.extend(filtered_functions);
}
Err(e) => {
crate::log_error!(
"Failed to get cached functions from external server '{}': {} (will be available when server starts)",
server.name(),
e
);
}
}
}
}
}
functions
}
fn filter_tools_by_patterns(tools: Vec<McpFunction>, allowed_tools: &[String]) -> Vec<McpFunction> {
if allowed_tools.is_empty() {
tools
} else {
tools
.into_iter()
.filter(|func| is_tool_allowed_by_patterns(&func.name, allowed_tools))
.collect()
}
}
pub fn is_tool_allowed_by_patterns(tool_name: &str, allowed_tools: &[String]) -> bool {
if allowed_tools.is_empty() {
return true;
}
for pattern in allowed_tools {
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
if tool_name.starts_with(prefix) {
return true;
}
} else {
if tool_name == pattern {
return true;
}
}
}
false
}
pub fn get_filtered_server_functions<F>(
server_type: &str,
allowed_tools: &[String],
get_functions: F,
) -> Vec<McpFunction>
where
F: FnOnce() -> Vec<McpFunction>,
{
let cache_key = if allowed_tools.is_empty() {
format!("{}_all", server_type)
} else {
format!("{}_{}", server_type, allowed_tools.join(","))
};
{
let cache = INTERNAL_FUNCTION_CACHE.read().unwrap();
if let Some(cached_functions) = cache.get(&cache_key) {
return cached_functions.clone();
}
}
crate::log_debug!("Computing and caching {} functions", server_type);
let all_functions = get_functions();
let filtered_functions = if allowed_tools.is_empty() {
all_functions
} else {
all_functions
.into_iter()
.filter(|func| is_tool_allowed_by_patterns(&func.name, allowed_tools))
.collect()
};
{
let mut cache = INTERNAL_FUNCTION_CACHE.write().unwrap();
cache.insert(cache_key, filtered_functions.clone());
}
filtered_functions
}
pub fn clear_function_cache() {
let mut cache = INTERNAL_FUNCTION_CACHE.write().unwrap();
let count = cache.len();
cache.clear();
if count > 0 {
crate::log_debug!("Cleared internal function cache for {} entries", count);
}
}
pub async fn execute_tool_call(
call: &McpToolCall,
config: &crate::config::Config,
cancellation_token: Option<tokio::sync::watch::Receiver<bool>>,
) -> Result<(McpToolResult, u64)> {
log_debug!("Debug: Executing tool call: {}", call.tool_name);
log_debug!(
"Debug: MCP has {} servers configured",
config.mcp.servers.len()
);
if let Ok(params) = serde_json::to_string_pretty(&call.parameters) {
log_debug!("Debug: Tool parameters: {}", params);
}
if config.mcp.servers.is_empty() {
return Err(anyhow::anyhow!("MCP has no servers configured"));
}
if let Some(ref token) = cancellation_token {
if *token.borrow() {
return Err(anyhow::anyhow!("Tool execution cancelled"));
}
}
let tool_start = std::time::Instant::now();
let result = try_execute_tool_call(call, config, cancellation_token).await;
let tool_duration = tool_start.elapsed();
let tool_time_ms = tool_duration.as_millis() as u64;
match result {
Ok(tool_result) => {
Ok((tool_result, tool_time_ms))
}
Err(e) => Err(e),
}
}
pub async fn build_tool_server_map(
config: &crate::config::Config,
) -> std::collections::HashMap<String, crate::config::McpServerConfig> {
let mut tool_map = std::collections::HashMap::new();
let enabled_servers: Vec<crate::config::McpServerConfig> = config.mcp.servers.to_vec();
for server in enabled_servers {
let server_functions = match server.connection_type() {
McpConnectionType::Builtin => {
match server.name() {
"developer" => {
get_filtered_server_functions("developer", server.tools(), || {
dev::get_all_functions()
})
}
"filesystem" => {
get_filtered_server_functions("filesystem", server.tools(), || {
fs::get_all_functions()
})
}
"agent" => {
let server_functions = agent::get_all_functions(config);
filter_tools_by_patterns(server_functions, server.tools())
}
"web" => get_filtered_server_functions("web", server.tools(), || {
web::get_all_functions()
}),
_ => {
crate::log_debug!("Unknown builtin server: {}", server.name());
Vec::new()
}
}
}
McpConnectionType::Http | McpConnectionType::Stdin => {
match server::get_server_functions_cached(&server).await {
Ok(functions) => filter_tools_by_patterns(functions, server.tools()),
Err(_) => Vec::new(), }
}
};
for function in server_functions {
tool_map
.entry(function.name)
.or_insert_with(|| server.clone());
}
}
crate::log_debug!("Built tool-to-server map with {} tools", tool_map.len());
tool_map
}
async fn try_execute_tool_call(
call: &McpToolCall,
config: &crate::config::Config,
cancellation_token: Option<tokio::sync::watch::Receiver<bool>>,
) -> Result<McpToolResult> {
if config.mcp.servers.is_empty() {
return Err(anyhow::anyhow!("MCP has no servers configured"));
}
let tool_execution_future =
execute_tool_without_cancellation(call, config, cancellation_token.clone());
if let Some(token) = cancellation_token {
if *token.borrow() {
return Err(anyhow::anyhow!("Tool execution cancelled"));
}
let mut cancel_receiver = token.clone();
let cancellation_future = async move {
loop {
if *cancel_receiver.borrow() {
break;
}
cancel_receiver.changed().await.ok();
}
};
tokio::select! {
biased;
_ = cancellation_future => {
Err(anyhow::anyhow!("Tool execution cancelled during execution"))
}
result = tool_execution_future => {
result
}
}
} else {
tool_execution_future.await
}
}
async fn execute_tool_without_cancellation(
call: &McpToolCall,
config: &crate::config::Config,
cancellation_token: Option<tokio::sync::watch::Receiver<bool>>,
) -> Result<McpToolResult> {
let tool_server_map = {
let mut map = std::collections::HashMap::new();
if let Some(server) = tool_map::get_server_for_tool(&call.tool_name) {
map.insert(call.tool_name.clone(), server);
}
map
};
if let Some(target_server) = tool_server_map.get(&call.tool_name) {
crate::log_debug!(
"Routing tool '{}' to server '{}' ({:?})",
call.tool_name,
target_server.name(),
target_server.connection_type()
);
match target_server.connection_type() {
McpConnectionType::Builtin => {
match target_server.name() {
"developer" => match call.tool_name.as_str() {
"shell" => {
crate::log_debug!(
"Executing shell command via developer server '{}'",
target_server.name()
);
let mut result = dev::execute_shell_command(call).await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
}
"ast_grep" => {
crate::log_debug!(
"Executing ast_grep command via developer server '{}'",
target_server.name()
);
match dev::execute_ast_grep_command(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("AST-grep execution failed: {}", e),
));
}
}
}
"plan" => {
crate::log_debug!(
"Executing plan command via developer server '{}'",
target_server.name()
);
match dev::execute_plan(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Plan execution failed: {}", e),
));
}
}
}
_ => {
return Err(anyhow::anyhow!(
"Tool '{}' not implemented in developer server",
call.tool_name
));
}
},
"filesystem" => match call.tool_name.as_str() {
"text_editor" => {
crate::log_debug!(
"Executing text_editor via filesystem server '{}'",
target_server.name()
);
match fs::execute_text_editor(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Text editor execution failed: {}", e),
));
}
}
}
"list_files" => {
crate::log_debug!(
"Executing list_files via filesystem server '{}'",
target_server.name()
);
let mut result = fs::execute_list_files(call).await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
}
"extract_lines" => {
crate::log_debug!(
"Executing extract_lines via filesystem server '{}'",
target_server.name()
);
match fs::execute_extract_lines(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Extract lines execution failed: {}", e),
));
}
}
}
"batch_edit" => {
crate::log_debug!(
"Executing batch_edit via filesystem server '{}'",
target_server.name()
);
match fs::execute_batch_edit(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Batch edit execution failed: {}", e),
));
}
}
}
_ => {
return Err(anyhow::anyhow!(
"Tool '{}' not implemented in filesystem server",
call.tool_name
));
}
},
"agent" => {
if call.tool_name.starts_with("agent_") || call.tool_name == "call_llm" {
crate::log_debug!(
"Executing agent command '{}' via agent server '{}'",
call.tool_name,
target_server.name()
);
let mut result = agent::execute_agent_command(
call,
config,
cancellation_token.clone(),
)
.await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
} else {
return Err(anyhow::anyhow!(
"Tool '{}' not implemented in agent server",
call.tool_name
));
}
}
"web" => match call.tool_name.as_str() {
"web_search" => {
crate::log_debug!(
"Executing web_search via web server '{}'",
target_server.name()
);
match web::execute_web_search(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Web search execution failed: {}", e),
));
}
}
}
"image_search" => {
crate::log_debug!(
"Executing image_search via web server '{}'",
target_server.name()
);
let mut result = web::execute_image_search(call).await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
}
"video_search" => {
crate::log_debug!(
"Executing video_search via web server '{}'",
target_server.name()
);
let mut result = web::execute_video_search(call).await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
}
"news_search" => {
crate::log_debug!(
"Executing news_search via web server '{}'",
target_server.name()
);
let mut result = web::execute_news_search(call).await?;
result.tool_id = call.tool_id.clone();
return Ok(result);
}
"read_html" => {
crate::log_debug!(
"Executing read_html via web server '{}'",
target_server.name()
);
match web::execute_read_html(call).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(e) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Read HTML execution failed: {}", e),
));
}
}
}
_ => {
return Err(anyhow::anyhow!(
"Tool '{}' not implemented in web server",
call.tool_name
));
}
},
_ => {
return Err(anyhow::anyhow!(
"Unknown builtin server: {}",
target_server.name()
));
}
}
}
McpConnectionType::Http | McpConnectionType::Stdin => {
match server::execute_tool_call(call, target_server, None).await {
Ok(mut result) => {
result.tool_id = call.tool_id.clone();
return Ok(result);
}
Err(err) => {
return Err(err);
}
}
}
}
}
let available_tools = tool_map::get_all_tool_names();
Err(anyhow::anyhow!(
"Tool '{}' not found in any configured MCP server. Available tools: {}",
call.tool_name,
if available_tools.is_empty() {
"none (tool map not initialized)".to_string()
} else {
available_tools.join(", ")
}
))
}
pub async fn handle_large_response(
result: McpToolResult,
config: &crate::config::Config,
mode: crate::session::output::OutputMode,
) -> Result<McpToolResult> {
let estimated_tokens = crate::session::estimate_tokens(&format!("{}", result.result));
if config.mcp_response_warning_threshold > 0
&& estimated_tokens > config.mcp_response_warning_threshold
{
use colored::Colorize;
let suppress_cli_output = mode.should_suppress_cli_output();
let non_interactive = !mode.is_interactive() || !std::io::stdin().is_terminal();
let server_name =
crate::session::chat::response::get_tool_server_name_async(&result.tool_name, config)
.await;
if !suppress_cli_output {
println!(
"{}",
format!(
"! WARNING: Tool '{}' ({}){} produced a large output ({} tokens)",
result.tool_name,
server_name,
if !result.tool_id.is_empty() {
format!(" [ID: {}]", result.tool_id)
} else {
String::new()
},
estimated_tokens
)
.bright_yellow()
);
println!(
"{}",
"This may consume significant tokens and impact your usage limits.".bright_yellow()
);
}
if suppress_cli_output || non_interactive {
if !suppress_cli_output {
println!(
"{}",
format!(
"Large output from '{}' ({}) automatically declined in non-interactive mode. Continuing...",
result.tool_name, server_name
)
.bright_red()
);
}
return Ok(McpToolResult::error(
result.tool_name.clone(),
result.tool_id.clone(),
format!("Large output from tool '{}' ({} tokens) was automatically declined in non-interactive mode to avoid excessive token usage. The tool executed successfully but the output was too large.", result.tool_name, estimated_tokens)
));
}
use crate::session::chat::get_animation_manager;
let animation_manager = get_animation_manager();
animation_manager.suspend().await;
print!(
"{}",
"Do you want to continue with this large output? [y/N]: ".bright_cyan()
);
std::io::stdout().flush().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap_or_default();
animation_manager.resume();
if !input.trim().to_lowercase().starts_with('y') {
println!(
"{}",
format!(
"Large output from '{}' ({}) declined by user. Continuing conversation...",
result.tool_name, server_name
)
.bright_red()
);
return Ok(McpToolResult::error(
result.tool_name.clone(),
result.tool_id.clone(),
format!("User declined to process large output from tool '{}' ({} tokens). The tool executed successfully but the output was too large and the user chose not to include it in the conversation to avoid excessive token usage.", result.tool_name, estimated_tokens)
));
}
println!("{}", "Proceeding with full output...".bright_green());
}
Ok(result)
}
pub async fn execute_layer_tool_call(
call: &McpToolCall,
config: &crate::config::Config,
layer_config: &crate::session::layers::LayerConfig,
cancellation_token: Option<tokio::sync::watch::Receiver<bool>>,
) -> Result<(McpToolResult, u64)> {
if layer_config.mcp.server_refs.is_empty() {
return Err(anyhow::anyhow!("Tool execution is disabled for this layer"));
}
let server_name = crate::mcp::tool_map::get_tool_server_name(&call.tool_name)
.unwrap_or_else(|| "unknown".to_string());
if !layer_config
.mcp
.is_tool_allowed(&call.tool_name, &server_name)
{
return Err(anyhow::anyhow!(
"Tool '{}' is not allowed for this layer",
call.tool_name
));
}
execute_tool_call(call, config, cancellation_token).await
}
pub async fn execute_tool_calls(
calls: &[McpToolCall],
config: &crate::config::Config,
) -> Vec<Result<(McpToolResult, u64)>> {
let mut results = Vec::new();
for call in calls {
let result = execute_tool_call(call, config, None).await;
results.push(result);
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_mcp_tool_result_is_error() {
let success_result = McpToolResult::success(
"test_tool".to_string(),
"test_id".to_string(),
"Success message".to_string(),
);
assert!(
!success_result.is_error(),
"Success result should not be an error"
);
let error_result = McpToolResult::error(
"test_tool".to_string(),
"test_id".to_string(),
"Error message".to_string(),
);
assert!(error_result.is_error(), "Error result should be an error");
let manual_result = McpToolResult {
tool_name: "test_tool".to_string(),
tool_id: "test_id".to_string(),
result: json!({
"content": [{"type": "text", "text": "No isError field"}]
}),
};
assert!(
!manual_result.is_error(),
"Result without isError field should default to false"
);
let explicit_false_result = McpToolResult {
tool_name: "test_tool".to_string(),
tool_id: "test_id".to_string(),
result: json!({
"content": [{"type": "text", "text": "Explicit false"}],
"isError": false
}),
};
assert!(
!explicit_false_result.is_error(),
"Result with isError: false should not be an error"
);
let explicit_true_result = McpToolResult {
tool_name: "test_tool".to_string(),
tool_id: "test_id".to_string(),
result: json!({
"content": [{"type": "text", "text": "Explicit true"}],
"isError": true
}),
};
assert!(
explicit_true_result.is_error(),
"Result with isError: true should be an error"
);
}
}