PicoBot/src/mcp/tool_adapter.rs
oudecheng 644f5f9132 feat: 子代理继承主代理的 MCP 工具
- 为 McpToolWrapper 添加 Clone trait,支持工具实例复用
- 修改 build_subagent_tools 方法,支持传入 MCP 工具列表
- 调整 runtime 构建顺序:先等待 MCP 连接,再将 MCP 工具传递给子代理

子代理现在可以自动使用主代理配置的 MCP 工具(如 filesystem、fetch 等)。
2026-05-26 11:53:40 +08:00

189 lines
5.3 KiB
Rust

//! MCP Tool Adapter - wraps MCP tools as PicoBot tools
use async_trait::async_trait;
use std::sync::Arc;
use rmcp::model::Tool;
use crate::mcp::client::McpClientManager;
use crate::tools::traits::{Tool as PicoBotTool, ToolResult};
/// Wrapper that adapts an MCP tool to PicoBot's Tool trait
#[derive(Clone)]
pub struct McpToolWrapper {
/// The MCP client manager
manager: Arc<McpClientManager>,
/// The server key this tool belongs to (from mcpServers map key)
server_key: String,
/// The original tool name on the MCP server
tool_name: String,
/// The full tool name with namespace (mcp_{key}_{tool})
full_name: String,
/// Tool information from MCP server
tool_info: Tool,
}
impl McpToolWrapper {
/// Create a new tool wrapper
pub fn new(
manager: Arc<McpClientManager>,
server_key: String,
tool_info: Tool,
) -> Self {
let tool_name = tool_info.name.clone().into_owned();
let full_name = format!("mcp_{}_{}", server_key, tool_name);
Self {
manager,
server_key,
tool_name,
full_name,
tool_info,
}
}
/// Get the server key
pub fn server_key(&self) -> &str {
&self.server_key
}
/// Get the original tool name
pub fn original_name(&self) -> &str {
&self.tool_name
}
}
#[async_trait]
impl PicoBotTool for McpToolWrapper {
fn name(&self) -> &str {
&self.full_name
}
fn description(&self) -> &str {
self.tool_info.description.as_deref().unwrap_or("MCP tool")
}
fn parameters_schema(&self) -> serde_json::Value {
// Convert Arc<JsonObject> to serde_json::Value
let schema = (*self.tool_info.input_schema).clone();
serde_json::Value::Object(schema)
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
tracing::debug!(
server_key = %self.server_key,
tool = %self.tool_name,
"Calling MCP tool"
);
let result = self
.manager
.call_tool(&self.server_key, &self.tool_name, args)
.await?;
// Convert MCP CallToolResult to PicoBot ToolResult
let output = extract_text_content(&result);
let is_error = result.is_error.unwrap_or(false);
Ok(ToolResult {
success: !is_error,
output,
error: if is_error {
Some("MCP tool returned error".to_string())
} else {
None
},
})
}
fn read_only(&self) -> bool {
// MCP tools may or may not be read-only; we default to false
// This could be enhanced if MCP servers provide this info via annotations
false
}
}
/// Extract text content from MCP CallToolResult
fn extract_text_content(result: &rmcp::model::CallToolResult) -> String {
let mut text_parts = Vec::new();
for content in &result.content {
if let Some(text) = content.as_text() {
text_parts.push(text.text.clone());
}
}
if text_parts.is_empty() {
// No text content found, try to serialize the whole result
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "Empty result".to_string())
} else {
text_parts.join("\n")
}
}
/// Register all MCP tools from connected servers into a tool registry
pub async fn register_mcp_tools(
manager: Arc<McpClientManager>,
registry: &mut crate::tools::registry::ToolRegistry,
) -> anyhow::Result<()> {
let all_tools = manager.all_tools().await;
for (server_key, tool_info) in all_tools {
let wrapper = McpToolWrapper::new(
manager.clone(),
server_key.clone(),
tool_info,
);
tracing::info!(
name = %wrapper.name(),
server_key = %server_key,
"Registering MCP tool"
);
registry.register(wrapper);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::{CallToolResult, Content};
#[test]
fn test_extract_text_content_from_text() {
let result = CallToolResult::success(vec![
Content::text("Hello"),
Content::text("World"),
]);
let text = extract_text_content(&result);
assert_eq!(text, "Hello\nWorld");
}
#[test]
fn test_extract_text_content_empty() {
let result = CallToolResult::success(vec![]);
let text = extract_text_content(&result);
// When content is empty, the function serializes the result to JSON
// which contains an empty content array
assert!(text.contains("content") || text.contains("Empty result"));
}
#[test]
fn test_mcp_tool_wrapper_name() {
let manager = Arc::new(McpClientManager::new());
// Create a minimal tool info using rmcp's Tool constructor
let schema: serde_json::Map<String, serde_json::Value> = serde_json::json!({"type": "object"})
.as_object()
.unwrap()
.clone();
let tool_info = Tool::new("echo", "Echo tool", schema);
let wrapper = McpToolWrapper::new(manager, "filesystem".to_string(), tool_info);
assert_eq!(wrapper.name(), "mcp_filesystem_echo");
assert_eq!(wrapper.original_name(), "echo");
assert_eq!(wrapper.server_key(), "filesystem");
}
}