use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Map, Value, json};
use super::{InputState, Mcp, McpResult, Task, apps};
use crate::error::{Error, Result};
use crate::message::ToolCall;
use crate::tool::{Tool, ToolError, ToolResult};
pub(crate) type FixedArgument = Arc<dyn Fn() -> Value + Send + Sync>;
pub(crate) type Wrap = Arc<dyn Fn(&McpResult, &Map<String, Value>) -> ToolResult + Send + Sync>;
#[derive(Clone, Default)]
pub struct ToolShape {
pub(crate) as_name: Option<String>,
pub(crate) description: Option<String>,
pub(crate) fixed_arguments: Vec<(String, FixedArgument)>,
pub(crate) wrap: Option<Wrap>,
}
impl ToolShape {
pub fn new() -> ToolShape {
ToolShape::default()
}
pub fn as_name(mut self, name: impl Into<String>) -> ToolShape {
self.as_name = Some(name.into());
self
}
pub fn description(mut self, text: impl Into<String>) -> ToolShape {
self.description = Some(text.into());
self
}
pub fn fixed_argument(self, name: impl Into<String>, value: Value) -> ToolShape {
self.fixed_argument_with(name, move || value.clone())
}
pub fn fixed_argument_with(
mut self,
name: impl Into<String>,
value: impl Fn() -> Value + Send + Sync + 'static,
) -> ToolShape {
self.fixed_arguments.push((name.into(), Arc::new(value)));
self
}
pub fn wrap<R: Into<ToolResult>>(
mut self,
wrap: impl Fn(&McpResult, &Map<String, Value>) -> R + Send + Sync + 'static,
) -> ToolShape {
self.wrap = Some(Arc::new(move |result, arguments| {
wrap(result, arguments).into()
}));
self
}
pub(crate) fn merge(mut self, other: &ToolShape) -> ToolShape {
if other.as_name.is_some() {
self.as_name = other.as_name.clone();
}
if other.description.is_some() {
self.description = other.description.clone();
}
self.fixed_arguments
.extend(other.fixed_arguments.iter().cloned());
if other.wrap.is_some() {
self.wrap = other.wrap.clone();
}
self
}
}
pub struct McpTool {
mcp: Mcp,
pub name: String,
pub description: Option<String>,
pub server_name: String,
pub parameters_schema: Value,
pub meta: Map<String, Value>,
pub ui_uri: Option<String>,
pub visibility: Vec<String>,
pub(crate) fixed_arguments: Vec<(String, FixedArgument)>,
pub(crate) wrap: Option<Wrap>,
annotations: Value,
}
impl std::fmt::Debug for McpTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("McpTool");
d.field("name", &self.name);
if self.server_name != self.name {
d.field("from", &self.server_name);
}
if self.is_read_only() {
d.field("read_only", &true);
}
d.finish()
}
}
impl McpTool {
pub fn new(mcp: Mcp, definition: &Value, prefix: Option<&str>, shape: ToolShape) -> McpTool {
let server_name = definition
.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let name = shape.as_name.clone().unwrap_or_else(|| match prefix {
Some(prefix) => format!("{prefix}_{server_name}"),
None => server_name.clone(),
});
let description = shape.description.clone().or_else(|| {
definition
.get("description")
.and_then(Value::as_str)
.map(str::to_string)
});
let schema = definition
.get("inputSchema")
.cloned()
.unwrap_or_else(|| json!({}));
let parameters_schema = model_schema(schema, &shape.fixed_arguments);
let meta = definition
.get("_meta")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
McpTool {
mcp,
name,
description,
server_name,
parameters_schema,
ui_uri: apps::uri(&meta),
visibility: apps::visibility(&meta),
meta,
fixed_arguments: shape.fixed_arguments,
wrap: shape.wrap,
annotations: definition
.get("annotations")
.cloned()
.unwrap_or_else(|| json!({})),
}
}
fn hint(&self, key: &str) -> Option<bool> {
self.annotations.get(key).and_then(Value::as_bool)
}
pub fn is_read_only(&self) -> bool {
self.hint("readOnlyHint") == Some(true)
}
pub fn is_destructive(&self) -> bool {
!self.is_read_only() && self.hint("destructiveHint") != Some(false)
}
pub fn is_idempotent(&self) -> bool {
self.hint("idempotentHint") == Some(true)
}
pub fn is_open_world(&self) -> bool {
self.hint("openWorldHint") != Some(false)
}
pub async fn call(&self, arguments: Value) -> Result<ToolResult> {
let arguments = match arguments {
Value::Object(map) => map,
_ => Map::new(),
};
self.mcp.run(self, arguments, None).await
}
pub fn task(&self, state: &Value, tool_call: Option<ToolCall>) -> Task {
Task::load(Some(self.mcp.clone()), state, tool_call)
}
}
fn model_schema(mut schema: Value, fixed: &[(String, FixedArgument)]) -> Value {
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
obj.remove("title");
}
if fixed.is_empty() || schema.get("properties").is_none() {
return schema;
}
let hidden: Vec<&str> = fixed.iter().map(|(name, _)| name.as_str()).collect();
if let Some(Value::Object(properties)) = schema.get_mut("properties") {
properties.retain(|name, _| !hidden.contains(&name.as_str()));
}
let required: Vec<Value> = schema
.get("required")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|name| !name.as_str().is_some_and(|n| hidden.contains(&n)))
.cloned()
.collect();
schema["required"] = Value::Array(required);
schema
}
fn boxed(error: Error) -> ToolError {
Box::new(error)
}
#[async_trait]
impl Tool for McpTool {
fn name(&self) -> String {
self.name.clone()
}
fn description(&self) -> String {
self.description.clone().unwrap_or_default()
}
fn parameters_schema(&self) -> Option<Value> {
Some(self.parameters_schema.clone())
}
fn requires_approval(&self) -> bool {
self.mcp.requires_approval(self)
}
fn is_model_visible(&self) -> bool {
self.visibility.iter().any(|v| v == "model")
}
fn mcp_task(&self, state: &Value, tool_call: &ToolCall) -> Option<Task> {
Some(self.task(state, Some(tool_call.clone())))
}
async fn execute(
&self,
arguments: Map<String, Value>,
_tool_call: &ToolCall,
) -> Result<ToolResult, ToolError> {
self.mcp.run(self, arguments, None).await.map_err(boxed)
}
async fn resume(
&self,
input: &Value,
arguments: Map<String, Value>,
_tool_call: &ToolCall,
) -> Result<ToolResult, ToolError> {
self.mcp
.run(self, arguments, Some(InputState::from_h(input)))
.await
.map_err(boxed)
}
}