use std::env;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("generated_tools.rs");
// Scan the tools directory for #[mcp_tool] annotations
let tools_dir = Path::new("src/mcp/tools");
let tools = scan_for_tools(tools_dir);
// Generate the tool router code
let generated_code = generate_tool_router_code(&tools);
fs::write(&dest_path, generated_code).unwrap();
// Tell cargo to rerun if tools directory changes
println!("cargo:rerun-if-changed=src/mcp/tools");
}
#[derive(Debug)]
struct ToolInfo {
name: String,
description: String,
args_type: String,
function_path: String,
}
fn scan_for_tools(dir: &Path) -> Vec<ToolInfo> {
let mut tools = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Recursively scan subdirectories
tools.extend(scan_for_tools(&path));
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
// Parse Rust files for #[mcp_tool] attributes
if let Ok(content) = fs::read_to_string(&path) {
if let Some(tool) = parse_mcp_tool_from_file(&content, &path) {
tools.push(tool);
}
}
}
}
}
tools
}
fn parse_mcp_tool_from_file(content: &str, file_path: &Path) -> Option<ToolInfo> {
// Look for #[mcp_tool( pattern
let mcp_tool_start = content.find("#[mcp_tool(")?;
let attr_end = content[mcp_tool_start..].find(")]")?;
let attr_content = &content[mcp_tool_start..mcp_tool_start + attr_end + 2];
// Extract name and description
let name = extract_attribute_value(attr_content, "name")?;
let description = extract_attribute_value(attr_content, "description")?;
// Find the function signature after the attribute
let fn_start = content[mcp_tool_start + attr_end..].find("pub async fn ")?;
let fn_content = &content[mcp_tool_start + attr_end + fn_start..];
// Extract function name and args type
let fn_sig_end = fn_content.find('{')?;
let fn_sig = &fn_content[..fn_sig_end];
// Extract function name
let fn_name_start = fn_sig.find("pub async fn ")? + "pub async fn ".len();
let fn_name_end = fn_sig[fn_name_start..].find('(')?;
let fn_name = fn_sig[fn_name_start..fn_name_start + fn_name_end].trim();
// Extract args type (second parameter)
let args_start = fn_sig.find("args:")? + "args:".len();
let args_end = fn_sig[args_start..].find(')')?;
let args_type = fn_sig[args_start..args_start + args_end].trim().to_string();
// Build function path from file path
let function_path = build_function_path(file_path, fn_name);
Some(ToolInfo {
name,
description,
args_type,
function_path,
})
}
fn extract_attribute_value(attr: &str, key: &str) -> Option<String> {
let pattern = format!("{} = \"", key);
let start = attr.find(&pattern)? + pattern.len();
let end = attr[start..].find('"')?;
Some(attr[start..start + end].to_string())
}
fn build_function_path(file_path: &Path, fn_name: &str) -> String {
// Convert file path to module path using components for cross-platform compatibility
let components: Vec<_> = file_path
.components()
.map(|c| c.as_os_str().to_str().unwrap())
.collect();
// Find "src" component to anchor the module path
let src_pos = components
.iter()
.position(|&c| c == "src")
.expect("Path should contain 'src'");
// Extract parts after "src"
let mut module_parts: Vec<String> = components[src_pos + 1..]
.iter()
.map(|s| s.to_string())
.collect();
// Remove .rs extension from the last part
if let Some(last) = module_parts.last_mut() {
if last.ends_with(".rs") {
*last = last.trim_end_matches(".rs").to_string();
}
}
// Construct the full module path to the file
let module_path = module_parts.join("::");
// The function is inside this module.
// We reference it as crate::module_path::fn_name
// This avoids relying on re-exports in parent modules
format!("crate::{}::{}", module_path, fn_name)
}
fn generate_tool_router_code(tools: &[ToolInfo]) -> String {
let mut code = String::from("// Auto-generated by build.rs\n\n");
// Add necessary imports
code.push_str("use crate::mcp::tools::classification_nodes::{ListAreaPathsArgs, ListIterationPathsArgs};\n");
code.push_str(
"use crate::mcp::tools::organizations::{GetCurrentUserArgs, ListOrganizationsArgs};\n",
);
code.push_str("use crate::mcp::tools::projects::ListProjectsArgs;\n");
code.push_str("use crate::mcp::tools::tags::ListTagsArgs;\n");
code.push_str("use crate::mcp::tools::teams::{\n");
code.push_str(
" GetTeamArgs, GetTeamCurrentIterationArgs, ListTeamMembersArgs, ListTeamsArgs,\n",
);
code.push_str(
" boards::{GetBoardArgs, ListBoardColumnsArgs, ListBoardRowsArgs, ListBoardsArgs},\n",
);
code.push_str("};\n");
code.push_str("use crate::mcp::tools::work_item_types::ListWorkItemTypesArgs;\n");
code.push_str("use crate::mcp::tools::work_items::{\n");
code.push_str(" AddCommentArgs, CreateWorkItemArgs, GetWorkItemArgs, GetWorkItemsArgs, LinkWorkItemsArgs,\n");
code.push_str(" QueryWorkItemsArgs, QueryWorkItemsArgsWiql, UpdateWorkItemArgs,\n");
code.push_str("};\n");
code.push_str("use rmcp::{\n");
code.push_str(" ErrorData as McpError,\n");
code.push_str(" handler::server::wrapper::Parameters,\n");
code.push_str(" model::CallToolResult,\n");
code.push_str(" tool, tool_router,\n");
code.push_str("};\n\n");
code.push_str("#[tool_router]\nimpl AzureMcpServer {\n");
code.push_str(" pub fn new(client: AzureDevOpsClient) -> Self {\n");
code.push_str(" Self {\n");
code.push_str(" client: Arc::new(client),\n");
code.push_str(" tool_router: Self::tool_router(),\n");
code.push_str(" }\n");
code.push_str(" }\n\n");
for tool in tools {
code.push_str(&format!(
" #[tool(description = \"{}\")]\n",
tool.description
));
code.push_str(&format!(" async fn {}(\n", tool.name));
code.push_str(" &self,\n");
code.push_str(&format!(" args: Parameters<{}>,\n", tool.args_type));
code.push_str(" ) -> Result<CallToolResult, McpError> {\n");
code.push_str(&format!(
" {}(&self.client, args.0).await\n",
tool.function_path
));
code.push_str(" }\n\n");
}
code.push_str("}\n");
code
}