Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 92 additions & 14 deletions src-tauri/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>)>) -> Result<(String, Vec<String>), String> {
Expand Down Expand Up @@ -712,6 +720,9 @@ fn successful_stdout(output: CommandOutput) -> Result<String, String> {
fn mcp_package(package_root: &Path) -> Option<McpPackage> {
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) => {
Expand All @@ -737,8 +748,8 @@ fn mcp_package(package_root: &Path) -> Option<McpPackage> {
}
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(
Expand Down Expand Up @@ -766,16 +777,23 @@ fn mcp_package_supports_node(package: &McpPackage, node_version: NodeVersion) ->
}

fn mcp_package_from_command_dir(dir: &Path) -> Option<LocatedMcpPackage> {
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<LocatedMcpPackage> {
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 })
Expand Down Expand Up @@ -1275,7 +1293,7 @@ mod tests {
std::fs::write(&current_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();

Expand All @@ -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();

Expand All @@ -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();

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down