use crate::error::{Error, Result};
use crate::mcp::tools::{Tool, ToolHandler, ToolResult};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use tracing::{info, instrument};
pub struct RustAgentHandler;
impl Default for RustAgentHandler {
fn default() -> Self {
Self::new()
}
}
impl RustAgentHandler {
pub fn new() -> Self {
Self
}
pub fn tool_definitions() -> Vec<Tool> {
vec![Self::analyze_project_tool(), Self::safety_audit_tool()]
}
fn analyze_project_tool() -> Tool {
Tool::with_schema(
"rust_analyze_project",
"Analyzes a Rust project using cargo check and clippy.
Returns diagnostics including errors and warnings.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the project root (default: current directory)",
},
"fix": {
"type": "boolean",
"description": "Attempt to automatically fix issues using cargo fix",
"default": false
}
},
"additionalProperties": false
}),
)
}
fn safety_audit_tool() -> Tool {
Tool::with_schema(
"rust_safety_audit",
"Scans Rust code for unsafe blocks and checks for safety comments.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to scan",
}
},
"required": ["path"],
"additionalProperties": false
}),
)
}
#[instrument(skip(self, arguments), fields(tool = %name))]
pub async fn call_tool(
&self,
name: &str,
arguments: HashMap<String, Value>,
) -> Result<ToolResult> {
info!(tool = %name, "Executing Rust Agent Tool");
match name {
"rust_analyze_project" => self.handle_analyze_project(arguments).await,
"rust_safety_audit" => self.handle_safety_audit(arguments).await,
_ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
}
}
async fn handle_analyze_project(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
let path_str = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let fix = args.get("fix").and_then(|v| v.as_bool()).unwrap_or(false);
let path = Path::new(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"Path does not exist: {}",
path_str
)));
}
let mut cmd = Command::new("cargo");
cmd.arg("check").current_dir(path);
let output = cmd
.output()
.map_err(|e| Error::Mcp(format!("Failed to run cargo: {}", e)))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let status = if output.status.success() {
"success"
} else {
"failure"
};
let result = json!({
"status": status,
"stdout": stdout,
"stderr": stderr,
"fix_attempted": fix
});
Ok(ToolResult::text(serde_json::to_string_pretty(&result)?))
}
async fn handle_safety_audit(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
let path_str = args
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::Mcp("Missing path argument".into()))?;
let output = Command::new("grep")
.args(["-r", "unsafe", path_str])
.output()
.map_err(|e| Error::Mcp(format!("Failed to run grep: {}", e)))?;
let matches = String::from_utf8_lossy(&output.stdout);
let result = json!({
"unsafe_occurrences": matches.lines().count(),
"details": matches
});
Ok(ToolResult::text(serde_json::to_string_pretty(&result)?))
}
}
#[async_trait]
impl ToolHandler for RustAgentHandler {
async fn call(&self, arguments: HashMap<String, Value>) -> Result<ToolResult> {
let tool_name = arguments
.get("_tool")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| Error::Mcp("Missing _tool identifier".into()))?;
self.call_tool(&tool_name, arguments).await
}
}