use async_trait::async_trait;
use rmcp::model::CallToolRequestParams;
use rmcp::service::{Peer, RoleClient};
use salvor_core::Effect;
use serde_json::Value;
use crate::DynTool;
use crate::context::ToolCtx;
use crate::error::{HandlerError, ToolError};
use crate::outcome::ToolOutcome;
pub struct McpTool {
peer: Peer<RoleClient>,
name: String,
description: String,
input_schema: Value,
effect: Effect,
}
impl McpTool {
pub(super) fn new(
peer: Peer<RoleClient>,
name: String,
description: String,
input_schema: Value,
effect: Effect,
) -> Self {
Self {
peer,
name,
description,
input_schema,
effect,
}
}
}
#[async_trait]
impl DynTool for McpTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn effect(&self) -> Effect {
self.effect
}
fn input_schema(&self) -> Value {
self.input_schema.clone()
}
async fn call_json(
&self,
_ctx: &ToolCtx,
input: Value,
) -> Result<ToolOutcome<Value>, ToolError> {
let mut params = CallToolRequestParams::new(self.name.clone());
if !input.is_null() {
let map = serde_json::from_value(input).map_err(|source| ToolError::InvalidInput {
tool: self.name.clone(),
source,
})?;
params = params.with_arguments(map);
}
let result = self
.peer
.call_tool(params)
.await
.map_err(|source| ToolError::Handler {
tool: self.name.clone(),
source: HandlerError::new(source),
})?;
if result.is_error == Some(true) {
let message = error_message(&result);
return Err(ToolError::Handler {
tool: self.name.clone(),
source: HandlerError::message(message),
});
}
let value =
serde_json::to_value(&result).map_err(|source| ToolError::OutputSerialization {
tool: self.name.clone(),
source,
})?;
Ok(ToolOutcome::Output(value))
}
}
fn error_message(result: &rmcp::model::CallToolResult) -> String {
let text: Vec<String> = result
.content
.iter()
.filter_map(|block| block.as_text().map(|t| t.text.clone()))
.collect();
if text.is_empty() {
"the MCP tool reported an error with no message".to_owned()
} else {
text.join("\n")
}
}