coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! MCP tool adapter for integrating MCP tools with CoderLib

use async_trait::async_trait;
use std::sync::Arc;

use crate::{
    tools::{Tool, ToolResponse, ToolError, Permission},
    integration::HostIntegration,
    mcp::{
        client::McpClient,
        error::{McpError, McpResult},
    },
};

/// Adapter that wraps MCP tools as CoderLib tools
#[derive(Clone)]
pub struct McpToolAdapter {
    /// Name of the server this tool belongs to
    server_name: String,
    /// Name of the tool
    tool_name: String,
    /// Tool information from MCP
    #[cfg(feature = "mcp")]
    tool_info: rmcp::model::Tool,
    #[cfg(not(feature = "mcp"))]
    tool_info: (),
    /// MCP client for executing the tool
    client: Arc<McpClient>,
    /// Required permission for this tool
    permission: Permission,
}

impl McpToolAdapter {
    /// Create a new MCP tool adapter
    #[cfg(feature = "mcp")]
    pub fn new(
        server_name: String,
        tool_info: rmcp::model::Tool,
        client: Arc<McpClient>,
    ) -> Self {
        let tool_name = tool_info.name.to_string();
        let permission = Self::determine_permission(&tool_info);
        
        Self {
            server_name,
            tool_name,
            tool_info,
            client,
            permission,
        }
    }
    
    #[cfg(not(feature = "mcp"))]
    pub fn new(
        server_name: String,
        _tool_info: (),
        client: Arc<McpClient>,
    ) -> Self {
        Self {
            server_name: server_name.clone(),
            tool_name: "disabled".to_string(),
            tool_info: (),
            client,
            permission: Permission::ReadFile(std::path::PathBuf::from(".")),
        }
    }
    
    /// Determine the required permission for an MCP tool
    #[cfg(feature = "mcp")]
    fn determine_permission(tool: &rmcp::model::Tool) -> Permission {
        // Analyze tool name for hints about required permissions
        let name_lower = tool.name.to_lowercase();
        if name_lower.contains("delete") || name_lower.contains("remove") || name_lower.contains("rm") {
            return Permission::SystemModification;
        }
        if name_lower.contains("write") || name_lower.contains("create") || name_lower.contains("edit") {
            return Permission::WriteFile(std::path::PathBuf::from("."));
        }
        if name_lower.contains("exec") || name_lower.contains("run") || name_lower.contains("command") {
            return Permission::SystemModification;
        }

        // Default to read-only permission
        Permission::ReadFile(std::path::PathBuf::from("."))
    }
    
    #[cfg(not(feature = "mcp"))]
    fn determine_permission(_tool: &()) -> Permission {
        Permission::ReadFile(std::path::PathBuf::from("."))
    }
    
    /// Convert CoderLib parameters to MCP format
    fn convert_parameters(&self, params: serde_json::Value) -> Result<serde_json::Map<String, serde_json::Value>, ToolError> {
        match params {
            serde_json::Value::Object(map) => Ok(map),
            _ => Err(ToolError::InvalidParameters("Parameters must be an object".to_string()))
        }
    }
    
    /// Convert MCP response to CoderLib format
    #[cfg(feature = "mcp")]
    fn convert_response(&self, result: rmcp::model::CallToolResult) -> Result<ToolResponse, ToolError> {
        if result.is_error.unwrap_or(false) {
            let error_msg = result.content.first()
                .map(|c| match &c.raw {
                    rmcp::model::RawContent::Text(text) => text.text.clone(),
                    _ => "Unknown error".to_string(),
                })
                .unwrap_or_else(|| "Unknown error".to_string());
            return Ok(ToolResponse::failure(error_msg));
        }

        let content = result.content.iter()
            .filter_map(|c| match &c.raw {
                rmcp::model::RawContent::Text(text) => Some(text.text.as_str()),
                _ => None,
            })
            .collect::<Vec<_>>()
            .join("\n");

        Ok(ToolResponse::success(content))
    }
    
    #[cfg(not(feature = "mcp"))]
    fn convert_response(&self, _result: ()) -> Result<ToolResponse, ToolError> {
        Err(ToolError::Unavailable("MCP feature not enabled".to_string()))
    }
    
    /// Validate execution permission
    async fn validate_execution_permission(&self, params: &serde_json::Value, host: &dyn HostIntegration) -> Result<(), ToolError> {
        // Check if tool requires dangerous permissions
        if matches!(self.permission, Permission::SystemModification) {
            // Require explicit user confirmation for system modifications
            let confirmation = host.request_permission(self.permission.clone()).await
                .map_err(|e| ToolError::PermissionDenied(format!("Permission request failed: {}", e)))?;

            if !confirmation {
                return Err(ToolError::PermissionDenied("User denied system modification permission".to_string()));
            }
        }

        // Validate parameters for potential security issues
        self.validate_parameters_security(params)?;

        Ok(())
    }
    
    /// Validate parameters for security issues
    fn validate_parameters_security(&self, params: &serde_json::Value) -> Result<(), ToolError> {
        // Check for potential command injection
        if let Some(obj) = params.as_object() {
            for (key, value) in obj {
                if let Some(str_val) = value.as_str() {
                    if str_val.contains("$(") || str_val.contains("`") || str_val.contains(";") {
                        return Err(ToolError::SecurityViolation(
                            format!("Potentially dangerous parameter value in {}: {}", key, str_val)
                        ));
                    }
                }
            }
        }
        Ok(())
    }
    
    /// Get the full tool name (server_name::tool_name)
    pub fn full_name(&self) -> String {
        format!("{}::{}", self.server_name, self.tool_name)
    }
    
    /// Get the server name
    pub fn server_name(&self) -> &str {
        &self.server_name
    }
    
    /// Get the tool name
    pub fn tool_name(&self) -> &str {
        &self.tool_name
    }
}

#[async_trait]
impl Tool for McpToolAdapter {
    #[cfg(feature = "mcp")]
    async fn execute(
        &self,
        parameters: serde_json::Value,
        host: &dyn HostIntegration,
    ) -> Result<ToolResponse, ToolError> {
        // Validate permissions
        self.validate_execution_permission(&parameters, host).await?;
        
        // Convert parameters to MCP format
        let mcp_params = self.convert_parameters(parameters)?;
        
        // Execute MCP tool
        let result = self.client.call_tool(
            rmcp::model::CallToolRequestParam {
                name: self.tool_name.clone().into(),
                arguments: Some(mcp_params),
            }
        ).await.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
        
        // Convert response to CoderLib format
        self.convert_response(result)
    }
    
    #[cfg(not(feature = "mcp"))]
    async fn execute(
        &self,
        _parameters: serde_json::Value,
        _host: &dyn HostIntegration,
    ) -> Result<ToolResponse, ToolError> {
        Err(ToolError::Unavailable("MCP feature not enabled".to_string()))
    }
    
    fn requires_permission(&self) -> Permission {
        self.permission.clone()
    }

    fn clone_box(&self) -> Box<dyn Tool> {
        Box::new(self.clone())
    }
    
    fn name(&self) -> &str {
        &self.tool_name
    }
    
    #[cfg(feature = "mcp")]
    fn description(&self) -> &str {
        self.tool_info.description.as_deref().unwrap_or("MCP tool")
    }
    
    #[cfg(not(feature = "mcp"))]
    fn description(&self) -> &str {
        "MCP tool (disabled)"
    }
    
    #[cfg(feature = "mcp")]
    fn parameter_schema(&self) -> serde_json::Value {
        serde_json::Value::Object((*self.tool_info.input_schema).clone())
    }
    
    #[cfg(not(feature = "mcp"))]
    fn parameter_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {},
            "description": "MCP feature not enabled"
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_full_name() {
        let client = Arc::new(McpClient::new("test-server".to_string(), Default::default()));
        
        #[cfg(feature = "mcp")]
        let tool_info = rmcp::model::Tool {
            name: "test-tool".to_string().into(),
            description: Some("Test tool".to_string().into()),
            input_schema: std::sync::Arc::new(serde_json::Map::new()),
            annotations: None,
        };
        
        #[cfg(not(feature = "mcp"))]
        let tool_info = ();
        
        let adapter = McpToolAdapter::new("test-server".to_string(), tool_info, client);
        assert_eq!(adapter.full_name(), "test-server::test-tool");
    }
    
    #[test]
    fn test_parameter_validation() {
        let client = Arc::new(McpClient::new("test-server".to_string(), Default::default()));
        
        #[cfg(feature = "mcp")]
        let tool_info = rmcp::model::Tool {
            name: "test-tool".to_string().into(),
            description: Some("Test tool".to_string().into()),
            input_schema: std::sync::Arc::new(serde_json::Map::new()),
            annotations: None,
        };
        
        #[cfg(not(feature = "mcp"))]
        let tool_info = ();
        
        let adapter = McpToolAdapter::new("test-server".to_string(), tool_info, client);
        
        // Test valid parameters
        let valid_params = serde_json::json!({"key": "value"});
        assert!(adapter.convert_parameters(valid_params).is_ok());
        
        // Test invalid parameters
        let invalid_params = serde_json::json!("string");
        assert!(adapter.convert_parameters(invalid_params).is_err());
    }
}