From 98ab4782f2c03d6bb67a3d21a73a2b3525188790 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 25 Mar 2026 21:06:06 +0530 Subject: [PATCH] feat(transport): add which_command for cross-platform executable resolution Adds a `which_command()` helper that resolves executable paths via the `which` crate before constructing a `tokio::process::Command`. This fixes Windows failures where `.cmd` shim scripts (e.g. `npx.cmd`) are not found by `Command::new()` without a fully-qualified path. Closes #456 --- crates/rmcp/Cargo.toml | 4 ++ crates/rmcp/src/transport.rs | 2 +- crates/rmcp/src/transport/child_process.rs | 48 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index cbf02ea48..5292a24d4 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -52,6 +52,9 @@ tower-service = { version = "0.3", optional = true } # for child process transport process-wrap = { version = "9.0", features = ["tokio1"], optional = true } +# for cross-platform executable path resolution +which = { version = "7", optional = true } + # for ws transport # tokio-tungstenite ={ version = "0.26", optional = true } @@ -133,6 +136,7 @@ transport-child-process = [ "transport-async-rw", "tokio/process", "dep:process-wrap", + "dep:which", ] transport-streamable-http-server = [ "transport-streamable-http-server-session", diff --git a/crates/rmcp/src/transport.rs b/crates/rmcp/src/transport.rs index 2a11f4fed..f2941c50c 100644 --- a/crates/rmcp/src/transport.rs +++ b/crates/rmcp/src/transport.rs @@ -84,7 +84,7 @@ pub use worker::WorkerTransport; #[cfg(feature = "transport-child-process")] pub mod child_process; #[cfg(feature = "transport-child-process")] -pub use child_process::{ConfigureCommandExt, TokioChildProcess}; +pub use child_process::{ConfigureCommandExt, TokioChildProcess, which_command}; #[cfg(feature = "transport-io")] pub mod io; diff --git a/crates/rmcp/src/transport/child_process.rs b/crates/rmcp/src/transport/child_process.rs index e33800b18..22ea63fd6 100644 --- a/crates/rmcp/src/transport/child_process.rs +++ b/crates/rmcp/src/transport/child_process.rs @@ -233,6 +233,54 @@ impl ConfigureCommandExt for tokio::process::Command { } } +/// Resolve the absolute path to an executable using the system `PATH`, +/// then return a [`tokio::process::Command`] pointing at it. +/// +/// This is especially useful on Windows where `.cmd` / `.exe` shim scripts +/// (e.g. `npx.cmd`) are not reliably found by [`tokio::process::Command`] +/// without a fully-qualified path. +/// +/// # Example +/// ```rust,no_run +/// use rmcp::transport::child_process::{which_command, ConfigureCommandExt}; +/// +/// # fn example() -> std::io::Result<()> { +/// let cmd = which_command("npx")? +/// .configure(|cmd| { +/// cmd.arg("-y").arg("@modelcontextprotocol/server-everything"); +/// }); +/// # Ok(()) +/// # } +/// ``` +pub fn which_command( + name: impl AsRef, +) -> std::io::Result { + let resolved = which::which(name.as_ref()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + Ok(tokio::process::Command::new(resolved)) +} + +#[cfg(test)] +mod tests_which { + #[test] + fn which_command_resolves_known_binary() { + // `ls` exists on every Unix system, `cmd` on Windows + #[cfg(unix)] + let result = super::which_command("ls"); + #[cfg(windows)] + let result = super::which_command("cmd"); + + assert!(result.is_ok()); + } + + #[test] + fn which_command_fails_for_nonexistent() { + let result = super::which_command("this_binary_definitely_does_not_exist_12345"); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound); + } +} + #[cfg(unix)] #[cfg(test)] mod tests {