diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index cbf02ea4..b2229006 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 } @@ -134,6 +137,10 @@ transport-child-process = [ "tokio/process", "dep:process-wrap", ] +which-command = [ + "transport-child-process", + "dep:which", +] transport-streamable-http-server = [ "transport-streamable-http-server-session", "server-side-http", diff --git a/crates/rmcp/src/transport.rs b/crates/rmcp/src/transport.rs index 2a11f4fe..6fe4413e 100644 --- a/crates/rmcp/src/transport.rs +++ b/crates/rmcp/src/transport.rs @@ -83,6 +83,8 @@ pub use worker::WorkerTransport; #[cfg(feature = "transport-child-process")] pub mod child_process; +#[cfg(feature = "which-command")] +pub use child_process::which_command; #[cfg(feature = "transport-child-process")] pub use child_process::{ConfigureCommandExt, TokioChildProcess}; diff --git a/crates/rmcp/src/transport/child_process.rs b/crates/rmcp/src/transport/child_process.rs index e33800b1..ebb6cc92 100644 --- a/crates/rmcp/src/transport/child_process.rs +++ b/crates/rmcp/src/transport/child_process.rs @@ -233,6 +233,56 @@ 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::{which_command, ConfigureCommandExt}; +/// +/// # fn example() -> std::io::Result<()> { +/// let cmd = which_command("npx")? +/// .configure(|cmd| { +/// cmd.arg("-y").arg("@modelcontextprotocol/server-everything"); +/// }); +/// # Ok(()) +/// # } +/// ``` +#[cfg(feature = "which-command")] +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(feature = "which-command")] +#[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 {