use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError};
use serde::de::DeserializeOwned;
use std::path::PathBuf;
use tracing::debug;
use crate::project::ProjectWorkspace;
pub fn resolve_build_directory(
workspace: &ProjectWorkspace,
requested_build_dir: Option<&str>,
) -> Result<PathBuf, CallToolError> {
match requested_build_dir {
Some(build_dir_str) => {
debug!(
"Attempting to use specified build directory: {}",
build_dir_str
);
let requested_path = PathBuf::from(build_dir_str);
let absolute_path = if requested_path.is_absolute() {
debug!("Using absolute path as-is: {}", requested_path.display());
requested_path
} else {
let absolute = workspace.project_root_path.join(&requested_path);
debug!(
"Converting relative path '{}' to absolute path '{}' using project root '{}'",
build_dir_str,
absolute.display(),
workspace.project_root_path.display()
);
absolute
};
if workspace
.get_component_by_build_dir(&absolute_path)
.is_some()
{
debug!(
"Build directory '{}' found in workspace",
absolute_path.display()
);
Ok(absolute_path)
} else {
debug!(
"Build directory '{}' not found in workspace, will attempt dynamic discovery",
absolute_path.display()
);
if !absolute_path.exists() {
let available_dirs = workspace.get_build_dirs();
let is_relative = !PathBuf::from(build_dir_str).is_absolute();
let relative_path_note = if is_relative {
format!(
" (You provided relative path '{}' which was resolved to '{}' using scan root '{}')",
build_dir_str,
absolute_path.display(),
workspace.project_root_path.display()
)
} else {
String::new()
};
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"Build directory '{}' does not exist{}. Scan root: '{}'. Run get_project_details first to see available build directories with absolute paths. Available directories: {:?}. STRONGLY RECOMMEND: Use absolute paths from get_project_details output.",
absolute_path.display(),
relative_path_note,
workspace.project_root_path.display(),
available_dirs
),
)));
}
Ok(absolute_path)
}
}
None => {
debug!("No build directory specified, attempting auto-detection");
let build_dirs = workspace.get_build_dirs();
match build_dirs.len() {
0 => {
debug!("No build directories found in workspace");
Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"No build directories found in project. Scan root: '{}'. Run get_project_details first to see project status and available build configurations. If no build directories exist, you may need to run cmake or meson to generate build configuration.",
workspace.project_root_path.display()
),
)))
}
1 => {
debug!("Single build directory found: {:?}", build_dirs[0]);
Ok(build_dirs[0].clone())
}
_ => {
debug!("Multiple build directories found: {:?}", build_dirs);
Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Multiple build directories found. Scan root: '{}'. Run get_project_details to see all available options with absolute paths, then specify one using the build_directory parameter. Available directories: {:?}. STRONGLY RECOMMEND: Use absolute paths from get_project_details output.",
workspace.project_root_path.display(),
build_dirs
),
)))
}
}
}
}
}
pub trait ToolArguments {
fn deserialize_tool<T: DeserializeOwned>(self, tool_name: &str) -> Result<T, CallToolError>;
}
impl ToolArguments for Option<serde_json::Map<String, serde_json::Value>> {
fn deserialize_tool<T: DeserializeOwned>(self, tool_name: &str) -> Result<T, CallToolError> {
serde_json::from_value(
self.map(serde_json::Value::Object)
.unwrap_or(serde_json::Value::Null),
)
.map_err(|e| {
CallToolError::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to deserialize {tool_name} arguments: {e}"),
))
})
}
}
pub trait McpToolHandler<T> {
const TOOL_NAME: &'static str;
#[allow(dead_code)]
fn call_tool_sync(&self, _tool: T) -> Result<CallToolResult, CallToolError> {
panic!("call_tool_sync not implemented - this tool should use call_tool_async")
}
async fn call_tool_async(&self, _tool: T) -> Result<CallToolResult, CallToolError> {
panic!("call_tool_async not implemented - this tool should use call_tool_sync")
}
}
#[macro_export]
macro_rules! register_tools {
($handler_type:ty {
$($tool_type:ty => $handler_method:ident ($tool_mode:ident)),+ $(,)?
}) => {
impl $handler_type {
pub async fn dispatch_tool(
&self,
tool_name: &str,
arguments: Option<serde_json::Map<String, serde_json::Value>>,
) -> Result<rust_mcp_sdk::schema::CallToolResult, rust_mcp_sdk::schema::schema_utils::CallToolError> {
use $crate::mcp_server::server_helpers::{McpToolHandler, ToolArguments};
match tool_name {
$(
<Self as McpToolHandler<$tool_type>>::TOOL_NAME => {
let tool: $tool_type = arguments.deserialize_tool(tool_name)?;
register_tools!(@dispatch_call self, tool, $tool_mode)
}
)+
_ => Err(rust_mcp_sdk::schema::schema_utils::CallToolError::unknown_tool(
format!("Unknown tool: {}", tool_name)
))
}
}
pub fn registered_tools() -> Vec<rust_mcp_sdk::schema::Tool> {
vec![
$(
<$tool_type>::tool(),
)+
]
}
}
};
(@dispatch_call $self:expr, $tool:expr, sync) => {
$self.call_tool_sync($tool)
};
(@dispatch_call $self:expr, $tool:expr, async) => {
$self.call_tool_async($tool).await
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_with_explicit_directory() {
let _result: fn(&ProjectWorkspace, Option<&str>) -> Result<PathBuf, CallToolError> =
resolve_build_directory;
}
}