From c0a81aa6ed915804a13cc2469d2d89fe1d09b820 Mon Sep 17 00:00:00 2001 From: Domye Date: Sun, 16 Aug 2026 14:44:51 +0800 Subject: [PATCH 01/10] =?UTF-8?q?fix(mcp):=20=E6=94=AF=E6=8C=81=20pnpm=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9A=84=E5=85=A8=E5=B1=80=E5=8C=85=E5=92=8C?= =?UTF-8?q?=E8=A3=B8=20Node=20=E5=AE=89=E8=A3=85=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `pnpm_shim_target` 函数解析 pnpm 启动器内联脚本路径 - 在定位 MCP 包时增加 pnpm bin 目录作为备选路径 - 在查找 npm cli 时回退至 PATH 环境变量以支持无 npm 伴生的 Node 安装 - 添加针对 pnpm shim 解析的单元测试 --- src-tauri/src/commands/mcp.rs | 71 +++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 24a7ff2e88..4fa4dc5e20 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -107,7 +107,12 @@ impl NodeRuntime { }) }); let shim_package = launcher_dir.as_deref().and_then(mcp_package_from_command_dir); - let package = preferred_mcp_package(npm_package, shim_package, &node_version); + // pnpm installs global packages in pnpm's own bin dir instead of npm's global + // root; look there when the node-adjacent shim is absent. + let pnpm_bin_package = locate_command("pnpm") + .and_then(|command| Path::new(&command).parent().map(Path::to_path_buf)) + .and_then(|dir| mcp_package_from_command_dir(&dir)); + let package = preferred_mcp_package(npm_package, shim_package.or(pnpm_bin_package), &node_version); let package_is_compatible = package .as_ref() .and_then(|located| located.package.minimum_node_version) @@ -579,6 +584,13 @@ fn normalize_canonical_path(path: PathBuf) -> PathBuf { fn find_npm_cli(node_path: &Path, launcher_dir: Option<&Path>) -> Option { let mut candidates = launcher_dir.map(npm_cli_candidates_in_dir).unwrap_or_default(); candidates.extend(npm_cli_candidates(node_path)); + // Bare Node installs (pnpm-managed node, version managers) ship no npm next to + // the node binary; fall back to whatever npm is on PATH and resolve its script. + if let Some(npm_command) = locate_command("npm") { + if let Some(dir) = Path::new(&npm_command).parent() { + candidates.extend(npm_cli_candidates_in_dir(dir)); + } + } let mut seen = HashSet::new(); candidates.into_iter().find_map(|candidate| { @@ -616,12 +628,35 @@ fn node_script_from_launcher(path: &Path) -> Option { if let Some(target) = command_shim_target(&canonical) { return Some(target); } + if let Some(target) = pnpm_shim_target(&canonical) { + return Some(target); + } if is_native_npm_launcher(&canonical) || is_shell_script(&canonical) { return None; } Some(canonical) } +/// Extracts the real script path embedded in a pnpm launcher shim. pnpm shims +/// reference the package script inline (for example +/// `node "%~dp0\..\pnpm-global\v11\\node_modules\@dbx-app\mcp-server\bin\dbx-mcp-server.js"` +/// or `$basedir/...`), which the cmd-shim marker parse above does not cover. +fn pnpm_shim_target(path: &Path) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let script = content.split('"').find_map(|token| { + let token = token.trim(); + if !token.ends_with(".js") || !token.contains("node_modules") { + return None; + } + Some(token) + })?; + let shim_dir = path.parent()?.to_string_lossy().into_owned(); + let script = script.replace("%~dp0", &shim_dir).replace("$basedir_win", &shim_dir).replace("$basedir", &shim_dir); + let target = PathBuf::from(&script); + let target = if target.is_absolute() { target } else { path.parent()?.join(target) }; + canonical_runtime_path(&target) +} + fn command_shim_target(path: &Path) -> Option { if std::fs::metadata(path).ok()?.len() > 128 * 1024 { return None; @@ -1133,9 +1168,10 @@ mod tests { use super::{bash_login_script, prefixed_output_path, NodeRuntimeCandidate}; use super::{ canonical_runtime_path, is_mcp_compatible_node_version, mcp_command_for_runtime, mcp_native_binary_path_for, - mcp_package, normalized_reported_path, npm_cli_candidates, parse_minimum_node_version, parse_node_version, - prefer_runtime, require_managed_mcp_command, resolve_managed_mcp_command, stdout_after_shell_marker, - NodeRuntime, NodeVersion, MCP_MIN_NODE_VERSION_REQUIREMENT, MCP_PACKAGE_NAME, SHELL_COMMAND_MARKER, + mcp_package, node_script_from_launcher, normalized_reported_path, npm_cli_candidates, + parse_minimum_node_version, parse_node_version, pnpm_shim_target, prefer_runtime, require_managed_mcp_command, + resolve_managed_mcp_command, stdout_after_shell_marker, NodeRuntime, NodeVersion, + MCP_MIN_NODE_VERSION_REQUIREMENT, MCP_PACKAGE_NAME, SHELL_COMMAND_MARKER, }; #[cfg(not(windows))] use super::{shell_command_script, shell_quote}; @@ -1216,6 +1252,33 @@ mod tests { assert_eq!(normalized_reported_path(&path), Some(path)); } + #[test] + fn pnpm_shim_resolves_inline_script_path() { + use std::time::{SystemTime, UNIX_EPOCH}; + + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = std::env::temp_dir().join(format!("dbx-pnpm-shim-test-{}-{nonce}", std::process::id())); + let bin_dir = dir.join("bin"); + let script = dir.join("node_modules").join("@dbx-app").join("mcp-server").join("bin").join("dbx-mcp-server.js"); + std::fs::create_dir_all(script.parent().unwrap()).unwrap(); + std::fs::write(&script, "// launcher\n").unwrap(); + + let sep = std::path::MAIN_SEPARATOR; + let shim = bin_dir.join("dbx-mcp-server.CMD"); + std::fs::create_dir_all(&bin_dir).unwrap(); + std::fs::write( + &shim, + format!( + "@SETLOCAL\r\nnode \"%~dp0{sep}..{sep}node_modules{sep}@dbx-app{sep}mcp-server{sep}bin{sep}dbx-mcp-server.js\" %*\r\n" + ), + ) + .unwrap(); + + assert_eq!(pnpm_shim_target(&shim), canonical_runtime_path(&script)); + assert_eq!(node_script_from_launcher(&shim), canonical_runtime_path(&script)); + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn installed_runtime_outranks_an_earlier_runtime_without_mcp() { let first = runtime("/runtime/node-26", None); From 41b23eed0c91cd212d47aad40c6573d484366fff Mon Sep 17 00:00:00 2001 From: Domye Date: Sun, 16 Aug 2026 14:49:08 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(mcp):=20=E6=94=AF=E6=8C=81=20pnpm=20?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=8C=85=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pnpm 安装命令常量 - 在未检测到 npm 包时优先使用 pnpm - 实现 install_command 方法以支持 pnpm - 更新 update_command 逻辑以处理 pnpm 安装场景 - 修正状态检查中的 install_command 引用 --- src-tauri/src/commands/mcp.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 4fa4dc5e20..c5312a8968 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -10,6 +10,7 @@ use tauri::{AppHandle, Manager}; const MCP_PACKAGE_NAME: &str = "@dbx-app/mcp-server"; const MCP_LATEST_URL: &str = "https://registry.npmjs.org/@dbx-app%2fmcp-server/latest"; const MCP_INSTALL_COMMAND: &str = "npm install -g @dbx-app/mcp-server@latest"; +const MCP_PNPM_INSTALL_COMMAND: &str = "pnpm add -g @dbx-app/mcp-server"; const MCP_PNPM_UPDATE_COMMAND: &str = "pnpm update -g @dbx-app/mcp-server"; const MCP_UNINSTALL_COMMAND: &str = "npm uninstall -g @dbx-app/mcp-server"; const MCP_PNPM_UNINSTALL_COMMAND: &str = "pnpm remove -g @dbx-app/mcp-server"; @@ -123,8 +124,13 @@ impl NodeRuntime { package.as_ref().filter(|_| package_is_compatible).map(|located| located.package.script_path.clone()); let mcp_bin_path = package.as_ref().and_then(|located| located.bin_path.clone()).or_else(|| mcp_bin_path(&npm_prefix)); - let package_manager = - package.as_ref().map(|located| located.package_manager.clone()).unwrap_or(McpPackageManager::Npm); + // Prefer pnpm for install/update/uninstall when pnpm is reachable and no + // npm-installed package was located, so pnpm-based setups see pnpm commands. + let package_manager = package.as_ref().map(|located| located.package_manager.clone()).unwrap_or_else(|| { + locate_command("pnpm") + .map(|command_path| McpPackageManager::Pnpm { command_path: PathBuf::from(command_path) }) + .unwrap_or(McpPackageManager::Npm) + }); // TRAE on Windows splits executable paths containing spaces, so expose the native package binary as a safe direct launch option. let mcp_native_bin_path = package_is_compatible .then(|| package.as_ref().and_then(|located| mcp_native_binary_path(&located.package_root, &npm_root))) @@ -163,6 +169,13 @@ impl NodeRuntime { } } + fn install_command(&self) -> &'static str { + match &self.package_manager { + McpPackageManager::Npm => MCP_INSTALL_COMMAND, + McpPackageManager::Pnpm { .. } => MCP_PNPM_INSTALL_COMMAND, + } + } + fn uninstall_command(&self) -> &'static str { match &self.package_manager { McpPackageManager::Npm => MCP_UNINSTALL_COMMAND, @@ -175,6 +188,9 @@ impl NodeRuntime { McpPackageManager::Pnpm { command_path } if self.has_mcp_package() => { run_package_manager_command(command_path, &["update", "-g", MCP_PACKAGE_NAME], &self.node_launcher_path) } + McpPackageManager::Pnpm { command_path } => { + run_package_manager_command(command_path, &["add", "-g", MCP_PACKAGE_NAME], &self.node_launcher_path) + } _ => self.npm_output(&["install", "-g", "@dbx-app/mcp-server@latest"]), } } @@ -243,7 +259,7 @@ pub async fn check_mcp_server_status(app: AppHandle) -> Result Date: Sun, 16 Aug 2026 15:18:36 +0800 Subject: [PATCH 03/10] feat(mcp): add detection for PATH shim package manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加 locate_command("dbx-mcp-server") 逻辑以检测 PATH 中的 shim - 将 path_shim_package 合并到 preferred_mcp_package 的参数中 - 支持 Yarn、Bun 及手动配置目录的包管理器检测 --- src-tauri/src/commands/mcp.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index c5312a8968..c3c156d38a 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -113,7 +113,13 @@ impl NodeRuntime { let pnpm_bin_package = locate_command("pnpm") .and_then(|command| Path::new(&command).parent().map(Path::to_path_buf)) .and_then(|dir| mcp_package_from_command_dir(&dir)); - let package = preferred_mcp_package(npm_package, shim_package.or(pnpm_bin_package), &node_version); + // Any package manager that puts a dbx-mcp-server shim on PATH (Yarn, Bun, + // manually curated dirs) is detected through the shim itself. + let path_shim_package = locate_command("dbx-mcp-server") + .and_then(|command| Path::new(&command).parent().map(Path::to_path_buf)) + .and_then(|dir| mcp_package_from_command_dir(&dir)); + let package = + preferred_mcp_package(npm_package, shim_package.or(pnpm_bin_package).or(path_shim_package), &node_version); let package_is_compatible = package .as_ref() .and_then(|located| located.package.minimum_node_version) From 1ff30eec3a75a28efc582242a466acc5236768a0 Mon Sep 17 00:00:00 2001 From: Domye Date: Sun, 16 Aug 2026 18:02:47 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat(mcp):=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=AE=A1=E7=90=86=E6=A8=A1=E5=BC=8F=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=B9=B6=E7=A6=81=E6=AD=A2=E5=AF=B9=E6=9C=AA=E7=9F=A5=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E6=89=8B=E5=8A=A8=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 managed_automatically 字段到 McpServerStatus 接口 - 新增 Unknown 包管理器类型,用于处理无法确认来源的安装(如 Yarn、Bun 或手动复制) - 将 npm_cli_path 和 npm_root 改为 Option 类型,支持 pnpm-only 或裸 Node 环境 - 安装/卸载按钮在检测到非自动管理模式时禁用,并显示手动管理提示 - 更新错误信息文案,区分 npm 不可用与未知管理器的情况 - 补充单元测试验证未知包管理器的只读行为及 pnpm 识别逻辑 --- .../editor/EditorSettingsDialog.vue | 14 +- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/lib/backend/tauri.ts | 1 + src-tauri/src/commands/mcp.rs | 197 ++++++++++++++---- 5 files changed, 165 insertions(+), 49 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index e4208f4241..b4562085da 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -6348,7 +6348,10 @@ onUnmounted(() => {
-
+
+ {{ t("settings.mcpManualManagementHint") }} +
+
{{ mcpCommand }}
@@ -6356,7 +6359,7 @@ onUnmounted(() => { - - - -