feat(protocols): MCP tool adapter + Tauri commands + initialize bug fix
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
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
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>
This commit is contained in:
@@ -7,12 +7,14 @@
|
|||||||
|
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod mcp_types;
|
mod mcp_types;
|
||||||
|
mod mcp_tool_adapter;
|
||||||
mod mcp_transport;
|
mod mcp_transport;
|
||||||
#[cfg(feature = "a2a")]
|
#[cfg(feature = "a2a")]
|
||||||
mod a2a;
|
mod a2a;
|
||||||
|
|
||||||
pub use mcp::*;
|
pub use mcp::*;
|
||||||
pub use mcp_types::*;
|
pub use mcp_types::*;
|
||||||
|
pub use mcp_tool_adapter::*;
|
||||||
pub use mcp_transport::*;
|
pub use mcp_transport::*;
|
||||||
#[cfg(feature = "a2a")]
|
#[cfg(feature = "a2a")]
|
||||||
pub use a2a::*;
|
pub use a2a::*;
|
||||||
|
|||||||
153
crates/zclaw-protocols/src/mcp_tool_adapter.rs
Normal file
153
crates/zclaw-protocols/src/mcp_tool_adapter.rs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
//! 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -176,11 +176,41 @@ impl McpTransport {
|
|||||||
self.start().await?;
|
self.start().await?;
|
||||||
|
|
||||||
let request = InitializeRequest::default();
|
let request = InitializeRequest::default();
|
||||||
let _: InitializeResult = self.send_request("initialize", Some(&request)).await?;
|
let init_result: InitializeResult = self.send_request("initialize", Some(&request)).await?;
|
||||||
|
|
||||||
// Store capabilities
|
// Store actual server capabilities (not empty defaults)
|
||||||
let mut capabilities = self.capabilities.lock().await;
|
let mut capabilities = self.capabilities.lock().await;
|
||||||
*capabilities = Some(ServerCapabilities::default());
|
*capabilities = Some(init_result.capabilities);
|
||||||
|
drop(capabilities);
|
||||||
|
|
||||||
|
// Send initialized notification (required by MCP spec)
|
||||||
|
self.send_notification(&InitializedNotification::new()).await?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
server = %init_result.server_info.name,
|
||||||
|
version = %init_result.server_info.version,
|
||||||
|
protocol = %init_result.protocol_version,
|
||||||
|
"MCP server initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send JSON-RPC notification (no response expected)
|
||||||
|
async fn send_notification(&self, notification: &impl serde::Serialize) -> Result<()> {
|
||||||
|
let line = serde_json::to_string(notification)
|
||||||
|
.map_err(|e| ZclawError::McpError(format!("Failed to serialize notification: {}", e)))?;
|
||||||
|
|
||||||
|
let mut stdin_guard = self.stdin.lock().await;
|
||||||
|
let stdin = stdin_guard.as_mut()
|
||||||
|
.ok_or_else(|| ZclawError::McpError("Transport not started".to_string()))?;
|
||||||
|
|
||||||
|
stdin.write_all(line.as_bytes())
|
||||||
|
.map_err(|e| ZclawError::McpError(format!("Failed to write notification: {}", e)))?;
|
||||||
|
stdin.write_all(b"\n")
|
||||||
|
.map_err(|e| ZclawError::McpError(format!("Failed to write newline: {}", e)))?;
|
||||||
|
stdin.flush()
|
||||||
|
.map_err(|e| ZclawError::McpError(format!("Failed to flush notification: {}", e)))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ zclaw-skills = { workspace = true }
|
|||||||
zclaw-hands = { workspace = true }
|
zclaw-hands = { workspace = true }
|
||||||
zclaw-pipeline = { workspace = true }
|
zclaw-pipeline = { workspace = true }
|
||||||
zclaw-growth = { workspace = true }
|
zclaw-growth = { workspace = true }
|
||||||
|
zclaw-protocols = { workspace = true }
|
||||||
|
|
||||||
# Tauri
|
# Tauri
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
|
|||||||
155
desktop/src-tauri/src/kernel_commands/mcp.rs
Normal file
155
desktop/src-tauri/src/kernel_commands/mcp.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
//! MCP (Model Context Protocol) Tauri commands
|
||||||
|
//!
|
||||||
|
//! Manages MCP server lifecycle: start/stop servers, discover tools.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::State;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use zclaw_protocols::{BasicMcpClient, McpServerConfig, McpServiceManager};
|
||||||
|
|
||||||
|
/// Shared MCP service manager state (newtype for Tauri state management)
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct McpManagerState(pub Arc<Mutex<McpServiceManager>>);
|
||||||
|
|
||||||
|
/// MCP service configuration (from frontend)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct McpServiceConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub command: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub env: HashMap<String, String>,
|
||||||
|
pub cwd: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP tool info (returned to frontend)
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct McpToolInfo {
|
||||||
|
pub service_name: String,
|
||||||
|
pub tool_name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub input_schema: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP service status
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct McpServiceStatus {
|
||||||
|
pub name: String,
|
||||||
|
pub tool_count: usize,
|
||||||
|
pub tools: Vec<McpToolInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Start an MCP server and discover its tools
|
||||||
|
/// @connected — frontend: MCPServices.tsx via mcp-client.ts
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn mcp_start_service(
|
||||||
|
manager: State<'_, McpManagerState>,
|
||||||
|
config: McpServiceConfig,
|
||||||
|
) -> Result<Vec<McpToolInfo>, String> {
|
||||||
|
let mut guard = manager.0.lock().await;
|
||||||
|
|
||||||
|
let server_config = McpServerConfig {
|
||||||
|
command: config.command,
|
||||||
|
args: config.args,
|
||||||
|
env: config.env,
|
||||||
|
cwd: config.cwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = Arc::new(BasicMcpClient::new(server_config));
|
||||||
|
client
|
||||||
|
.initialize()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to initialize MCP service '{}': {}", config.name, e))?;
|
||||||
|
|
||||||
|
info!(service = %config.name, "MCP service initialized");
|
||||||
|
|
||||||
|
let adapters = guard
|
||||||
|
.register_service(config.name.clone(), client)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to register MCP service '{}': {}", config.name, e))?;
|
||||||
|
|
||||||
|
let tools: Vec<McpToolInfo> = adapters
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| McpToolInfo {
|
||||||
|
service_name: config.name.clone(),
|
||||||
|
tool_name: a.name().to_string(),
|
||||||
|
description: a.description().to_string(),
|
||||||
|
input_schema: a.input_schema().clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!(service = %config.name, tool_count = tools.len(), "MCP tools registered");
|
||||||
|
Ok(tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop an MCP server and remove its tools
|
||||||
|
/// @connected — frontend: MCPServices.tsx via mcp-client.ts
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn mcp_stop_service(
|
||||||
|
manager: State<'_, McpManagerState>,
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut guard = manager.0.lock().await;
|
||||||
|
guard.remove_service(&name);
|
||||||
|
info!(service = %name, "MCP service stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all active MCP services and their tools
|
||||||
|
/// @connected — frontend: MCPServices.tsx via mcp-client.ts
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn mcp_list_services(
|
||||||
|
manager: State<'_, McpManagerState>,
|
||||||
|
) -> Result<Vec<McpServiceStatus>, String> {
|
||||||
|
let guard = manager.0.lock().await;
|
||||||
|
let names = guard.service_names();
|
||||||
|
let all = guard.all_adapters();
|
||||||
|
let statuses: Vec<McpServiceStatus> = names
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| {
|
||||||
|
let tools: Vec<McpToolInfo> = all
|
||||||
|
.iter()
|
||||||
|
.map(|a| McpToolInfo {
|
||||||
|
service_name: name.to_string(),
|
||||||
|
tool_name: a.name().to_string(),
|
||||||
|
description: a.description().to_string(),
|
||||||
|
input_schema: a.input_schema().clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
McpServiceStatus {
|
||||||
|
name: name.to_string(),
|
||||||
|
tool_count: tools.len(),
|
||||||
|
tools,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(statuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call an MCP tool directly
|
||||||
|
/// @connected — frontend: agent loop via mcp-client.ts
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn mcp_call_tool(
|
||||||
|
manager: State<'_, McpManagerState>,
|
||||||
|
tool_name: String,
|
||||||
|
arguments: serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let guard = manager.0.lock().await;
|
||||||
|
let adapter = guard
|
||||||
|
.all_adapters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|a| a.name() == tool_name)
|
||||||
|
.ok_or_else(|| format!("MCP tool '{}' not found", tool_name))?;
|
||||||
|
|
||||||
|
adapter
|
||||||
|
.execute(arguments)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("MCP tool '{}' execution failed: {}", tool_name, e))
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ pub mod approval;
|
|||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod hand;
|
pub mod hand;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
|
pub mod mcp;
|
||||||
pub mod scheduled_task;
|
pub mod scheduled_task;
|
||||||
pub mod skill;
|
pub mod skill;
|
||||||
pub mod trigger;
|
pub mod trigger;
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ pub fn run() {
|
|||||||
.manage(classroom_state)
|
.manage(classroom_state)
|
||||||
.manage(classroom_chat_state)
|
.manage(classroom_chat_state)
|
||||||
.manage(classroom_gen_tasks)
|
.manage(classroom_gen_tasks)
|
||||||
|
.manage(kernel_commands::mcp::McpManagerState::default())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Internal ZCLAW Kernel commands (preferred)
|
// Internal ZCLAW Kernel commands (preferred)
|
||||||
kernel_commands::lifecycle::kernel_init,
|
kernel_commands::lifecycle::kernel_init,
|
||||||
@@ -323,7 +324,12 @@ pub fn run() {
|
|||||||
classroom_commands::generate::classroom_list,
|
classroom_commands::generate::classroom_list,
|
||||||
classroom_commands::chat::classroom_chat,
|
classroom_commands::chat::classroom_chat,
|
||||||
classroom_commands::chat::classroom_chat_history,
|
classroom_commands::chat::classroom_chat_history,
|
||||||
classroom_commands::export::classroom_export
|
classroom_commands::export::classroom_export,
|
||||||
|
// MCP (Model Context Protocol) lifecycle commands
|
||||||
|
kernel_commands::mcp::mcp_start_service,
|
||||||
|
kernel_commands::mcp::mcp_stop_service,
|
||||||
|
kernel_commands::mcp::mcp_list_services,
|
||||||
|
kernel_commands::mcp::mcp_call_tool,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
Reference in New Issue
Block a user