diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 24a7ff2e88..f59e6907d8 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -351,11 +351,19 @@ fn resolve_managed_mcp_command( return Some(command); } - // PATH shims may use `#!/usr/bin/env node`, bypassing the runtime compatibility check above. - if let Some(shim) = locate_path_shim() { + let runtime = runtime.filter(|runtime| is_mcp_compatible_node_version(&runtime.node_version))?; + let shim = locate_path_shim()?; + let Some(located) = mcp_package_from_command_path(&shim) else { log::warn!("Ignoring unbound MCP package shim at {}", shim.display()); + return None; + }; + let node_version = parse_node_version(&runtime.node_version)?; + if !mcp_package_supports_node(&located.package, node_version) { + log::warn!("Ignoring MCP package shim at {} because it requires a newer Node.js runtime", shim.display()); + return None; } - None + + Some((path_string(&runtime.node_path), vec![path_string(&located.package.script_path)])) } fn require_managed_mcp_command(command: Option<(String, Vec)>) -> Result<(String, Vec), String> { @@ -712,6 +720,9 @@ fn successful_stdout(output: CommandOutput) -> Result { fn mcp_package(package_root: &Path) -> Option { let content = std::fs::read_to_string(package_root.join("package.json")).ok()?; let value: serde_json::Value = serde_json::from_str(&content).ok()?; + if value.get("name").and_then(serde_json::Value::as_str)? != MCP_PACKAGE_NAME { + return None; + } let entry = match value.get("bin")? { serde_json::Value::String(entry) => entry.as_str(), serde_json::Value::Object(entries) => { @@ -737,8 +748,8 @@ fn mcp_package(package_root: &Path) -> Option { } let version = value.get("version").and_then(serde_json::Value::as_str).map(ToOwned::to_owned); let minimum_node_version = - value.pointer("/engines/node").and_then(serde_json::Value::as_str).and_then(parse_minimum_node_version); - Some(McpPackage { version, script_path, minimum_node_version }) + value.pointer("/engines/node").and_then(serde_json::Value::as_str).and_then(parse_minimum_node_version)?; + Some(McpPackage { version, script_path, minimum_node_version: Some(minimum_node_version) }) } fn preferred_mcp_package( @@ -766,16 +777,23 @@ fn mcp_package_supports_node(package: &McpPackage, node_version: NodeVersion) -> } fn mcp_package_from_command_dir(dir: &Path) -> Option { - let (bin_path, script_path) = ["dbx-mcp-server", "mcp-server"].into_iter().find_map(|command| { + ["dbx-mcp-server", "mcp-server"].into_iter().find_map(|command| { command_file_names(command).into_iter().map(|name| dir.join(name)).find_map(|path| { if !path.is_file() { return None; } - node_script_from_launcher(&path).map(|script_path| (path, script_path)) + mcp_package_from_command_path(&path) }) - })?; + }) +} + +fn mcp_package_from_command_path(command_path: &Path) -> Option { + let bin_path = canonical_runtime_path(command_path)?; + let script_path = node_script_from_launcher(&bin_path)?; let (package_root, package) = mcp_package_from_script(&script_path)?; - let package_manager = pnpm_command_near(dir) + let package_manager = command_path + .parent() + .and_then(pnpm_command_near) .map(|command_path| McpPackageManager::Pnpm { command_path }) .unwrap_or(McpPackageManager::Npm); Some(LocatedMcpPackage { package_root, package, bin_path: Some(bin_path), package_manager }) @@ -1275,7 +1293,7 @@ mod tests { std::fs::write(¤t_entry, "// native launcher\n").unwrap(); std::fs::write( dir.join("package.json"), - r#"{"version":"0.4.38","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + r#"{"name":"@dbx-app/mcp-server","version":"0.4.38","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, ) .unwrap(); @@ -1289,7 +1307,7 @@ mod tests { std::fs::write(&legacy_entry, "// legacy server\n").unwrap(); std::fs::write( dir.join("package.json"), - r#"{"version":"0.4.32","bin":{"dbx-mcp-server":"dist/index.js"},"engines":{"node":">=22.13.0"}}"#, + r#"{"name":"@dbx-app/mcp-server","version":"0.4.32","bin":{"dbx-mcp-server":"dist/index.js"},"engines":{"node":">=22.13.0"}}"#, ) .unwrap(); @@ -1316,7 +1334,7 @@ mod tests { std::fs::write(&undeclared_entry, "// undeclared entry\n").unwrap(); std::fs::write( package_root.join("package.json"), - r#"{"version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, ) .unwrap(); @@ -1389,6 +1407,66 @@ mod tests { assert_eq!(command.1, vec!["/runtime/node-24-mcp/dist/index.js"]); } + #[test] + fn mcp_command_resolves_split_pnpm_home_shim() { + 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-split-pnpm-test-{}-{nonce}", std::process::id())); + let node_bin_dir = dir.join("node-runtime").join("bin"); + let node_path = node_bin_dir.join("node"); + let npm_path = node_bin_dir.join("npm"); + let pnpm_home = dir.join("pnpm-home"); + let pnpm_path = pnpm_home.join("pnpm"); + let mcp_shim = pnpm_home.join("dbx-mcp-server"); + let package_root = pnpm_home.join("global").join("node_modules").join("@dbx-app").join("mcp-server"); + let script_path = package_root.join("bin").join("dbx-mcp-server.js"); + + std::fs::create_dir_all(&node_bin_dir).unwrap(); + std::fs::create_dir_all(script_path.parent().unwrap()).unwrap(); + std::fs::write(&node_path, "fake node runtime\n").unwrap(); + std::fs::write(&npm_path, "fake npm cli\n").unwrap(); + std::fs::write(&pnpm_path, "fake pnpm cli\n").unwrap(); + std::fs::write(&script_path, "// fake pnpm-installed mcp server\n").unwrap(); + std::fs::write( + package_root.join("package.json"), + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + ) + .unwrap(); + std::fs::write(&mcp_shim, format!("# cmd-shim-target={}\n", script_path.display())).unwrap(); + + let node_path = canonical_runtime_path(&node_path).unwrap(); + let script_path = canonical_runtime_path(&script_path).unwrap(); + let compatible_runtime = runtime_with_version_and_root( + node_path.to_string_lossy().as_ref(), + dir.join("node-runtime").join("lib").join("node_modules").to_string_lossy().as_ref(), + None, + "v24.16.0", + ); + + let command = resolve_managed_mcp_command(Some(&compatible_runtime), || Some(mcp_shim.clone())).unwrap(); + + assert_eq!(command.0, node_path.to_string_lossy()); + assert_eq!(command.1, vec![script_path.to_string_lossy()]); + assert_ne!(command.0, mcp_shim.to_string_lossy()); + + std::fs::write( + package_root.join("package.json"), + r#"{"name":"untrusted-package","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + ) + .unwrap(); + assert!(resolve_managed_mcp_command(Some(&compatible_runtime), || Some(mcp_shim.clone())).is_none()); + + std::fs::write( + package_root.join("package.json"), + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=25.0.0"}}"#, + ) + .unwrap(); + assert!(resolve_managed_mcp_command(Some(&compatible_runtime), || Some(mcp_shim.clone())).is_none()); + + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn incompatible_runtime_does_not_fall_back_to_available_mcp_shim() { let incompatible = runtime_with_version_and_root( @@ -1443,7 +1521,7 @@ mod tests { std::fs::write(&script_path, "// fake mcp server\n").unwrap(); std::fs::write( package_root.join("package.json"), - r#"{"version":"0.4.38","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + r#"{"name":"@dbx-app/mcp-server","version":"0.4.38","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, ) .unwrap(); let node_script = format!( @@ -1516,7 +1594,7 @@ mod tests { std::fs::write(&script_path, "// fake pnpm-installed mcp server\n").unwrap(); std::fs::write( package_root.join("package.json"), - r#"{"version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, ) .unwrap(); let node_script = format!(