From 77621e5a6f3206dfd77fbd778495b27981f9f44d Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 12 Apr 2026 12:02:08 +0200 Subject: [PATCH 1/3] feat: integrate Tauri event bridge and refactor MCP tools management - Added `initTauriRegistryBridge` to connect Tauri events with browser events for MCP tools. - Introduced `useRegistryChanged` hook to simplify registry change handling in `McpToolsPanel` and `ToolEnginePanel`. - Updated `tool_manager` integration in the MCP service to ensure consistent tool management. - Enhanced error handling and async support in tool management functions. - Refactored tests to support async operations for improved reliability. --- src-tauri/mcp.example.json | 4 + src-tauri/src/modules/mcp/native.rs | 197 +++++++++++++++++- src-tauri/src/modules/mcp/registry.rs | 2 +- src-tauri/src/modules/mcp/service.rs | 30 ++- src-tauri/tests/mcp_tools.rs | 17 +- src/main.tsx | 3 + src/modules/mcp/components/McpToolsPanel.tsx | 10 +- .../toolengine/components/ToolEnginePanel.tsx | 3 + src/shared/mcpEvents.ts | 19 ++ src/shared/useRegistryChanged.ts | 10 + 10 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 src/shared/useRegistryChanged.ts diff --git a/src-tauri/mcp.example.json b/src-tauri/mcp.example.json index 0afb307..5d7bf25 100644 --- a/src-tauri/mcp.example.json +++ b/src-tauri/mcp.example.json @@ -4,6 +4,10 @@ "dice": { "type": "native", "id": "dice" + }, + "tool_manager": { + "type": "native", + "id": "tool_manager" } } } diff --git a/src-tauri/src/modules/mcp/native.rs b/src-tauri/src/modules/mcp/native.rs index 89221ad..7026d2c 100644 --- a/src-tauri/src/modules/mcp/native.rs +++ b/src-tauri/src/modules/mcp/native.rs @@ -1,24 +1,41 @@ use super::types::ToolDef; +use crate::modules::mcp::service as mcp_service; +use crate::modules::tool_engine::runtime as tool_engine_runtime; +use crate::modules::tool_engine::service as tool_engine_service; +use crate::shared::state::AppState; use serde_json::{json, Value}; +use std::collections::HashSet; const MAX_SIDES: u64 = 1_000_000; +/// Server key / native id used in `mcp.json` for the built-in tool manager. +pub const TOOL_MANAGER_ID: &str = "tool_manager"; + +enum NativeKind { + Dice, + ToolManager(AppState), +} + pub struct NativeProvider { pub server_name: String, pub tools: Vec, - handler: fn(&str, &Value) -> Result, + kind: NativeKind, } impl NativeProvider { - pub fn call(&self, tool_name: &str, args: &Value) -> Result { + pub async fn call(&self, tool_name: &str, args: &Value) -> Result { if !self.tools.iter().any(|t| t.name == tool_name) { return Err(format!("unknown native tool: {tool_name}")); } - (self.handler)(tool_name, args) + match &self.kind { + NativeKind::Dice => handle_dice(tool_name, args), + NativeKind::ToolManager(state) => handle_tool_manager(tool_name, args, state).await, + } } } -/// Built-in dice tools under the given server key (must match `mcp.json` server name). +// ── Dice ──────────────────────────────────────────────────────────── + pub fn dice_named(server_key: &str) -> NativeProvider { NativeProvider { server_name: server_key.to_string(), @@ -39,7 +56,7 @@ pub fn dice_named(server_key: &str) -> NativeProvider { }), direct_return: true, }], - handler: handle_dice, + kind: NativeKind::Dice, } } @@ -58,10 +75,178 @@ fn handle_dice(_tool_name: &str, args: &Value) -> Result { Ok(format!("Rolled a d{sides}: {result}")) } +// ── Tool Manager ──────────────────────────────────────────────────── + +pub fn tool_manager_named(server_key: &str, state: AppState) -> NativeProvider { + NativeProvider { + server_name: server_key.to_string(), + tools: vec![ToolDef { + server_name: server_key.to_string(), + name: "manage_tools".to_string(), + description: Some( + "Manage container-based tools: list available tools, install a tool, or uninstall a tool. \ + Use action 'list' to see all available tools and their install status. \ + Use action 'install' with a tool_id to install a new tool. \ + Use action 'uninstall' with a tool_id to remove an installed tool." + .to_string(), + ), + input_schema: json!({ + "type": "object", + "required": ["action"], + "properties": { + "action": { + "type": "string", + "enum": ["list", "install", "uninstall"], + "description": "The operation to perform" + }, + "tool_id": { + "type": "string", + "description": "Tool identifier (required for install/uninstall, e.g. 'pengine/file-manager')" + } + } + }), + direct_return: false, + }], + kind: NativeKind::ToolManager(state), + } +} + +async fn handle_tool_manager( + _tool_name: &str, + args: &Value, + state: &AppState, +) -> Result { + let action = args + .get("action") + .and_then(|v| v.as_str()) + .ok_or("missing 'action' parameter")?; + + match action { + "list" => handle_list_tools(state).await, + "install" => { + let tool_id = args + .get("tool_id") + .and_then(|v| v.as_str()) + .ok_or("missing 'tool_id' for install")?; + handle_install_tool(tool_id, state).await + } + "uninstall" => { + let tool_id = args + .get("tool_id") + .and_then(|v| v.as_str()) + .ok_or("missing 'tool_id' for uninstall")?; + handle_uninstall_tool(tool_id, state).await + } + _ => Err(format!("unknown action: {action}")), + } +} + +async fn handle_list_tools(state: &AppState) -> Result { + let catalog = tool_engine_service::load_catalog()?; + let installed = tool_engine_service::installed_tool_ids(&state.mcp_config_path); + let installed_set: HashSet<&str> = installed.iter().map(|s| s.as_str()).collect(); + + let mut lines = Vec::new(); + for tool in &catalog.tools { + let status = if installed_set.contains(tool.id.as_str()) { + "installed" + } else { + "not installed" + }; + lines.push(format!( + "- {} (id: {}, v{}): {} [{}]", + tool.name, tool.id, tool.version, tool.description, status + )); + } + + if lines.is_empty() { + Ok("No tools available in the catalog.".to_string()) + } else { + Ok(format!("Available tools:\n{}", lines.join("\n"))) + } +} + +async fn handle_install_tool(tool_id: &str, state: &AppState) -> Result { + run_tool_mutation(tool_id, state, "install", ToolAction::Install).await?; + Ok(format!( + "Tool '{tool_id}' installed successfully and is now available." + )) +} + +async fn handle_uninstall_tool(tool_id: &str, state: &AppState) -> Result { + run_tool_mutation(tool_id, state, "uninstall", ToolAction::Uninstall).await?; + Ok(format!("Tool '{tool_id}' uninstalled successfully.")) +} + +enum ToolAction { + Install, + Uninstall, +} + +/// Shared sequence for install / uninstall: detect runtime, lock, log, act, log, rebuild. +async fn run_tool_mutation( + tool_id: &str, + state: &AppState, + verb: &str, + action: ToolAction, +) -> Result<(), String> { + let runtime = tool_engine_runtime::detect_runtime().await.ok_or( + "No container runtime (Docker/Podman) found. Please install Docker or Podman first.", + )?; + + { + let _te_guard = state.tool_engine_mutex.lock().await; + state + .emit_log("toolengine", &format!("{verb}ing {tool_id} via chat…")) + .await; + match action { + ToolAction::Install => { + tool_engine_service::install_tool( + tool_id, + &runtime, + &state.mcp_config_path, + &state.mcp_config_mutex, + ) + .await?; + } + ToolAction::Uninstall => { + tool_engine_service::uninstall_tool( + tool_id, + &runtime, + &state.mcp_config_path, + &state.mcp_config_mutex, + ) + .await?; + } + } + state + .emit_log("toolengine", &format!("{tool_id} {verb}ed via chat")) + .await; + } + + if let Err(e) = mcp_service::rebuild_registry_into_state(state).await { + state + .emit_log("mcp", &format!("registry rebuild after {verb} failed: {e}")) + .await; + } + Ok(()) +} + +// ── Registry ──────────────────────────────────────────────────────── + /// Resolve `id` from `mcp.json` (`type: native`) into a provider under `server_key`. -pub fn native_for(server_key: &str, id: &str) -> Result { +/// `app_state` is required for stateful natives like `tool_manager`. +pub fn native_for( + server_key: &str, + id: &str, + app_state: Option<&AppState>, +) -> Result { match id { "dice" => Ok(dice_named(server_key)), + TOOL_MANAGER_ID => { + let state = app_state.ok_or_else(|| format!("{TOOL_MANAGER_ID} requires AppState"))?; + Ok(tool_manager_named(server_key, state.clone())) + } _ => Err(format!("unknown native id: {id}")), } } diff --git a/src-tauri/src/modules/mcp/registry.rs b/src-tauri/src/modules/mcp/registry.rs index d2febea..320eca1 100644 --- a/src-tauri/src/modules/mcp/registry.rs +++ b/src-tauri/src/modules/mcp/registry.rs @@ -120,7 +120,7 @@ impl Provider { pub async fn call_tool(&self, name: &str, args: Value) -> Result { match self { - Provider::Native(n) => n.call(name, &args), + Provider::Native(n) => n.call(name, &args).await, Provider::Mcp(c) => c.call_tool(name, args).await, } } diff --git a/src-tauri/src/modules/mcp/service.rs b/src-tauri/src/modules/mcp/service.rs index 97afd8e..533d3b7 100644 --- a/src-tauri/src/modules/mcp/service.rs +++ b/src-tauri/src/modules/mcp/service.rs @@ -7,8 +7,10 @@ use super::registry::{Provider, ToolRegistry}; use super::types::{McpConfig, ServerEntry}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use tauri::Emitter; const FILESYSTEM_SERVER_KEY: &str = "filesystem"; +const REGISTRY_CHANGED_EVENT: &str = "pengine-registry-changed"; fn app_data_mcp_path(store_path: &Path) -> PathBuf { store_path @@ -127,6 +129,10 @@ fn default_config_value() -> serde_json::Value { "dice": { "type": "native", "id": "dice" + }, + "tool_manager": { + "type": "native", + "id": "tool_manager" } } }) @@ -149,12 +155,14 @@ pub fn load_or_init_config(path: &Path) -> Result { } /// Connect one server from config (native or stdio). Shared by tests and incremental rebuilds. +/// `app_state` is needed for stateful native tools (e.g. `tool_manager`); pass `None` in tests. pub async fn connect_one_server( server_key: &str, entry: &ServerEntry, + app_state: Option<&crate::shared::state::AppState>, ) -> (Option, String) { match entry { - ServerEntry::Native { id } => match native::native_for(server_key, id) { + ServerEntry::Native { id } => match native::native_for(server_key, id, app_state) { Ok(p) => { let n = p.tools.len(); let cmd_word = if n == 1 { "command" } else { "commands" }; @@ -197,7 +205,7 @@ pub async fn build_mcp_providers(cfg: &McpConfig) -> (Vec, Vec let mut status = Vec::new(); for (server_key, entry) in &cfg.servers { - let (prov, line) = connect_one_server(server_key, entry).await; + let (prov, line) = connect_one_server(server_key, entry, None).await; status.push(line); if let Some(p) = prov { providers.push(p); @@ -255,6 +263,17 @@ pub async fn rebuild_registry_into_state( } } + // Ensure tool_manager is always present (auto-add for existing configs). + if !cfg.servers.contains_key(native::TOOL_MANAGER_ID) { + cfg.servers.insert( + native::TOOL_MANAGER_ID.to_string(), + ServerEntry::Native { + id: native::TOOL_MANAGER_ID.to_string(), + }, + ); + let _ = save_config(&state.mcp_config_path, &cfg); + } + cfg }; @@ -265,7 +284,7 @@ pub async fn rebuild_registry_into_state( // connects only emit a log line — no need to rebuild the registry. let mut providers = Vec::new(); for (server_key, entry) in &cfg.servers { - let (prov, line) = connect_one_server(server_key, entry).await; + let (prov, line) = connect_one_server(server_key, entry, Some(state)).await; state.emit_log("mcp", &line).await; let Some(p) = prov else { continue }; providers.push(p); @@ -279,6 +298,11 @@ pub async fn rebuild_registry_into_state( &format!("ready ({n} tool{})", if n == 1 { "" } else { "s" }), ) .await; + + if let Some(handle) = state.app_handle.lock().await.as_ref() { + let _ = handle.emit(REGISTRY_CHANGED_EVENT, ()); + } + Ok(()) } diff --git a/src-tauri/tests/mcp_tools.rs b/src-tauri/tests/mcp_tools.rs index 7838ad0..cdfca3d 100644 --- a/src-tauri/tests/mcp_tools.rs +++ b/src-tauri/tests/mcp_tools.rs @@ -15,8 +15,8 @@ fn temp_mcp_path(name: &str) -> PathBuf { p } -#[test] -fn dice_returns_valid_result() { +#[tokio::test] +async fn dice_returns_valid_result() { let provider = native::dice(); assert_eq!(provider.tools.len(), 1); assert_eq!(provider.tools[0].name, "roll_dice"); @@ -24,6 +24,7 @@ fn dice_returns_valid_result() { let out = provider .call("roll_dice", &json!({"sides": 20})) + .await .expect("dice call"); assert!(out.starts_with("Rolled a d20: "), "got: {out}"); @@ -31,17 +32,19 @@ fn dice_returns_valid_result() { assert!((1..=20).contains(&num)); } -#[test] -fn dice_clamps_invalid_sides() { +#[tokio::test] +async fn dice_clamps_invalid_sides() { let provider = native::dice(); let out = provider .call("roll_dice", &json!({"sides": 0})) + .await .expect("sides=0"); assert!(out.starts_with("Rolled a d2: "), "clamped to 2, got: {out}"); let out = provider .call("roll_dice", &json!({"sides": 9999999})) + .await .expect("sides=9999999"); assert!( out.starts_with("Rolled a d1000000: "), @@ -49,10 +52,10 @@ fn dice_clamps_invalid_sides() { ); } -#[test] -fn dice_rejects_unknown_tool() { +#[tokio::test] +async fn dice_rejects_unknown_tool() { let provider = native::dice(); - let err = provider.call("unknown", &json!({})).unwrap_err(); + let err = provider.call("unknown", &json!({})).await.unwrap_err(); assert!(err.contains("unknown native tool")); } diff --git a/src/main.tsx b/src/main.tsx index eddb197..cc62ca2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,11 @@ import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; +import { initTauriRegistryBridge } from "./shared/mcpEvents"; import "./index.css"; +initTauriRegistryBridge(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/modules/mcp/components/McpToolsPanel.tsx b/src/modules/mcp/components/McpToolsPanel.tsx index 2cf4519..26135c8 100644 --- a/src/modules/mcp/components/McpToolsPanel.tsx +++ b/src/modules/mcp/components/McpToolsPanel.tsx @@ -8,7 +8,7 @@ import { type McpTool, type ServerEntry, } from ".."; -import { PENGINE_MCP_REGISTRY_CHANGED } from "../../../shared/mcpEvents"; +import { useRegistryChanged } from "../../../shared/useRegistryChanged"; import { AddServerForm } from "./AddServerForm"; import { McpServerCard } from "./McpServerCard"; @@ -119,13 +119,7 @@ export function McpToolsPanel() { }; }, []); - useEffect(() => { - const onRegistryChanged = () => { - void reload(); - }; - window.addEventListener(PENGINE_MCP_REGISTRY_CHANGED, onRegistryChanged); - return () => window.removeEventListener(PENGINE_MCP_REGISTRY_CHANGED, onRegistryChanged); - }, [reload]); + useRegistryChanged(reload); // ── Server CRUD handlers ─────────────────────────────────────────── diff --git a/src/modules/toolengine/components/ToolEnginePanel.tsx b/src/modules/toolengine/components/ToolEnginePanel.tsx index 06afc83..f457c4f 100644 --- a/src/modules/toolengine/components/ToolEnginePanel.tsx +++ b/src/modules/toolengine/components/ToolEnginePanel.tsx @@ -1,6 +1,7 @@ import * as Accordion from "@radix-ui/react-accordion"; import { useCallback, useEffect, useRef, useState } from "react"; import { notifyMcpRegistryChanged } from "../../../shared/mcpEvents"; +import { useRegistryChanged } from "../../../shared/useRegistryChanged"; import { fetchRuntimeStatus, fetchToolCatalog, @@ -52,6 +53,8 @@ export function ToolEnginePanel() { }; }, [loadData]); + useRegistryChanged(loadData); + const handleInstall = async (toolId: string) => { setBusyTool(toolId); setBusyKind("install"); diff --git a/src/shared/mcpEvents.ts b/src/shared/mcpEvents.ts index a41e45a..e05f60b 100644 --- a/src/shared/mcpEvents.ts +++ b/src/shared/mcpEvents.ts @@ -5,3 +5,22 @@ export function notifyMcpRegistryChanged(): void { if (typeof window === "undefined") return; window.dispatchEvent(new Event(PENGINE_MCP_REGISTRY_CHANGED)); } + +/** Tauri event name — must match `REGISTRY_CHANGED_EVENT` in `mcp/service.rs`. */ +const TAURI_REGISTRY_CHANGED = "pengine-registry-changed"; + +/** + * Bridge backend Tauri event into the browser window event that panels already listen for. + * Call once at app startup. + */ +export function initTauriRegistryBridge(): void { + import("@tauri-apps/api/event") + .then(({ listen }) => + listen(TAURI_REGISTRY_CHANGED, () => { + notifyMcpRegistryChanged(); + }), + ) + .catch(() => { + // Not running inside Tauri shell — no bridge needed. + }); +} diff --git a/src/shared/useRegistryChanged.ts b/src/shared/useRegistryChanged.ts new file mode 100644 index 0000000..9060eb7 --- /dev/null +++ b/src/shared/useRegistryChanged.ts @@ -0,0 +1,10 @@ +import { useEffect } from "react"; +import { PENGINE_MCP_REGISTRY_CHANGED } from "./mcpEvents"; + +/** Re-run `callback` whenever the MCP registry changes (install, uninstall, config edit). */ +export function useRegistryChanged(callback: () => void): void { + useEffect(() => { + window.addEventListener(PENGINE_MCP_REGISTRY_CHANGED, callback); + return () => window.removeEventListener(PENGINE_MCP_REGISTRY_CHANGED, callback); + }, [callback]); +} From e28d957864f6c8296117b5715acd0d08dd22f5b8 Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 12 Apr 2026 12:34:51 +0200 Subject: [PATCH 2/3] fix: improve error handling and data loading in Tool Engine panel - Updated Tool Engine panel to ensure data is reloaded after installation and uninstallation failures. - Enhanced error handling to provide clearer feedback on action failures. - Refactored MCP service to include proper locking mechanisms when accessing installed tool IDs, improving thread safety. - Added logging for failed configuration saves to aid in debugging. --- src-tauri/src/modules/mcp/native.rs | 5 ++++- src-tauri/src/modules/mcp/service.rs | 9 ++++++++- .../toolengine/components/ToolEnginePanel.tsx | 4 ++-- src/shared/mcpEvents.ts | 15 +++++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/modules/mcp/native.rs b/src-tauri/src/modules/mcp/native.rs index 7026d2c..55c3a4a 100644 --- a/src-tauri/src/modules/mcp/native.rs +++ b/src-tauri/src/modules/mcp/native.rs @@ -143,7 +143,10 @@ async fn handle_tool_manager( async fn handle_list_tools(state: &AppState) -> Result { let catalog = tool_engine_service::load_catalog()?; - let installed = tool_engine_service::installed_tool_ids(&state.mcp_config_path); + let installed = { + let _cfg_guard = state.mcp_config_mutex.lock().await; + tool_engine_service::installed_tool_ids(&state.mcp_config_path) + }; let installed_set: HashSet<&str> = installed.iter().map(|s| s.as_str()).collect(); let mut lines = Vec::new(); diff --git a/src-tauri/src/modules/mcp/service.rs b/src-tauri/src/modules/mcp/service.rs index 533d3b7..0f5b85d 100644 --- a/src-tauri/src/modules/mcp/service.rs +++ b/src-tauri/src/modules/mcp/service.rs @@ -271,7 +271,14 @@ pub async fn rebuild_registry_into_state( id: native::TOOL_MANAGER_ID.to_string(), }, ); - let _ = save_config(&state.mcp_config_path, &cfg); + if let Err(e) = save_config(&state.mcp_config_path, &cfg) { + log::warn!( + "failed to save mcp.json after auto-inserting native server {:?}: {} (path={})", + native::TOOL_MANAGER_ID, + e, + state.mcp_config_path.display() + ); + } } cfg diff --git a/src/modules/toolengine/components/ToolEnginePanel.tsx b/src/modules/toolengine/components/ToolEnginePanel.tsx index f457c4f..d8854a9 100644 --- a/src/modules/toolengine/components/ToolEnginePanel.tsx +++ b/src/modules/toolengine/components/ToolEnginePanel.tsx @@ -68,8 +68,8 @@ export function ToolEnginePanel() { notifyMcpRegistryChanged(); } else { setActionError(result.error ?? "Install failed"); + await loadData(); } - await loadData(); } finally { if (!cancelledRef.current) { setBusyTool(null); @@ -91,8 +91,8 @@ export function ToolEnginePanel() { notifyMcpRegistryChanged(); } else { setActionError(result.error ?? "Uninstall failed"); + await loadData(); } - await loadData(); } finally { if (!cancelledRef.current) { setBusyTool(null); diff --git a/src/shared/mcpEvents.ts b/src/shared/mcpEvents.ts index e05f60b..f8d744d 100644 --- a/src/shared/mcpEvents.ts +++ b/src/shared/mcpEvents.ts @@ -9,18 +9,29 @@ export function notifyMcpRegistryChanged(): void { /** Tauri event name — must match `REGISTRY_CHANGED_EVENT` in `mcp/service.rs`. */ const TAURI_REGISTRY_CHANGED = "pengine-registry-changed"; +let tauriBridgeInitialized = false; +let tauriBridgeListenPromise: Promise | null = null; + /** * Bridge backend Tauri event into the browser window event that panels already listen for. - * Call once at app startup. + * Call once at app startup. Safe to call multiple times — only one listener is registered. */ export function initTauriRegistryBridge(): void { - import("@tauri-apps/api/event") + if (typeof window === "undefined") return; + if (tauriBridgeInitialized) return; + if (tauriBridgeListenPromise !== null) return; + + tauriBridgeListenPromise = import("@tauri-apps/api/event") .then(({ listen }) => listen(TAURI_REGISTRY_CHANGED, () => { notifyMcpRegistryChanged(); }), ) + .then(() => { + tauriBridgeInitialized = true; + }) .catch(() => { + tauriBridgeListenPromise = null; // Not running inside Tauri shell — no bridge needed. }); } From 9c9244853663218dabd78ee090a2aedaaa889c67 Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 12 Apr 2026 13:03:18 +0200 Subject: [PATCH 3/3] refactor: update tool management descriptions and improve error handling - Revised descriptions in the tool manager to clarify user actions and tool management processes. - Enhanced error handling in the `run_tool_mutation` function to ensure proper error propagation. - Removed unnecessary comments in the Tauri registry bridge initialization for cleaner code. --- src-tauri/src/modules/mcp/native.rs | 15 +++++++++------ src/shared/mcpEvents.ts | 1 - 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/modules/mcp/native.rs b/src-tauri/src/modules/mcp/native.rs index 55c3a4a..953a65f 100644 --- a/src-tauri/src/modules/mcp/native.rs +++ b/src-tauri/src/modules/mcp/native.rs @@ -84,10 +84,12 @@ pub fn tool_manager_named(server_key: &str, state: AppState) -> NativeProvider { server_name: server_key.to_string(), name: "manage_tools".to_string(), description: Some( - "Manage container-based tools: list available tools, install a tool, or uninstall a tool. \ - Use action 'list' to see all available tools and their install status. \ - Use action 'install' with a tool_id to install a new tool. \ - Use action 'uninstall' with a tool_id to remove an installed tool." + "Manage container-based tools from the catalog. All catalog tools (e.g. File Manager) \ + are user-managed and can be freely installed or uninstalled on request. \ + Use action 'list' to see all available catalog tools and their install status. \ + Use action 'install' with a tool_id to install a tool. \ + Use action 'uninstall' with a tool_id to remove an installed tool. \ + Always call this tool when the user asks to install, uninstall, or list tools." .to_string(), ), input_schema: json!({ @@ -97,11 +99,11 @@ pub fn tool_manager_named(server_key: &str, state: AppState) -> NativeProvider { "action": { "type": "string", "enum": ["list", "install", "uninstall"], - "description": "The operation to perform" + "description": "The operation: 'list' to show available tools, 'install' or 'uninstall' to change a tool" }, "tool_id": { "type": "string", - "description": "Tool identifier (required for install/uninstall, e.g. 'pengine/file-manager')" + "description": "Required for install/uninstall. Use the exact id from the 'list' output (e.g. 'pengine/file-manager'). Call with action 'list' first if unsure." } } }), @@ -231,6 +233,7 @@ async fn run_tool_mutation( state .emit_log("mcp", &format!("registry rebuild after {verb} failed: {e}")) .await; + return Err(e); } Ok(()) } diff --git a/src/shared/mcpEvents.ts b/src/shared/mcpEvents.ts index f8d744d..f202c08 100644 --- a/src/shared/mcpEvents.ts +++ b/src/shared/mcpEvents.ts @@ -32,6 +32,5 @@ export function initTauriRegistryBridge(): void { }) .catch(() => { tauriBridgeListenPromise = null; - // Not running inside Tauri shell — no bridge needed. }); }