From 9e0aa496cdc03551dbbbede0c88a6aef0b92bab0 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 16:20:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(runtime):=20=E4=BF=AE=E5=A4=8D=20Skill/MCP?= =?UTF-8?q?=20=E8=B0=83=E7=94=A8=E9=93=BE=E8=B7=AF3=E4=B8=AA=E6=96=AD?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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>, MCP 服务启停时 自动同步工具列表到 LLM 可见的 ToolRegistry --- Cargo.lock | 168 +++++++++++++++++- crates/zclaw-kernel/src/kernel/mod.rs | 35 +++- .../zclaw-protocols/src/mcp_tool_adapter.rs | 40 ++++- crates/zclaw-runtime/Cargo.toml | 1 + crates/zclaw-runtime/src/driver/anthropic.rs | 10 +- crates/zclaw-runtime/src/driver/mod.rs | 7 + crates/zclaw-runtime/src/driver/openai.rs | 3 + crates/zclaw-runtime/src/tool/builtin.rs | 2 + .../src/tool/builtin/mcp_tool.rs | 48 +++++ .../src/kernel_commands/lifecycle.rs | 15 ++ desktop/src-tauri/src/kernel_commands/mcp.rs | 83 +++++++-- desktop/src/lib/mcp-client.ts | 6 +- 12 files changed, 389 insertions(+), 29 deletions(-) create mode 100644 crates/zclaw-runtime/src/tool/builtin/mcp_tool.rs diff --git a/Cargo.lock b/Cargo.lock index e43076e..3e53d22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1552,11 +1552,13 @@ dependencies = [ "tauri-build", "tauri-plugin-mcp", "tauri-plugin-opener", + "tauri-plugin-updater", "thiserror 2.0.18", "tokio", "toml 0.8.2", "tower-http 0.5.2", "tracing", + "tracing-subscriber", "uuid", "zclaw-growth", "zclaw-hands", @@ -2004,6 +2006,17 @@ dependencies = [ "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]] name = "find-msvc-tools" version = "0.1.9" @@ -3664,6 +3677,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3964,6 +3983,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -3979,6 +3999,18 @@ dependencies = [ "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]] name = "objc2-quartz-core" version = "0.3.2" @@ -4128,6 +4160,20 @@ dependencies = [ "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]] name = "pango" version = "0.18.3" @@ -5139,15 +5185,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower 0.5.3", "tower-http 0.6.8", @@ -5268,6 +5319,18 @@ dependencies = [ "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]] name = "rustls-pki-types" version = "1.14.0" @@ -5278,6 +5341,33 @@ dependencies = [ "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]] name = "rustls-webpki" version = "0.103.10" @@ -6530,6 +6620,17 @@ dependencies = [ "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]] name = "target-lexicon" version = "0.12.16" @@ -6720,6 +6821,39 @@ dependencies = [ "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]] name = "tauri-runtime" version = "2.10.1" @@ -8211,6 +8345,15 @@ dependencies = [ "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]] name = "webpki-roots" version = "1.0.6" @@ -9153,6 +9296,16 @@ dependencies = [ "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]] name = "xcap" version = "0.0.4" @@ -9330,7 +9483,7 @@ dependencies = [ "zclaw-runtime", "zclaw-skills", "zclaw-types", - "zip", + "zip 2.4.2", ] [[package]] @@ -9420,6 +9573,7 @@ dependencies = [ "uuid", "zclaw-growth", "zclaw-memory", + "zclaw-protocols", "zclaw-types", ] @@ -9597,6 +9751,18 @@ dependencies = [ "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]] name = "zmij" version = "1.0.21" diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index 2d76741..05d4402 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -52,6 +52,8 @@ pub struct Kernel { viking: Arc, /// Optional LLM driver for memory extraction (set by Tauri desktop layer) extraction_driver: Option>, + /// MCP tool adapters — shared with Tauri MCP manager, updated dynamically + mcp_adapters: Arc>>, /// A2A router for inter-agent messaging (gated by multi-agent feature) #[cfg(feature = "multi-agent")] a2a_router: Arc, @@ -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>> { + 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>>) { + tracing::info!("[Kernel] MCP adapters bridge connected"); + self.mcp_adapters = adapters; + } } #[derive(Debug, Clone)] diff --git a/crates/zclaw-protocols/src/mcp_tool_adapter.rs b/crates/zclaw-protocols/src/mcp_tool_adapter.rs index 00037e9..0f44835 100644 --- a/crates/zclaw-protocols/src/mcp_tool_adapter.rs +++ b/crates/zclaw-protocols/src/mcp_tool_adapter.rs @@ -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, } -impl McpToolAdapter { - pub fn new(tool: McpTool, client: Arc) -> 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) -> 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) -> Result> { + pub async fn from_server(service_name: String, client: Arc) -> Result> { 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, ) -> Result> { - 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()) diff --git a/crates/zclaw-runtime/Cargo.toml b/crates/zclaw-runtime/Cargo.toml index c056276..2111a07 100644 --- a/crates/zclaw-runtime/Cargo.toml +++ b/crates/zclaw-runtime/Cargo.toml @@ -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 } diff --git a/crates/zclaw-runtime/src/driver/anthropic.rs b/crates/zclaw-runtime/src/driver/anthropic.rs index 2f855ba..e943d88 100644 --- a/crates/zclaw-runtime/src/driver/anthropic.rs +++ b/crates/zclaw-runtime/src/driver/anthropic.rs @@ -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, diff --git a/crates/zclaw-runtime/src/driver/mod.rs b/crates/zclaw-runtime/src/driver/mod.rs index 098d019..b9b8a78 100644 --- a/crates/zclaw-runtime/src/driver/mod.rs +++ b/crates/zclaw-runtime/src/driver/mod.rs @@ -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 diff --git a/crates/zclaw-runtime/src/driver/openai.rs b/crates/zclaw-runtime/src/driver/openai.rs index 8df29d7..6dcbe4d 100644 --- a/crates/zclaw-runtime/src/driver/openai.rs +++ b/crates/zclaw-runtime/src/driver/openai.rs @@ -737,6 +737,9 @@ impl OpenAiDriver { input: input.clone(), }); } + ContentBlock::ToolResult { .. } => { + // ToolResult is only used in request messages, never in responses + } } } diff --git a/crates/zclaw-runtime/src/tool/builtin.rs b/crates/zclaw-runtime/src/tool/builtin.rs index 92fe20f..98cac4c 100644 --- a/crates/zclaw-runtime/src/tool/builtin.rs +++ b/crates/zclaw-runtime/src/tool/builtin.rs @@ -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; diff --git a/crates/zclaw-runtime/src/tool/builtin/mcp_tool.rs b/crates/zclaw-runtime/src/tool/builtin/mcp_tool.rs new file mode 100644 index 0000000..1e511ca --- /dev/null +++ b/crates/zclaw-runtime/src/tool/builtin/mcp_tool.rs @@ -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` and delegates execution +/// to the adapter, ignoring the `ToolContext` (MCP tools don't need +/// agent_id, workspace, etc.). +pub struct McpToolWrapper { + adapter: Arc, + /// Cached qualified name (service.tool) for Tool::name() + qualified_name: String, +} + +impl McpToolWrapper { + pub fn new(adapter: Arc) -> 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 { + self.adapter.execute(input).await + } +} diff --git a/desktop/src-tauri/src/kernel_commands/lifecycle.rs b/desktop/src-tauri/src/kernel_commands/lifecycle.rs index 45f331e..3155602 100644 --- a/desktop/src-tauri/src/kernel_commands/lifecycle.rs +++ b/desktop/src-tauri/src/kernel_commands/lifecycle.rs @@ -63,6 +63,7 @@ pub async fn kernel_init( state: State<'_, KernelState>, scheduler_state: State<'_, SchedulerState>, heartbeat_state: State<'_, HeartbeatEngineState>, + mcp_state: State<'_, crate::kernel_commands::mcp::McpManagerState>, config_request: Option, ) -> Result { 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)); } + // 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 if let Some(api_key) = config_request.as_ref().and_then(|r| r.api_key.clone()) { crate::summarizer_adapter::configure_summary_driver( diff --git a/desktop/src-tauri/src/kernel_commands/mcp.rs b/desktop/src-tauri/src/kernel_commands/mcp.rs index 8a6e6dc..3188c68 100644 --- a/desktop/src-tauri/src/kernel_commands/mcp.rs +++ b/desktop/src-tauri/src/kernel_commands/mcp.rs @@ -1,6 +1,8 @@ //! MCP (Model Context Protocol) Tauri commands //! //! 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::sync::Arc; @@ -9,11 +11,43 @@ use tauri::State; use tokio::sync::Mutex; 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) -#[derive(Clone, Default)] -pub struct McpManagerState(pub Arc>); +#[derive(Clone)] +pub struct McpManagerState { + pub manager: Arc>, + /// Shared with Kernel — updated on every start/stop so + /// `create_tool_registry()` picks up the latest MCP tools. + pub kernel_adapters: Arc>>, +} + +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>>) -> 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 = mgr.all_adapters().into_iter().cloned().collect(); + if let Ok(mut guard) = self.kernel_adapters.write() { + *guard = all; + } + } +} /// MCP service configuration (from frontend) #[derive(Debug, Deserialize)] @@ -53,7 +87,7 @@ pub async fn mcp_start_service( manager: State<'_, McpManagerState>, config: McpServiceConfig, ) -> Result, String> { - let mut guard = manager.0.lock().await; + let mut guard = manager.manager.lock().await; let server_config = McpServerConfig { command: config.command, @@ -78,13 +112,16 @@ pub async fn mcp_start_service( let tools: Vec = adapters .into_iter() .map(|a| McpToolInfo { - service_name: config.name.clone(), - tool_name: a.name().to_string(), + service_name: a.service_name().to_string(), + tool_name: a.tool_name().to_string(), description: a.description().to_string(), input_schema: a.input_schema().clone(), }) .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"); Ok(tools) } @@ -96,8 +133,12 @@ pub async fn mcp_stop_service( manager: State<'_, McpManagerState>, name: String, ) -> Result<(), String> { - let mut guard = manager.0.lock().await; + let mut guard = manager.manager.lock().await; 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"); Ok(()) } @@ -108,7 +149,7 @@ pub async fn mcp_stop_service( pub async fn mcp_list_services( manager: State<'_, McpManagerState>, ) -> Result, String> { - let guard = manager.0.lock().await; + let guard = manager.manager.lock().await; let names = guard.service_names(); let all = guard.all_adapters(); let statuses: Vec = names @@ -116,9 +157,10 @@ pub async fn mcp_list_services( .map(|name| { let tools: Vec = all .iter() + .filter(|a| a.service_name() == name) .map(|a| McpToolInfo { - service_name: name.to_string(), - tool_name: a.name().to_string(), + service_name: a.service_name().to_string(), + tool_name: a.tool_name().to_string(), description: a.description().to_string(), input_schema: a.input_schema().clone(), }) @@ -138,15 +180,24 @@ pub async fn mcp_list_services( #[tauri::command] pub async fn mcp_call_tool( manager: State<'_, McpManagerState>, + service_name: Option, tool_name: String, arguments: serde_json::Value, ) -> Result { - 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))?; + let guard = manager.manager.lock().await; + let adapter = if let Some(ref svc) = service_name { + // Route by service name + tool name for precision + guard.all_adapters() + .into_iter() + .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 .execute(arguments) diff --git a/desktop/src/lib/mcp-client.ts b/desktop/src/lib/mcp-client.ts index 5dac081..d610077 100644 --- a/desktop/src/lib/mcp-client.ts +++ b/desktop/src/lib/mcp-client.ts @@ -73,5 +73,9 @@ export async function callMcpTool( args: Record ): Promise { log.info('callMcpTool', { serviceName, toolName }); - return invoke('mcp_call_tool', { serviceName, toolName, args }); + return invoke('mcp_call_tool', { + service_name: serviceName, + tool_name: toolName, + arguments: args, + }); }