feat(transport): add which_command for cross-platform executable resolution (#774)

* 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

* refactor(transport): move which_command behind opt-in feature flag

Address review feedback: the `which` dependency is now gated behind a
separate `which-command` feature flag instead of being bundled into
`transport-child-process`. Users on Linux/macOS who don't need
cross-platform executable resolution no longer pull in the extra crate.

Also fixes the doc example import path to use the re-exported
`rmcp::transport::which_command`.
This commit is contained in:
Axel 2026-04-07 16:28:23 +05:30 • committed by GitHub
commit cabf71aa74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 0 deletions

View file

@ -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};

View file

@ -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::ffi::OsStr>,
) -> std::io::Result<tokio::process::Command> {
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 {