Skip to content
Open
99 changes: 92 additions & 7 deletions src-tauri/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -107,7 +108,18 @@ 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));
// 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)
Expand All @@ -118,8 +130,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)))
Expand Down Expand Up @@ -158,6 +175,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,
Expand All @@ -170,6 +194,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"]),
}
}
Expand Down Expand Up @@ -238,7 +265,7 @@ pub async fn check_mcp_server_status(app: AppHandle) -> Result<McpServerStatus,
native_bin_path,
script_path,
data_dir,
install_command: MCP_INSTALL_COMMAND.to_string(),
install_command: runtime.as_ref().map(NodeRuntime::install_command).unwrap_or(MCP_INSTALL_COMMAND).to_string(),
update_command: runtime.as_ref().map(NodeRuntime::update_command).unwrap_or(MCP_INSTALL_COMMAND).to_string(),
uninstall_command: runtime
.as_ref()
Expand Down Expand Up @@ -579,6 +606,13 @@ fn normalize_canonical_path(path: PathBuf) -> PathBuf {
fn find_npm_cli(node_path: &Path, launcher_dir: Option<&Path>) -> Option<PathBuf> {
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| {
Expand Down Expand Up @@ -616,12 +650,35 @@ fn node_script_from_launcher(path: &Path) -> Option<PathBuf> {
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\<hash>\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<PathBuf> {
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<PathBuf> {
if std::fs::metadata(path).ok()?.len() > 128 * 1024 {
return None;
Expand Down Expand Up @@ -1133,9 +1190,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};
Expand Down Expand Up @@ -1216,6 +1274,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);
Expand Down
Loading