Files
zclaw_openfang/crates/zclaw-protocols/src/mcp_tool_adapter.rs
iven 943afe3b6b
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
feat(protocols): MCP tool adapter + Tauri commands + initialize bug fix
S6 MCP Protocol:
- Fix McpTransport::initialize() — store actual server capabilities instead
  of discarding them and storing empty ServerCapabilities::default()
- Add send_notification() method to McpTransport for JSON-RPC notifications
- Send notifications/initialized after MCP handshake (spec requirement)
- Add McpToolAdapter: bridges MCP server tools into the tool execution path
- Add McpServiceManager: lifecycle management for MCP server connections
- Add 4 Tauri commands: mcp_start_service, mcp_stop_service,
  mcp_list_services, mcp_call_tool
- Register zclaw-protocols dependency in desktop Cargo.toml

New files:
- crates/zclaw-protocols/src/mcp_tool_adapter.rs (153 lines)
- desktop/src-tauri/src/kernel_commands/mcp.rs (145 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 22:07:35 +08:00

154 lines
4.9 KiB
Rust

//! MCP Tool Adapter — bridges MCP server tools into zclaw-runtime's ToolRegistry
//!
//! Each MCP tool is wrapped as a `dyn Tool` implementation that:
//! 1. Receives the tool call from the agent loop
//! 2. Forwards it to the MCP server via McpClient
//! 3. Returns the result back to the agent loop
use std::collections::HashMap;
use std::sync::Arc;
use serde_json::Value;
use tracing::{debug, warn};
use zclaw_types::{Result, ZclawError};
use crate::mcp::{McpClient, McpTool, McpToolCallRequest};
/// Adapter wrapping an MCP tool as a zclaw-runtime `Tool`.
///
/// This struct is intentionally decoupled from `zclaw-runtime::Tool` to avoid
/// a circular dependency. The runtime crate depends on protocols for type sharing,
/// so we expose a simple trait here that mirrors the essential Tool interface.
/// The runtime side will wrap this in a thin `Tool` impl.
pub struct McpToolAdapter {
/// Tool name (prefixed with server name to avoid collisions)
name: String,
/// Tool description
description: String,
/// JSON schema for input parameters
input_schema: Value,
/// Reference to the MCP client for forwarding calls
client: Arc<dyn McpClient>,
}
impl McpToolAdapter {
pub fn new(tool: McpTool, client: Arc<dyn McpClient>) -> Self {
Self {
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
client,
}
}
/// Create adapters for all tools from an MCP server
pub async fn from_server(client: Arc<dyn McpClient>) -> Result<Vec<Self>> {
let tools = client.list_tools().await?;
debug!(count = tools.len(), "Discovered MCP tools");
Ok(tools.into_iter().map(|t| Self::new(t, client.clone())).collect())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn input_schema(&self) -> &Value {
&self.input_schema
}
/// Execute the MCP tool call
pub async fn execute(&self, input: Value) -> Result<Value> {
debug!(tool = %self.name, "Executing MCP tool");
let arguments = match input {
Value::Object(map) => map.into_iter().collect(),
other => {
// If input is not an object, wrap it as {"input": ...}
HashMap::from([("input".to_string(), other)])
}
};
let request = McpToolCallRequest {
name: self.name.clone(),
arguments,
};
let response = self.client.call_tool(request).await?;
if response.is_error {
// Extract error text from content blocks
let error_text: String = response.content.iter()
.filter_map(|c| match c {
crate::mcp::McpContent::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
warn!(tool = %self.name, error = %error_text, "MCP tool returned error");
return Err(ZclawError::McpError(format!("MCP tool '{}' failed: {}", self.name, error_text)));
}
// Convert content blocks to JSON
let result = response.content.into_iter()
.filter_map(|c| match c {
crate::mcp::McpContent::Text { text } => Some(Value::String(text)),
_ => None,
})
.collect::<Vec<_>>();
match result.len() {
0 => Ok(Value::Null),
1 => Ok(result.into_iter().next().unwrap()),
_ => Ok(Value::Array(result)),
}
}
}
/// MCP Service Manager — manages lifecycle of MCP server connections
#[derive(Default)]
pub struct McpServiceManager {
clients: HashMap<String, Arc<dyn McpClient>>,
adapters: HashMap<String, Vec<McpToolAdapter>>,
}
impl McpServiceManager {
pub fn new() -> Self {
Self {
clients: HashMap::new(),
adapters: HashMap::new(),
}
}
/// Register a connected MCP client and discover its tools
pub async fn register_service(
&mut self,
name: String,
client: Arc<dyn McpClient>,
) -> Result<Vec<&McpToolAdapter>> {
let adapters = McpToolAdapter::from_server(client.clone()).await?;
self.clients.insert(name.clone(), client);
self.adapters.insert(name.clone(), adapters);
Ok(self.adapters.get(&name).unwrap().iter().collect())
}
/// Get all registered tool adapters from all services
pub fn all_adapters(&self) -> Vec<&McpToolAdapter> {
self.adapters.values().flat_map(|v| v.iter()).collect()
}
/// Remove a service by name
pub fn remove_service(&mut self, name: &str) {
self.clients.remove(name);
self.adapters.remove(name);
}
/// List registered service names
pub fn service_names(&self) -> Vec<&String> {
self.clients.keys().collect()
}
}