use async_trait::async_trait;
use std::sync::Arc;
use crate::{
tools::{Tool, ToolResponse, ToolError, Permission},
integration::HostIntegration,
mcp::{
client::McpClient,
error::{McpError, McpResult},
},
};
#[derive(Clone)]
pub struct McpToolAdapter {
server_name: String,
tool_name: String,
#[cfg(feature = "mcp")]
tool_info: rmcp::model::Tool,
#[cfg(not(feature = "mcp"))]
tool_info: (),
client: Arc<McpClient>,
permission: Permission,
}
impl McpToolAdapter {
#[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(".")),
}
}
#[cfg(feature = "mcp")]
fn determine_permission(tool: &rmcp::model::Tool) -> Permission {
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;
}
Permission::ReadFile(std::path::PathBuf::from("."))
}
#[cfg(not(feature = "mcp"))]
fn determine_permission(_tool: &()) -> Permission {
Permission::ReadFile(std::path::PathBuf::from("."))
}
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()))
}
}
#[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()))
}
async fn validate_execution_permission(&self, params: &serde_json::Value, host: &dyn HostIntegration) -> Result<(), ToolError> {
if matches!(self.permission, Permission::SystemModification) {
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()));
}
}
self.validate_parameters_security(params)?;
Ok(())
}
fn validate_parameters_security(&self, params: &serde_json::Value) -> Result<(), ToolError> {
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(())
}
pub fn full_name(&self) -> String {
format!("{}::{}", self.server_name, self.tool_name)
}
pub fn server_name(&self) -> &str {
&self.server_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> {
self.validate_execution_permission(¶meters, host).await?;
let mcp_params = self.convert_parameters(parameters)?;
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()))?;
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);
let valid_params = serde_json::json!({"key": "value"});
assert!(adapter.convert_parameters(valid_params).is_ok());
let invalid_params = serde_json::json!("string");
assert!(adapter.convert_parameters(invalid_params).is_err());
}
}