fix(mcp): use native binary for TRAE on Windows

This commit is contained in:
t8y2 2026-07-22 13:31:00 +08:00
parent 4e68cd84cc
commit 01fde8879f
6 changed files with 124 additions and 10 deletions

View File

@ -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<McpLaunchConfig | undefined>(() => {
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") }}
</div>
<div class="relative rounded-md border bg-background p-3">
<pre class="overflow-x-auto whitespace-pre text-xs leading-relaxed"><code>{{ mcpJsonRecommendedConfig }}</code></pre>
<Button type="button" variant="outline" size="icon" class="absolute right-2 top-2 h-7 w-7" :title="t('common.copy')" @click="copyMcpText('trae-config', mcpJsonRecommendedConfig)">
<pre class="overflow-x-auto whitespace-pre text-xs leading-relaxed"><code>{{ mcpTraeRecommendedConfig }}</code></pre>
<Button type="button" variant="outline" size="icon" class="absolute right-2 top-2 h-7 w-7" :title="t('common.copy')" @click="copyMcpText('trae-config', mcpTraeRecommendedConfig)">
<CheckCircle2 v-if="mcpCopied === 'trae-config'" class="h-3.5 w-3.5 text-green-500" />
<Copy v-else class="h-3.5 w-3.5" />
</Button>

View File

@ -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",

View File

@ -2306,6 +2306,7 @@ export async function checkMcpServerStatus(): Promise<import("@/lib/backend/taur
latest_version: null,
update_available: false,
bin_path: null,
native_bin_path: null,
script_path: null,
install_command: "npm install -g @dbx-app/mcp-server@latest --registry=https://registry.npmjs.org",
update_command: "npm install -g @dbx-app/mcp-server@latest --registry=https://registry.npmjs.org",

View File

@ -1449,6 +1449,7 @@ export interface McpServerStatus {
latest_version: string | null;
update_available: boolean;
bin_path: string | null;
native_bin_path: string | null;
script_path: string | null;
install_command: string;
update_command: string;

View File

@ -40,6 +40,10 @@ export function buildMcpJsonConfig(config?: McpLaunchConfig): string {
return JSON.stringify({ mcpServers: { dbx } }, null, 2);
}
export function buildMcpTraeConfig(config?: McpLaunchConfig, nativeBinPath?: string): string {
return buildMcpJsonConfig(nativeBinPath ? { command: nativeBinPath } : config);
}
export function buildMcpVsCodeConfig(config?: McpLaunchConfig): string {
const dbx: Record<string, unknown> = {
type: "stdio",

View File

@ -23,6 +23,7 @@ pub struct McpServerStatus {
pub latest_version: Option<String>,
pub update_available: bool,
pub bin_path: Option<String>,
pub native_bin_path: Option<String>,
pub script_path: Option<String>,
pub install_command: String,
pub update_command: String,
@ -48,6 +49,7 @@ struct NodeRuntime {
mcp_version: Option<String>,
mcp_script_path: Option<PathBuf>,
mcp_bin_path: Option<PathBuf>,
mcp_native_bin_path: Option<PathBuf>,
}
#[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<McpServerStatus, String> {
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<McpServerStatus, String> {
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<McpPackage> {
Some(McpPackage { version, script_path, minimum_node_version })
}
fn mcp_native_binary_path(package_root: &Path, npm_root: &Path) -> Option<PathBuf> {
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<PathBuf> {
[
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<NodeVersion> {
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"));