reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! Rust Agent MCP Handler
//!
//! Provides specialized Rust development tools exposed via MCP.
//!
//! # Available Tools
//!
//! | Tool | Description |
//! |------|-------------|
//! | `rust_analyze_project` | Runs cargo check/clippy and returns diagnostics |
//! | `rust_safety_audit` | Scans for unsafe blocks and missing documentation |
//! | `rust_dependency_audit` | Analyzes Cargo.toml for issues |

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};

/// Rust Agent MCP Handler
pub struct RustAgentHandler;

impl Default for RustAgentHandler {
    fn default() -> Self {
        Self::new()
    }
}

impl RustAgentHandler {
    pub fn new() -> Self {
        Self
    }

    /// Get tool definitions
    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
            )));
        }

        // Run cargo check
        let mut cmd = Command::new("cargo");
        cmd.arg("check").current_dir(path);

        // Capture output
        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()))?;

        // Simple grep-based implementation for now
        // In a real implementation, we would use syn to parse AST
        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
    }
}