use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::Mutex;
use super::types::{McpContent, McpToolDefinition};
use super::McpClient;
use crate::tools::{Tool, ToolContext, ToolRegistry, ToolResult};
pub struct McpToolBridge {
name_raw: String,
name_prefixed: String,
description: String,
input_schema: Value,
client: Arc<Mutex<McpClient>>,
}
impl McpToolBridge {
pub fn new(def: McpToolDefinition, client: Arc<Mutex<McpClient>>) -> Self {
let name_prefixed = format!("mcp_{}", def.name);
McpToolBridge {
name_raw: def.name,
name_prefixed,
description: def.description.unwrap_or_default(),
input_schema: def.input_schema,
client,
}
}
#[allow(dead_code)]
pub fn raw_name(&self) -> &str {
&self.name_raw
}
}
#[async_trait]
impl Tool for McpToolBridge {
fn name(&self) -> &str {
&self.name_prefixed
}
fn description(&self) -> &str {
&self.description
}
fn parameters_schema(&self) -> Value {
self.input_schema.clone()
}
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<ToolResult> {
let mut client = self.client.lock().await;
match client.call_tool(&self.name_raw, args).await {
Ok(result) => {
let is_error = result.is_error;
let output = format_mcp_result(&result.content, is_error);
Ok(ToolResult { output, is_error })
}
Err(e) => {
Ok(ToolResult {
output: format!("MCP tool '{}' call failed: {}", self.name_raw, e),
is_error: true,
})
}
}
}
}
pub fn format_mcp_result(content: &[McpContent], is_error: bool) -> String {
let mut parts: Vec<String> = Vec::new();
for block in content {
match block {
McpContent::Text { text } => {
parts.push(text.clone());
}
McpContent::Image { mime_type, .. } => {
parts.push(format!("[image: {}]", mime_type));
}
McpContent::Resource { resource } => {
parts.push(format!("[resource: {}]", resource.uri));
}
}
}
if parts.is_empty() {
if is_error {
"(MCP tool error — no details provided)".to_string()
} else {
"(MCP tool returned no output)".to_string()
}
} else {
parts.join("\n")
}
}
pub async fn register_mcp_tools(
client: Arc<Mutex<McpClient>>,
registry: &mut ToolRegistry,
) -> Result<usize> {
let tool_defs = {
let mut locked = client.lock().await;
locked.list_tools().await?
};
let count = tool_defs.len();
for def in tool_defs {
let bridge = McpToolBridge::new(def, Arc::clone(&client));
registry.register(Box::new(bridge));
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_def(name: &str, description: Option<&str>, schema: Value) -> McpToolDefinition {
McpToolDefinition {
name: name.to_string(),
description: description.map(|s| s.to_string()),
input_schema: schema,
}
}
#[test]
fn test_bridge_name_prefixing() {
let raw = "read_file";
let prefixed = format!("mcp_{}", raw);
assert_eq!(prefixed, "mcp_read_file");
}
#[test]
fn test_bridge_description_fallback() {
let def = make_def("ping", None, json!({"type": "object"}));
let desc = def.description.unwrap_or_default();
assert_eq!(desc, "");
}
#[test]
fn test_bridge_description_present() {
let def = make_def("read_file", Some("Read a file"), json!({"type": "object"}));
let desc = def.description.unwrap_or_default();
assert_eq!(desc, "Read a file");
}
#[test]
fn test_bridge_schema_forwarded() {
let schema = json!({
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
});
let def = make_def("read_file", Some("Read"), schema.clone());
assert_eq!(def.input_schema, schema);
}
#[test]
fn test_format_single_text() {
let content = vec![McpContent::Text {
text: "hello world".to_string(),
}];
let result = format_mcp_result(&content, false);
assert_eq!(result, "hello world");
}
#[test]
fn test_format_multiple_text() {
let content = vec![
McpContent::Text {
text: "line one".to_string(),
},
McpContent::Text {
text: "line two".to_string(),
},
];
let result = format_mcp_result(&content, false);
assert_eq!(result, "line one\nline two");
}
#[test]
fn test_format_image_placeholder() {
let content = vec![McpContent::Image {
data: "aGVsbG8=".to_string(),
mime_type: "image/png".to_string(),
}];
let result = format_mcp_result(&content, false);
assert_eq!(result, "[image: image/png]");
}
#[test]
fn test_format_resource_annotation() {
let content = vec![McpContent::Resource {
resource: super::super::types::McpResource {
uri: "file:///notes.txt".to_string(),
name: "notes.txt".to_string(),
description: None,
mime_type: None,
},
}];
let result = format_mcp_result(&content, false);
assert_eq!(result, "[resource: file:///notes.txt]");
}
#[test]
fn test_format_empty_error() {
let result = format_mcp_result(&[], true);
assert!(
result.contains("error"),
"Expected error message, got: {}",
result
);
}
#[test]
fn test_format_empty_success() {
let result = format_mcp_result(&[], false);
assert!(
result.contains("no output"),
"Expected 'no output' message, got: {}",
result
);
}
#[test]
fn test_format_mixed_content() {
let content = vec![
McpContent::Text {
text: "result text".to_string(),
},
McpContent::Image {
data: "abc".to_string(),
mime_type: "image/jpeg".to_string(),
},
];
let result = format_mcp_result(&content, false);
assert!(result.contains("result text"));
assert!(result.contains("[image: image/jpeg]"));
}
#[test]
fn test_prefix_contract() {
let raw_names = ["bash", "read_file", "search", "do_something_complex"];
for raw in &raw_names {
let expected = format!("mcp_{}", raw);
assert!(
expected.starts_with("mcp_"),
"Prefixed name must start with 'mcp_'"
);
assert!(
expected.ends_with(raw),
"Prefixed name must end with raw name"
);
}
}
}