From 01fde8879ff86b82da552976556325b25e4e7d63 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 22 Jul 2026 13:31:00 +0800 Subject: [PATCH] fix(mcp): use native binary for TRAE on Windows --- .../editor/EditorSettingsDialog.vue | 14 ++- .../__tests__/mcp/mcpConfigTemplates.spec.ts | 17 +++- apps/desktop/src/lib/backend/http.ts | 1 + apps/desktop/src/lib/backend/tauri.ts | 1 + .../desktop/src/lib/mcp/mcpConfigTemplates.ts | 4 + src-tauri/src/commands/mcp.rs | 97 ++++++++++++++++++- 6 files changed, 124 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 4e3802dec..6897b1b4a 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -95,9 +95,9 @@ import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sq import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache"; import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates"; import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, normalizeSqlVariableSyntaxOverrides, SQL_VARIABLE_SYNTAX_DATABASE_TYPES, SQL_VARIABLE_SYNTAX_KEYS, SQL_VARIABLE_SYNTAX_TOKENS, type SqlVariableSyntaxOverrides, type SqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; -import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; +import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpTraeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection"; -import { isMacOS } from "@/lib/backend/platform"; +import { isMacOS, isWindows } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; import { useToast } from "@/composables/useToast"; import type { DatabaseType, SqlSnippet } from "@/types/database"; @@ -1509,6 +1509,12 @@ const mcpLaunchConfig = computed(() => { const mcpJsonRecommendedConfig = computed(() => buildMcpJsonConfig(mcpLaunchConfig.value)); +const mcpTraeRecommendedConfig = computed(() => { + // TRAE currently splits Windows executable paths containing spaces, so bypass Node and launch the native MCP binary directly. + const nativeBinPath = !isWeb && isWindows() ? mcpStatus.value?.native_bin_path : undefined; + return buildMcpTraeConfig(mcpLaunchConfig.value, nativeBinPath ?? undefined); +}); + const mcpVsCodeRecommendedConfig = computed(() => buildMcpVsCodeConfig(mcpLaunchConfig.value)); const mcpCherryStudioRecommendedConfig = computed(() => buildMcpCherryStudioConfig(mcpLaunchConfig.value)); @@ -5055,8 +5061,8 @@ onUnmounted(cleanupPreviewEditor); {{ t("settings.mcpTraeConfigPath") }}
-
{{ mcpJsonRecommendedConfig }}
- diff --git a/apps/desktop/src/lib/__tests__/mcp/mcpConfigTemplates.spec.ts b/apps/desktop/src/lib/__tests__/mcp/mcpConfigTemplates.spec.ts index 8f59fd9c1..199d87d8a 100644 --- a/apps/desktop/src/lib/__tests__/mcp/mcpConfigTemplates.spec.ts +++ b/apps/desktop/src/lib/__tests__/mcp/mcpConfigTemplates.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl } from "@/lib/mcp/mcpConfigTemplates"; +import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpTraeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl } from "@/lib/mcp/mcpConfigTemplates"; describe("MCP config templates", () => { it("builds the standard mcpServers JSON used by Claude, Cursor, TRAE, and Windsurf", () => { @@ -27,6 +27,21 @@ describe("MCP config templates", () => { }); }); + it("uses the native binary for TRAE when Windows Node lives under Program Files", () => { + const nodeLaunch = { + command: "C:\\Program Files\\nodejs\\node.exe", + args: ["C:\\Users\\supervisor\\AppData\\Roaming\\npm\\node_modules\\@dbx-app\\mcp-server\\bin\\dbx-mcp-server.js"], + }; + const nativeBinPath = "C:\\Users\\supervisor\\AppData\\Roaming\\npm\\node_modules\\@dbx-app\\mcp-win32-x64\\bin\\dbx-mcp.exe"; + + expect(JSON.parse(buildMcpTraeConfig(nodeLaunch, nativeBinPath))).toEqual({ + mcpServers: { dbx: { command: nativeBinPath } }, + }); + expect(JSON.parse(buildMcpTraeConfig(nodeLaunch))).toEqual({ + mcpServers: { dbx: nodeLaunch }, + }); + }); + it("includes Web runtime settings without restoring permission environment variables", () => { const launch = { command: "dbx-mcp-server", diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 520800a39..e105f4ae9 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2306,6 +2306,7 @@ export async function checkMcpServerStatus(): Promise = { type: "stdio", diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 5b94528cf..e219770ec 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -23,6 +23,7 @@ pub struct McpServerStatus { pub latest_version: Option, pub update_available: bool, pub bin_path: Option, + pub native_bin_path: Option, pub script_path: Option, pub install_command: String, pub update_command: String, @@ -48,6 +49,7 @@ struct NodeRuntime { mcp_version: Option, mcp_script_path: Option, mcp_bin_path: Option, + mcp_native_bin_path: Option, } #[derive(Debug)] @@ -81,8 +83,20 @@ impl NodeRuntime { // Resolve the package-declared launcher so npm layout changes do not break the built-in AI assistant. let mcp_script_path = package.filter(|_| package_is_compatible).map(|package| package.script_path); let mcp_bin_path = mcp_bin_path(&npm_prefix); + // 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(|| mcp_native_binary_path(&package_root, &npm_root)).flatten(); - Some(Self { node_path, npm_cli_path, npm_root, node_version, mcp_version, mcp_script_path, mcp_bin_path }) + Some(Self { + node_path, + npm_cli_path, + npm_root, + node_version, + mcp_version, + mcp_script_path, + mcp_bin_path, + mcp_native_bin_path, + }) } fn has_mcp_package(&self) -> bool { @@ -124,6 +138,8 @@ pub async fn check_mcp_server_status() -> Result { let current_version = runtime.as_ref().and_then(|runtime| runtime.mcp_version.clone()); let script_path = runtime.as_ref().and_then(|runtime| runtime.mcp_script_path.as_ref()).map(|path| path_string(path)); + let native_bin_path = + runtime.as_ref().and_then(|runtime| runtime.mcp_native_bin_path.as_ref()).map(|path| path_string(path)); let bin_path = fallback_bin.as_ref().map(|path| path_string(path)); let latest_version = latest_version.ok(); let update_available = current_version @@ -145,6 +161,7 @@ pub async fn check_mcp_server_status() -> Result { latest_version, update_available, bin_path, + native_bin_path, script_path, install_command: MCP_INSTALL_COMMAND.to_string(), update_command: MCP_INSTALL_COMMAND.to_string(), @@ -562,6 +579,43 @@ fn mcp_package(package_root: &Path) -> Option { Some(McpPackage { version, script_path, minimum_node_version }) } +fn mcp_native_binary_path(package_root: &Path, npm_root: &Path) -> Option { + let (package_name, binary_name) = mcp_native_package()?; + mcp_native_binary_path_for(package_root, npm_root, package_name, binary_name) +} + +fn mcp_native_binary_path_for( + package_root: &Path, + npm_root: &Path, + package_name: &str, + binary_name: &str, +) -> Option { + [ + package_root.join("node_modules").join(package_name).join("bin").join(binary_name), + npm_root.join(package_name).join("bin").join(binary_name), + ] + .into_iter() + .find_map(|path| canonical_runtime_path(&path)) +} + +fn mcp_native_package() -> Option<(&'static str, &'static str)> { + if cfg!(all(target_os = "macos", target_arch = "aarch64")) { + Some(("@dbx-app/mcp-darwin-arm64", "dbx-mcp")) + } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { + Some(("@dbx-app/mcp-darwin-x64", "dbx-mcp")) + } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { + Some(("@dbx-app/mcp-linux-arm64-gnu", "dbx-mcp")) + } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { + Some(("@dbx-app/mcp-linux-x64-gnu", "dbx-mcp")) + } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) { + Some(("@dbx-app/mcp-win32-arm64", "dbx-mcp.exe")) + } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) { + Some(("@dbx-app/mcp-win32-x64", "dbx-mcp.exe")) + } else { + None + } +} + fn parse_minimum_node_version(requirement: &str) -> Option { let version = requirement.trim().strip_prefix(">=")?.split_whitespace().next()?; parse_node_version(version) @@ -854,10 +908,10 @@ mod tests { #[cfg(not(windows))] use super::{bash_login_script, canonical_runtime_path, NodeRuntimeCandidate}; use super::{ - is_mcp_compatible_node_version, mcp_command_for_runtime, mcp_package, normalized_reported_path, - npm_cli_candidates, parse_minimum_node_version, parse_node_version, prefer_runtime, prefixed_output_path, - 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, + 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, + prefixed_output_path, 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}; @@ -881,6 +935,7 @@ mod tests { mcp_version: script_path.map(|_| "0.4.29".to_string()), mcp_script_path: script_path.map(PathBuf::from), mcp_bin_path: None, + mcp_native_bin_path: None, } } @@ -1019,6 +1074,38 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + #[test] + fn native_binary_resolves_nested_and_hoisted_optional_packages() { + 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-mcp-native-package-test-{}-{nonce}", std::process::id())); + let npm_root = dir.join("node_modules"); + let package_root = npm_root.join("@dbx-app").join("mcp-server"); + let package_name = "@dbx-app/mcp-win32-x64"; + let binary_name = "dbx-mcp.exe"; + let nested_binary = package_root.join("node_modules").join(package_name).join("bin").join(binary_name); + std::fs::create_dir_all(nested_binary.parent().unwrap()).unwrap(); + std::fs::write(&nested_binary, "nested binary").unwrap(); + + assert_eq!( + mcp_native_binary_path_for(&package_root, &npm_root, package_name, binary_name), + canonical_runtime_path(&nested_binary) + ); + + std::fs::remove_file(&nested_binary).unwrap(); + let hoisted_binary = npm_root.join(package_name).join("bin").join(binary_name); + std::fs::create_dir_all(hoisted_binary.parent().unwrap()).unwrap(); + std::fs::write(&hoisted_binary, "hoisted binary").unwrap(); + + assert_eq!( + mcp_native_binary_path_for(&package_root, &npm_root, package_name, binary_name), + canonical_runtime_path(&hoisted_binary) + ); + + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn mcp_command_binds_script_to_the_installation_node() { let installed = runtime("/runtime/node-24", Some("/runtime/node-24-mcp/dist/index.js"));