fix(runtime): 修复 Skill/MCP 调用链路3个断点
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

1. Anthropic Driver ToolResult 格式修复 — ContentBlock 添加 ToolResult 变体,
   tool_call_id 不再被丢弃, 按 Anthropic API 规范发送 tool_result 格式
2. 前端 callMcpTool 参数名对齐 — serviceName/toolName/args 改为
   service_name/tool_name/arguments, 后端支持 service_name 精确路由
3. MCP 工具桥接到 ToolRegistry — McpToolAdapter 添加 service_name/clone,
   新建 McpToolWrapper 实现 Tool trait, Kernel 添加 mcp_adapters 共享状态,
   McpManagerState 与 Kernel 共享同一 Arc<RwLock<Vec>>, MCP 服务启停时
   自动同步工具列表到 LLM 可见的 ToolRegistry
This commit is contained in:
iven
2026-04-11 16:20:38 +08:00
parent 2843bd204f
commit 9e0aa496cd
12 changed files with 389 additions and 29 deletions

View File

@@ -52,6 +52,8 @@ pub struct Kernel {
viking: Arc<zclaw_runtime::VikingAdapter>,
/// Optional LLM driver for memory extraction (set by Tauri desktop layer)
extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>,
/// MCP tool adapters — shared with Tauri MCP manager, updated dynamically
mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>,
/// A2A router for inter-agent messaging (gated by multi-agent feature)
#[cfg(feature = "multi-agent")]
a2a_router: Arc<A2aRouter>,
@@ -155,14 +157,14 @@ impl Kernel {
running_hand_runs: Arc::new(dashmap::DashMap::new()),
viking,
extraction_driver: None,
#[cfg(feature = "multi-agent")]
mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())), #[cfg(feature = "multi-agent")]
a2a_router,
#[cfg(feature = "multi-agent")]
a2a_inboxes: Arc::new(dashmap::DashMap::new()),
})
}
/// Create a tool registry with built-in tools.
/// Create a tool registry with built-in tools + MCP tools.
/// When `subagent_enabled` is false, TaskTool is excluded to prevent
/// the LLM from attempting sub-agent delegation in non-Ultra modes.
pub(crate) fn create_tool_registry(&self, subagent_enabled: bool) -> ToolRegistry {
@@ -179,6 +181,16 @@ impl Kernel {
tools.register(Box::new(task_tool));
}
// Register MCP tools (dynamically updated by Tauri MCP manager)
if let Ok(adapters) = self.mcp_adapters.read() {
for adapter in adapters.iter() {
let wrapper = zclaw_runtime::tool::builtin::McpToolWrapper::new(
std::sync::Arc::new(adapter.clone())
);
tools.register(Box::new(wrapper));
}
}
tools
}
@@ -405,6 +417,25 @@ impl Kernel {
tracing::info!("[Kernel] Extraction driver configured for Growth system");
self.extraction_driver = Some(driver);
}
/// Get a reference to the shared MCP adapters list.
///
/// The Tauri MCP manager updates this list when services start/stop.
/// The kernel reads it during `create_tool_registry()` to inject MCP tools
/// into the LLM's available tools.
pub fn mcp_adapters(&self) -> Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>> {
self.mcp_adapters.clone()
}
/// Replace the MCP adapters with a shared Arc (from Tauri MCP manager).
///
/// Call this after boot to connect the kernel to the Tauri MCP manager's
/// adapter list. After this, MCP service start/stop will automatically
/// be reflected in the LLM's available tools.
pub fn set_mcp_adapters(&mut self, adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>) {
tracing::info!("[Kernel] MCP adapters bridge connected");
self.mcp_adapters = adapters;
}
}
#[derive(Debug, Clone)]

View File

@@ -20,7 +20,9 @@ use crate::mcp::{McpClient, McpTool, McpToolCallRequest};
/// 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)
/// Service name this tool belongs to
service_name: String,
/// Tool name (original from MCP server, NOT prefixed)
name: String,
/// Tool description
description: String,
@@ -30,9 +32,22 @@ pub struct McpToolAdapter {
client: Arc<dyn McpClient>,
}
impl McpToolAdapter {
pub fn new(tool: McpTool, client: Arc<dyn McpClient>) -> Self {
impl Clone for McpToolAdapter {
fn clone(&self) -> Self {
Self {
service_name: self.service_name.clone(),
name: self.name.clone(),
description: self.description.clone(),
input_schema: self.input_schema.clone(),
client: self.client.clone(),
}
}
}
impl McpToolAdapter {
pub fn new(service_name: String, tool: McpTool, client: Arc<dyn McpClient>) -> Self {
Self {
service_name,
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
@@ -41,16 +56,29 @@ impl McpToolAdapter {
}
/// Create adapters for all tools from an MCP server
pub async fn from_server(client: Arc<dyn McpClient>) -> Result<Vec<Self>> {
pub async fn from_server(service_name: String, 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())
Ok(tools.into_iter().map(|t| Self::new(service_name.clone(), t, client.clone())).collect())
}
pub fn name(&self) -> &str {
&self.name
}
/// Full qualified name: service_name.tool_name (for ToolRegistry to avoid collisions)
pub fn qualified_name(&self) -> String {
format!("{}.{}", self.service_name, self.name)
}
pub fn service_name(&self) -> &str {
&self.service_name
}
pub fn tool_name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
@@ -129,7 +157,7 @@ impl McpServiceManager {
name: String,
client: Arc<dyn McpClient>,
) -> Result<Vec<&McpToolAdapter>> {
let adapters = McpToolAdapter::from_server(client.clone()).await?;
let adapters = McpToolAdapter::from_server(name.clone(), client.clone()).await?;
self.clients.insert(name.clone(), client);
self.adapters.insert(name.clone(), adapters);
Ok(self.adapters.get(&name).unwrap().iter().collect())

View File

@@ -11,6 +11,7 @@ description = "ZCLAW runtime with LLM drivers and agent loop"
zclaw-types = { workspace = true }
zclaw-memory = { workspace = true }
zclaw-growth = { workspace = true }
zclaw-protocols = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }

View File

@@ -231,15 +231,19 @@ impl AnthropicDriver {
input: input.clone(),
}],
}),
zclaw_types::Message::ToolResult { tool_call_id: _, tool: _, output, is_error } => {
let content = if *is_error {
zclaw_types::Message::ToolResult { tool_call_id, tool: _, output, is_error } => {
let content_text = if *is_error {
format!("Error: {}", output)
} else {
output.to_string()
};
Some(AnthropicMessage {
role: "user".to_string(),
content: vec![ContentBlock::Text { text: content }],
content: vec![ContentBlock::ToolResult {
tool_use_id: tool_call_id.clone(),
content: content_text,
is_error: *is_error,
}],
})
}
_ => None,

View File

@@ -116,6 +116,13 @@ pub enum ContentBlock {
Text { text: String },
Thinking { thinking: String },
ToolUse { id: String, name: String, input: serde_json::Value },
/// Anthropic API tool result — must be sent as `role: "user"` with this content block.
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
is_error: bool,
},
}
/// Stop reason

View File

@@ -737,6 +737,9 @@ impl OpenAiDriver {
input: input.clone(),
});
}
ContentBlock::ToolResult { .. } => {
// ToolResult is only used in request messages, never in responses
}
}
}

View File

@@ -9,6 +9,7 @@ mod skill_load;
mod path_validator;
mod task;
mod ask_clarification;
pub mod mcp_tool;
pub use file_read::FileReadTool;
pub use file_write::FileWriteTool;
@@ -19,6 +20,7 @@ pub use skill_load::SkillLoadTool;
pub use path_validator::{PathValidator, PathValidatorConfig};
pub use task::TaskTool;
pub use ask_clarification::AskClarificationTool;
pub use mcp_tool::McpToolWrapper;
use crate::tool::ToolRegistry;

View File

@@ -0,0 +1,48 @@
//! MCP Tool Wrapper — bridges MCP server tools into the ToolRegistry
//!
//! Wraps `McpToolAdapter` (from zclaw-protocols) as a `Tool` trait object
//! so the LLM can discover and call MCP tools during conversations.
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use zclaw_types::Result;
use crate::tool::{Tool, ToolContext};
/// Wraps an MCP tool adapter into the `Tool` trait.
///
/// The wrapper holds an `Arc<McpToolAdapter>` and delegates execution
/// to the adapter, ignoring the `ToolContext` (MCP tools don't need
/// agent_id, workspace, etc.).
pub struct McpToolWrapper {
adapter: Arc<zclaw_protocols::McpToolAdapter>,
/// Cached qualified name (service.tool) for Tool::name()
qualified_name: String,
}
impl McpToolWrapper {
pub fn new(adapter: Arc<zclaw_protocols::McpToolAdapter>) -> Self {
let qualified_name = adapter.qualified_name();
Self { adapter, qualified_name }
}
}
#[async_trait]
impl Tool for McpToolWrapper {
fn name(&self) -> &str {
&self.qualified_name
}
fn description(&self) -> &str {
self.adapter.description()
}
fn input_schema(&self) -> Value {
self.adapter.input_schema().clone()
}
async fn execute(&self, input: Value, _context: &ToolContext) -> Result<Value> {
self.adapter.execute(input).await
}
}