use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use ai_agents_core::{Tool, ToolExecutionContext, ToolPolicyBindings, ToolResult};
use super::wrapper::MCPWrapperTool;
pub struct MCPViewTool {
view_name: String,
parent: Arc<MCPWrapperTool>,
allowed_functions: Vec<String>,
description: String,
schema: Value,
}
impl MCPViewTool {
pub fn new(
view_name: String,
parent: Arc<MCPWrapperTool>,
allowed_functions: Vec<String>,
custom_description: Option<String>,
) -> Result<Self, String> {
if allowed_functions.is_empty() {
return Err(format!(
"View '{}': functions list cannot be empty",
view_name,
));
}
let parent_names = parent.function_names();
let mut missing: Vec<&str> = Vec::new();
for name in &allowed_functions {
if !parent_names.iter().any(|p| p == name) {
missing.push(name);
}
}
if !missing.is_empty() {
return Err(format!(
"View '{}': unknown functions {:?}. Available from '{}': {:?}",
view_name,
missing,
parent.id(),
parent_names,
));
}
let discovered = parent.get_functions_filtered(&allowed_functions);
let schema = MCPWrapperTool::build_schema(&view_name, &discovered);
let description = MCPWrapperTool::build_description(
&view_name,
custom_description.as_deref(),
&discovered,
);
Ok(Self {
view_name,
parent,
allowed_functions,
description,
schema,
})
}
fn is_allowed(&self, function: &str) -> bool {
self.allowed_functions.iter().any(|f| f == function)
}
}
#[async_trait]
impl Tool for MCPViewTool {
fn id(&self) -> &str {
&self.view_name
}
fn name(&self) -> &str {
&self.view_name
}
fn description(&self) -> &str {
&self.description
}
fn input_schema(&self) -> Value {
self.schema.clone()
}
fn policy_bindings(&self) -> ToolPolicyBindings {
ToolPolicyBindings {
operation_fields: vec!["function".to_string()],
..Default::default()
}
}
async fn execute(&self, args: Value, ctx: ToolExecutionContext) -> ToolResult {
let function = match args.get("function").and_then(|v| v.as_str()) {
Some(f) => f.to_string(),
None => {
return ToolResult::error(format!(
"'function' is required. Available functions: {}",
self.allowed_functions.join(", ")
));
}
};
if !self.is_allowed(&function) {
return ToolResult::error(format!(
"Function '{}' is not available in view '{}'. Available functions: {}",
function,
self.view_name,
self.allowed_functions.join(", ")
));
}
let params = args
.get("params")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
if self.parent.requires_hitl(&function) {
return ToolResult::ok_with_metadata(
format!(
"Function '{}' on view '{}' requires approval before execution.",
function, self.view_name
),
HashMap::from([
("_hitl_required".to_string(), serde_json::json!(true)),
("_hitl_function".to_string(), serde_json::json!(function)),
("_hitl_params".to_string(), params.clone()),
("_hitl_tool".to_string(), serde_json::json!(self.view_name)),
]),
);
}
let mut result = self.parent.call_function(&function, params).await;
let metadata = result.metadata.get_or_insert_with(HashMap::new);
metadata.insert(
"mcp_view_id".to_string(),
serde_json::json!(ctx.canonical_id),
);
metadata.insert(
"mcp_parent_id".to_string(),
serde_json::json!(self.parent.id()),
);
metadata.insert("mcp_function".to_string(), serde_json::json!(function));
result
}
}