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::idempotency::IdempotencyPath;
use crate::outcome::ToolOutcome;
pub struct McpTool {
peer: Peer<RoleClient>,
name: String,
description: String,
input_schema: Value,
output_schema: Option<Value>,
effect: Effect,
idempotency_key: Option<IdempotencyPath>,
}
impl McpTool {
pub(super) fn new(
peer: Peer<RoleClient>,
name: String,
description: String,
input_schema: Value,
output_schema: Option<Value>,
effect: Effect,
idempotency_key: Option<IdempotencyPath>,
) -> Self {
Self {
peer,
name,
description,
input_schema,
output_schema,
effect,
idempotency_key,
}
}
fn declared_key(&self, input: &Value) -> Result<Option<String>, ToolError> {
match &self.idempotency_key {
Some(path) => path.derive(&self.name, input).map(Some),
None => Ok(None),
}
}
}
#[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()
}
fn output_schema(&self) -> Option<Value> {
self.output_schema.clone()
}
fn idempotency_key(&self, input: &Value) -> Option<String> {
self.declared_key(input).ok().flatten()
}
async fn call_json(
&self,
_ctx: &ToolCtx,
input: Value,
) -> Result<ToolOutcome<Value>, ToolError> {
self.declared_key(&input)?;
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")
}
}