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

168
Cargo.lock generated
View File

@@ -1552,11 +1552,13 @@ dependencies = [
"tauri-build", "tauri-build",
"tauri-plugin-mcp", "tauri-plugin-mcp",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-updater",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"toml 0.8.2", "toml 0.8.2",
"tower-http 0.5.2", "tower-http 0.5.2",
"tracing", "tracing",
"tracing-subscriber",
"uuid", "uuid",
"zclaw-growth", "zclaw-growth",
"zclaw-hands", "zclaw-hands",
@@ -2004,6 +2006,17 @@ dependencies = [
"rustc_version 0.4.1", "rustc_version 0.4.1",
] ]
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -3664,6 +3677,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -3964,6 +3983,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"block2", "block2",
"libc",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@@ -3979,6 +3999,18 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.3.2"
@@ -4128,6 +4160,20 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -5139,15 +5185,20 @@ dependencies = [
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower 0.5.3", "tower 0.5.3",
"tower-http 0.6.8", "tower-http 0.6.8",
@@ -5268,6 +5319,18 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.0"
@@ -5278,6 +5341,33 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.10" version = "0.103.10"
@@ -6530,6 +6620,17 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tar"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@@ -6720,6 +6821,39 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http 1.4.0",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver 1.0.27",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time 0.3.47",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.10.1" version = "2.10.1"
@@ -8211,6 +8345,15 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.6" version = "1.0.6"
@@ -9153,6 +9296,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix 1.1.4",
]
[[package]] [[package]]
name = "xcap" name = "xcap"
version = "0.0.4" version = "0.0.4"
@@ -9330,7 +9483,7 @@ dependencies = [
"zclaw-runtime", "zclaw-runtime",
"zclaw-skills", "zclaw-skills",
"zclaw-types", "zclaw-types",
"zip", "zip 2.4.2",
] ]
[[package]] [[package]]
@@ -9420,6 +9573,7 @@ dependencies = [
"uuid", "uuid",
"zclaw-growth", "zclaw-growth",
"zclaw-memory", "zclaw-memory",
"zclaw-protocols",
"zclaw-types", "zclaw-types",
] ]
@@ -9597,6 +9751,18 @@ dependencies = [
"zopfli", "zopfli",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.13.0",
"memchr",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"

View File

@@ -52,6 +52,8 @@ pub struct Kernel {
viking: Arc<zclaw_runtime::VikingAdapter>, viking: Arc<zclaw_runtime::VikingAdapter>,
/// Optional LLM driver for memory extraction (set by Tauri desktop layer) /// Optional LLM driver for memory extraction (set by Tauri desktop layer)
extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>, 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) /// A2A router for inter-agent messaging (gated by multi-agent feature)
#[cfg(feature = "multi-agent")] #[cfg(feature = "multi-agent")]
a2a_router: Arc<A2aRouter>, a2a_router: Arc<A2aRouter>,
@@ -155,14 +157,14 @@ impl Kernel {
running_hand_runs: Arc::new(dashmap::DashMap::new()), running_hand_runs: Arc::new(dashmap::DashMap::new()),
viking, viking,
extraction_driver: None, extraction_driver: None,
#[cfg(feature = "multi-agent")] mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())), #[cfg(feature = "multi-agent")]
a2a_router, a2a_router,
#[cfg(feature = "multi-agent")] #[cfg(feature = "multi-agent")]
a2a_inboxes: Arc::new(dashmap::DashMap::new()), 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 /// When `subagent_enabled` is false, TaskTool is excluded to prevent
/// the LLM from attempting sub-agent delegation in non-Ultra modes. /// the LLM from attempting sub-agent delegation in non-Ultra modes.
pub(crate) fn create_tool_registry(&self, subagent_enabled: bool) -> ToolRegistry { pub(crate) fn create_tool_registry(&self, subagent_enabled: bool) -> ToolRegistry {
@@ -179,6 +181,16 @@ impl Kernel {
tools.register(Box::new(task_tool)); 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 tools
} }
@@ -405,6 +417,25 @@ impl Kernel {
tracing::info!("[Kernel] Extraction driver configured for Growth system"); tracing::info!("[Kernel] Extraction driver configured for Growth system");
self.extraction_driver = Some(driver); 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)] #[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. /// so we expose a simple trait here that mirrors the essential Tool interface.
/// The runtime side will wrap this in a thin `Tool` impl. /// The runtime side will wrap this in a thin `Tool` impl.
pub struct McpToolAdapter { 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, name: String,
/// Tool description /// Tool description
description: String, description: String,
@@ -30,9 +32,22 @@ pub struct McpToolAdapter {
client: Arc<dyn McpClient>, client: Arc<dyn McpClient>,
} }
impl McpToolAdapter { impl Clone for McpToolAdapter {
pub fn new(tool: McpTool, client: Arc<dyn McpClient>) -> Self { fn clone(&self) -> 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, name: tool.name,
description: tool.description, description: tool.description,
input_schema: tool.input_schema, input_schema: tool.input_schema,
@@ -41,16 +56,29 @@ impl McpToolAdapter {
} }
/// Create adapters for all tools from an MCP server /// 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?; let tools = client.list_tools().await?;
debug!(count = tools.len(), "Discovered MCP tools"); 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 { pub fn name(&self) -> &str {
&self.name &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 { pub fn description(&self) -> &str {
&self.description &self.description
} }
@@ -129,7 +157,7 @@ impl McpServiceManager {
name: String, name: String,
client: Arc<dyn McpClient>, client: Arc<dyn McpClient>,
) -> Result<Vec<&McpToolAdapter>> { ) -> 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.clients.insert(name.clone(), client);
self.adapters.insert(name.clone(), adapters); self.adapters.insert(name.clone(), adapters);
Ok(self.adapters.get(&name).unwrap().iter().collect()) 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-types = { workspace = true }
zclaw-memory = { workspace = true } zclaw-memory = { workspace = true }
zclaw-growth = { workspace = true } zclaw-growth = { workspace = true }
zclaw-protocols = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }

View File

@@ -231,15 +231,19 @@ impl AnthropicDriver {
input: input.clone(), input: input.clone(),
}], }],
}), }),
zclaw_types::Message::ToolResult { tool_call_id: _, tool: _, output, is_error } => { zclaw_types::Message::ToolResult { tool_call_id, tool: _, output, is_error } => {
let content = if *is_error { let content_text = if *is_error {
format!("Error: {}", output) format!("Error: {}", output)
} else { } else {
output.to_string() output.to_string()
}; };
Some(AnthropicMessage { Some(AnthropicMessage {
role: "user".to_string(), 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, _ => None,

View File

@@ -116,6 +116,13 @@ pub enum ContentBlock {
Text { text: String }, Text { text: String },
Thinking { thinking: String }, Thinking { thinking: String },
ToolUse { id: String, name: String, input: serde_json::Value }, 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 /// Stop reason

View File

@@ -737,6 +737,9 @@ impl OpenAiDriver {
input: input.clone(), 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 path_validator;
mod task; mod task;
mod ask_clarification; mod ask_clarification;
pub mod mcp_tool;
pub use file_read::FileReadTool; pub use file_read::FileReadTool;
pub use file_write::FileWriteTool; pub use file_write::FileWriteTool;
@@ -19,6 +20,7 @@ pub use skill_load::SkillLoadTool;
pub use path_validator::{PathValidator, PathValidatorConfig}; pub use path_validator::{PathValidator, PathValidatorConfig};
pub use task::TaskTool; pub use task::TaskTool;
pub use ask_clarification::AskClarificationTool; pub use ask_clarification::AskClarificationTool;
pub use mcp_tool::McpToolWrapper;
use crate::tool::ToolRegistry; 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
}
}

View File

@@ -63,6 +63,7 @@ pub async fn kernel_init(
state: State<'_, KernelState>, state: State<'_, KernelState>,
scheduler_state: State<'_, SchedulerState>, scheduler_state: State<'_, SchedulerState>,
heartbeat_state: State<'_, HeartbeatEngineState>, heartbeat_state: State<'_, HeartbeatEngineState>,
mcp_state: State<'_, crate::kernel_commands::mcp::McpManagerState>,
config_request: Option<KernelConfigRequest>, config_request: Option<KernelConfigRequest>,
) -> Result<KernelStatusResponse, String> { ) -> Result<KernelStatusResponse, String> {
let mut kernel_lock = state.lock().await; let mut kernel_lock = state.lock().await;
@@ -168,6 +169,20 @@ pub async fn kernel_init(
kernel.set_extraction_driver(std::sync::Arc::new(extraction_driver)); kernel.set_extraction_driver(std::sync::Arc::new(extraction_driver));
} }
// Bridge MCP adapters — kernel reads from this shared list during
// create_tool_registry() so the LLM can discover MCP tools.
// Share the McpManagerState's Arc with the Kernel so both point to the same list.
{
let shared_arc = mcp_state.kernel_adapters.clone();
// Copy any adapters already in the kernel's default list into the shared one
let kernel_default = kernel.mcp_adapters();
if let (Ok(src), Ok(mut dst)) = (kernel_default.read(), shared_arc.write()) {
*dst = src.clone();
}
kernel.set_mcp_adapters(shared_arc);
tracing::info!("[kernel_init] Bridged MCP adapters to Kernel for LLM tool discovery");
}
// Configure summary driver so the Growth system can generate L0/L1 summaries // Configure summary driver so the Growth system can generate L0/L1 summaries
if let Some(api_key) = config_request.as_ref().and_then(|r| r.api_key.clone()) { if let Some(api_key) = config_request.as_ref().and_then(|r| r.api_key.clone()) {
crate::summarizer_adapter::configure_summary_driver( crate::summarizer_adapter::configure_summary_driver(

View File

@@ -1,6 +1,8 @@
//! MCP (Model Context Protocol) Tauri commands //! MCP (Model Context Protocol) Tauri commands
//! //!
//! Manages MCP server lifecycle: start/stop servers, discover tools. //! Manages MCP server lifecycle: start/stop servers, discover tools.
//! When services change, the kernel's shared adapter list is synced
//! so the LLM can discover and call MCP tools in conversations.
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -9,11 +11,43 @@ use tauri::State;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::info; use tracing::info;
use zclaw_protocols::{BasicMcpClient, McpServerConfig, McpServiceManager}; use zclaw_protocols::{BasicMcpClient, McpServerConfig, McpServiceManager, McpToolAdapter};
/// Shared MCP service manager state (newtype for Tauri state management) /// Shared MCP service manager state (newtype for Tauri state management)
#[derive(Clone, Default)] #[derive(Clone)]
pub struct McpManagerState(pub Arc<Mutex<McpServiceManager>>); pub struct McpManagerState {
pub manager: Arc<Mutex<McpServiceManager>>,
/// Shared with Kernel — updated on every start/stop so
/// `create_tool_registry()` picks up the latest MCP tools.
pub kernel_adapters: Arc<std::sync::RwLock<Vec<McpToolAdapter>>>,
}
impl Default for McpManagerState {
fn default() -> Self {
Self {
manager: Arc::new(Mutex::new(McpServiceManager::new())),
kernel_adapters: Arc::new(std::sync::RwLock::new(Vec::new())),
}
}
}
impl McpManagerState {
/// Create with a pre-allocated kernel_adapters Arc for sharing with Kernel.
pub fn with_shared_adapters(kernel_adapters: Arc<std::sync::RwLock<Vec<McpToolAdapter>>>) -> Self {
Self {
manager: Arc::new(Mutex::new(McpServiceManager::new())),
kernel_adapters,
}
}
/// Sync all adapters from the service manager into the kernel's shared list.
fn sync_to_kernel(&self, mgr: &McpServiceManager) {
let all: Vec<McpToolAdapter> = mgr.all_adapters().into_iter().cloned().collect();
if let Ok(mut guard) = self.kernel_adapters.write() {
*guard = all;
}
}
}
/// MCP service configuration (from frontend) /// MCP service configuration (from frontend)
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -53,7 +87,7 @@ pub async fn mcp_start_service(
manager: State<'_, McpManagerState>, manager: State<'_, McpManagerState>,
config: McpServiceConfig, config: McpServiceConfig,
) -> Result<Vec<McpToolInfo>, String> { ) -> Result<Vec<McpToolInfo>, String> {
let mut guard = manager.0.lock().await; let mut guard = manager.manager.lock().await;
let server_config = McpServerConfig { let server_config = McpServerConfig {
command: config.command, command: config.command,
@@ -78,13 +112,16 @@ pub async fn mcp_start_service(
let tools: Vec<McpToolInfo> = adapters let tools: Vec<McpToolInfo> = adapters
.into_iter() .into_iter()
.map(|a| McpToolInfo { .map(|a| McpToolInfo {
service_name: config.name.clone(), service_name: a.service_name().to_string(),
tool_name: a.name().to_string(), tool_name: a.tool_name().to_string(),
description: a.description().to_string(), description: a.description().to_string(),
input_schema: a.input_schema().clone(), input_schema: a.input_schema().clone(),
}) })
.collect(); .collect();
// Sync adapters to kernel so LLM can see the new tools
manager.sync_to_kernel(&guard);
info!(service = %config.name, tool_count = tools.len(), "MCP tools registered"); info!(service = %config.name, tool_count = tools.len(), "MCP tools registered");
Ok(tools) Ok(tools)
} }
@@ -96,8 +133,12 @@ pub async fn mcp_stop_service(
manager: State<'_, McpManagerState>, manager: State<'_, McpManagerState>,
name: String, name: String,
) -> Result<(), String> { ) -> Result<(), String> {
let mut guard = manager.0.lock().await; let mut guard = manager.manager.lock().await;
guard.remove_service(&name); guard.remove_service(&name);
// Sync adapters to kernel so LLM no longer sees removed tools
manager.sync_to_kernel(&guard);
info!(service = %name, "MCP service stopped"); info!(service = %name, "MCP service stopped");
Ok(()) Ok(())
} }
@@ -108,7 +149,7 @@ pub async fn mcp_stop_service(
pub async fn mcp_list_services( pub async fn mcp_list_services(
manager: State<'_, McpManagerState>, manager: State<'_, McpManagerState>,
) -> Result<Vec<McpServiceStatus>, String> { ) -> Result<Vec<McpServiceStatus>, String> {
let guard = manager.0.lock().await; let guard = manager.manager.lock().await;
let names = guard.service_names(); let names = guard.service_names();
let all = guard.all_adapters(); let all = guard.all_adapters();
let statuses: Vec<McpServiceStatus> = names let statuses: Vec<McpServiceStatus> = names
@@ -116,9 +157,10 @@ pub async fn mcp_list_services(
.map(|name| { .map(|name| {
let tools: Vec<McpToolInfo> = all let tools: Vec<McpToolInfo> = all
.iter() .iter()
.filter(|a| a.service_name() == name)
.map(|a| McpToolInfo { .map(|a| McpToolInfo {
service_name: name.to_string(), service_name: a.service_name().to_string(),
tool_name: a.name().to_string(), tool_name: a.tool_name().to_string(),
description: a.description().to_string(), description: a.description().to_string(),
input_schema: a.input_schema().clone(), input_schema: a.input_schema().clone(),
}) })
@@ -138,15 +180,24 @@ pub async fn mcp_list_services(
#[tauri::command] #[tauri::command]
pub async fn mcp_call_tool( pub async fn mcp_call_tool(
manager: State<'_, McpManagerState>, manager: State<'_, McpManagerState>,
service_name: Option<String>,
tool_name: String, tool_name: String,
arguments: serde_json::Value, arguments: serde_json::Value,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let guard = manager.0.lock().await; let guard = manager.manager.lock().await;
let adapter = guard let adapter = if let Some(ref svc) = service_name {
.all_adapters() // Route by service name + tool name for precision
.into_iter() guard.all_adapters()
.find(|a| a.name() == tool_name) .into_iter()
.ok_or_else(|| format!("MCP tool '{}' not found", tool_name))?; .find(|a| a.service_name() == svc && a.tool_name() == tool_name)
.ok_or_else(|| format!("MCP tool '{}' in service '{}' not found", tool_name, svc))?
} else {
// Fallback: match by tool name only (first match)
guard.all_adapters()
.into_iter()
.find(|a| a.tool_name() == tool_name)
.ok_or_else(|| format!("MCP tool '{}' not found", tool_name))?
};
adapter adapter
.execute(arguments) .execute(arguments)

View File

@@ -73,5 +73,9 @@ export async function callMcpTool(
args: Record<string, unknown> args: Record<string, unknown>
): Promise<unknown> { ): Promise<unknown> {
log.info('callMcpTool', { serviceName, toolName }); log.info('callMcpTool', { serviceName, toolName });
return invoke<unknown>('mcp_call_tool', { serviceName, toolName, args }); return invoke<unknown>('mcp_call_tool', {
service_name: serviceName,
tool_name: toolName,
arguments: args,
});
} }