diff --git a/doc/custom-mcp-tools.md b/doc/custom-mcp-tools.md index 7b50d24..0b0abe5 100644 --- a/doc/custom-mcp-tools.md +++ b/doc/custom-mcp-tools.md @@ -14,7 +14,7 @@ The dashboard **Tools** column lists whatever is in `mcp.json`. Use **+ Add cust | Situation | Path | |-----------|------| | Packaged app (recommended mental model) | Next to `connection.json` under the app data directory (same folder as Telegram token storage). | -| Local dev | Often `src-tauri/mcp.json` when you run from a source tree (see resolver in `mcp_service::resolve_mcp_config_path`). | +| Local dev | Same as packaged: next to `connection.json` (see `mcp_service::resolve_mcp_config_path`). Use `PENGINE_MCP_CONFIG` if you want a repo-local file. | | Override | Set **`PENGINE_MCP_CONFIG`** to an absolute or relative path. | The active path is returned by **`GET http://127.0.0.1:21516/v1/mcp/config`** (`config_path` field). diff --git a/src-tauri/src/infrastructure/http_server.rs b/src-tauri/src/infrastructure/http_server.rs index e0d9a24..590a584 100644 --- a/src-tauri/src/infrastructure/http_server.rs +++ b/src-tauri/src/infrastructure/http_server.rs @@ -359,6 +359,8 @@ async fn handle_mcp_filesystem_put( .filter(|p| !p.is_empty()) .collect(); + let catalog_result = te_service::load_catalog().await; + let sync_note = { let _guard = state.mcp_config_mutex.lock().await; @@ -377,8 +379,15 @@ async fn handle_mcp_filesystem_put( mcp_service::set_filesystem_allowed_paths(&mut cfg, &paths); let mut note = None::; - if let Err(e) = te_service::sync_workspace_mounted_tools_if_installed(&mut cfg, &paths) { - note = Some(e); + match &catalog_result { + Ok(cat) => { + if let Err(e) = + te_service::sync_workspace_mounted_tools_for_catalog(&mut cfg, &paths, cat) + { + note = Some(e); + } + } + Err(e) => note = Some(e.clone()), } mcp_service::save_config(&state.mcp_config_path, &cfg).map_err(|e| { diff --git a/src-tauri/src/modules/mcp/service.rs b/src-tauri/src/modules/mcp/service.rs index 96e559d..ee780be 100644 --- a/src-tauri/src/modules/mcp/service.rs +++ b/src-tauri/src/modules/mcp/service.rs @@ -31,11 +31,8 @@ const MCP_CONFIG_ENV: &str = "PENGINE_MCP_CONFIG"; /// Resolve the active `mcp.json` path. /// /// - Optional override: [`MCP_CONFIG_ENV`] → use that file. -/// - **Release builds** (packaged native app): always `$APP_DATA/mcp.json` next to -/// `connection.json`, so Tool Engine installs and workspace folders persist regardless of where -/// the `.app` bundle lives (e.g. `/Applications` vs still under a source tree). -/// - **Debug builds**: walk up from [`std::env::current_exe`] to find crate-root or -/// `…/src-tauri/mcp.json` for local development, then fall back to app data. +/// - Otherwise: always `$APP_DATA/mcp.json` next to `connection.json`, so Tool Engine installs +/// and workspace folders persist regardless of cwd or where the binary lives (debug or release). pub fn resolve_mcp_config_path(store_path: &Path) -> (PathBuf, &'static str) { if let Ok(raw) = std::env::var(MCP_CONFIG_ENV) { let t = raw.trim(); @@ -44,27 +41,6 @@ pub fn resolve_mcp_config_path(store_path: &Path) -> (PathBuf, &'static str) { } } - #[cfg(debug_assertions)] - { - if let Ok(exe) = std::env::current_exe() { - let mut dir = exe.parent().map(Path::to_path_buf); - for _ in 0..16 { - let Some(ref d) = dir else { - break; - }; - let from_repo_root = d.join("src-tauri").join("mcp.json"); - if from_repo_root.exists() { - return (from_repo_root, "project"); - } - let in_crate_root = d.join("mcp.json"); - if d.join("Cargo.toml").exists() && in_crate_root.exists() { - return (in_crate_root, "project"); - } - dir = d.parent().map(Path::to_path_buf); - } - } - } - let app_path = app_data_mcp_path(store_path); (app_path, "app_data") } @@ -234,6 +210,7 @@ pub async fn rebuild_registry_into_state( state: &crate::shared::state::AppState, ) -> Result<(), String> { let _rebuild = state.mcp_rebuild_mutex.lock().await; + let catalog_result = crate::modules::tool_engine::service::load_catalog().await; let cfg = { let _cfg_guard = state.mcp_config_mutex.lock().await; let mut cfg = match load_or_init_config(&state.mcp_config_path) { @@ -248,10 +225,19 @@ pub async fn rebuild_registry_into_state( let paths = filesystem_allowed_paths(&cfg); let mut ws_changed = false; - match crate::modules::tool_engine::service::sync_workspace_mounted_tools_if_installed( - &mut cfg, &paths, - ) { - Ok(changed) => ws_changed |= changed, + match &catalog_result { + Ok(cat) => { + match crate::modules::tool_engine::service::sync_workspace_mounted_tools_for_catalog( + &mut cfg, &paths, cat, + ) { + Ok(changed) => ws_changed |= changed, + Err(e) => { + state + .emit_log("toolengine", &format!("workspace mount sync skipped: {e}")) + .await; + } + } + } Err(e) => { state .emit_log("toolengine", &format!("workspace mount sync skipped: {e}")) @@ -371,10 +357,9 @@ mod tests { assert!(!cfg.servers.contains_key("filesystem")); } - /// Release binaries always use `mcp.json` next to `connection.json` (no exe walk). - #[cfg(not(debug_assertions))] + /// Default resolution: `mcp.json` next to `connection.json` (no project-tree walk). #[test] - fn resolve_mcp_config_release_uses_app_data_adjacent_to_store() { + fn resolve_mcp_config_uses_app_data_adjacent_to_store() { let store = PathBuf::from("/tmp/pengine-fake-app/connection.json"); let (path, src) = resolve_mcp_config_path(&store); assert_eq!(src, "app_data"); diff --git a/src-tauri/src/modules/mcp/types.rs b/src-tauri/src/modules/mcp/types.rs index 2e9759d..9b62f4a 100644 --- a/src-tauri/src/modules/mcp/types.rs +++ b/src-tauri/src/modules/mcp/types.rs @@ -1,9 +1,8 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; -/// Root config: `$APP_DATA/mcp.json` next to `connection.json` for release/native builds; debug -/// builds may use crate `mcp.json` (see `service::resolve_mcp_config_path`). Override with -/// `PENGINE_MCP_CONFIG`. +/// Root config: `$APP_DATA/mcp.json` next to `connection.json` (see `service::resolve_mcp_config_path`). +/// Override with `PENGINE_MCP_CONFIG`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpConfig { /// Host folders shared with the File Manager container (`/app/`). Replaces legacy diff --git a/src-tauri/src/modules/tool_engine/service.rs b/src-tauri/src/modules/tool_engine/service.rs index 523ea95..009f8e8 100644 --- a/src-tauri/src/modules/tool_engine/service.rs +++ b/src-tauri/src/modules/tool_engine/service.rs @@ -345,11 +345,14 @@ pub fn installed_tool_ids(mcp_config_path: &Path) -> Vec { /// Rewrite every **installed** catalog tool that uses `mount_workspace` so argv matches `host_paths` /// (empty list → in-image stub root only). Returns whether `mcp.json` should be saved. -pub fn sync_workspace_mounted_tools_if_installed( +/// +/// Pass the catalog from [`load_catalog`] (or tests) so callers can fetch **before** holding +/// `mcp_config_mutex`, avoiding network I/O under that lock. +pub fn sync_workspace_mounted_tools_for_catalog( cfg: &mut McpConfig, host_paths: &[String], + catalog: &ToolCatalog, ) -> Result { - let catalog = load_embedded_catalog()?; let mut changed = false; for entry in &catalog.tools { let key = server_key(&entry.id); @@ -444,12 +447,14 @@ pub async fn uninstall_tool( } // Remove the container image — prefer the ref from the installed entry. - let image_ref = installed_image_ref.or_else(|| { - load_embedded_catalog() + let image_ref = match installed_image_ref { + Some(r) => Some(r), + None => load_catalog() + .await .ok() .and_then(|cat| cat.tools.iter().find(|t| t.id == tool_id).cloned()) - .and_then(|entry| image_reference(&entry).ok()) - }); + .and_then(|entry| image_reference(&entry).ok()), + }; if let Some(ref img) = image_ref { let _ = tokio::process::Command::new(&runtime.binary) .args(["rmi", img]) diff --git a/src-tauri/src/modules/tool_engine/tools.json b/src-tauri/src/modules/tool_engine/tools.json index 5153a48..d6ee129 100644 --- a/src-tauri/src/modules/tool_engine/tools.json +++ b/src-tauri/src/modules/tool_engine/tools.json @@ -1,8 +1,8 @@ { "schema_version": 1, - "generated_at": "2026-04-12T00:00:00Z", - "catalog_revision": 1, - "valid_until": "2026-05-12T00:00:00Z", + "generated_at": "2026-04-13T00:00:00Z", + "catalog_revision": 2, + "valid_until": "2026-05-13T00:00:00Z", "minimum_pengine_version": "0.5.0", "tools": [ {