🪟 Windows Path Resolution Problem in tokio::process::Command
On Windows, launching a process like this:
use rmcp::{ServiceExt, transport::{TokioChildProcess,ConfigureCommandExt}};use tokio::process::Command;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let client = ().serve(TokioChildProcess::new(Command::new("npx").configure(|cmd| {
cmd.arg("-y").arg("@modelcontextprotocol/server-everything");}))?).await?;Ok(())}can fail, because:
Unlike Linux or macOS, Windows does not reliably resolve executables via the PATH environment variable in tokio::process::Command.
Many tools like npx are .cmd shim scripts, often stored in locations like:
C:\Users\<User>\AppData\Roaming\npm\npx.cmd
Without the full executable path, you’ll get runtime errors like:
The system cannot find the file specified. (os error 2)
Why Use [which](https://docs.rs/which/latest/which/)
The which crate resolves the absolute path to an executable:
- On Linux/macOS, it works like the native
which command in a shell. - On Windows, it handles
.cmd / .exe resolution and searches PATH correctly.
By wrapping this logic in a high-level abstraction, your library can work seamlessly across platforms without requiring users to handle these quirks manually.
💡 Solution — A Builder That Hides which Internally
Below is a minimal CmdBuilder that:
- Automatically resolves the executable path using
which under the hood. - Provides a fluent API with
.arg(...) chaining for adding arguments. - Offers
.configure(...) for low-level full control over the Command. - Returns a ready-to-use
TokioChildProcess.
use which::which;use rmcp::{ServiceExt, transport::{TokioChildProcess,ConfigureCommandExt}};use tokio::process::Command;use std::error::Error;structCmdBuilder{command:Command,}implCmdBuilder{/// Creates the builder and resolves the full program path (important on Windows).fnnew(name:&str) -> Result<Self,Box<dynError>>{let path = which(name)?;Ok(Self{command:Command::new(path)})}/// Adds an argument (chainable).fnarg(mutself,arg:&str) -> Self{self.command.arg(arg);self}/// Allows full, low-level access to `Command`.fnconfigure<F>(mutself,f:F) -> SelfwhereF:FnOnce(&mutCommand),{f(&mutself.command);self}/// Finalizes and returns a `TokioChildProcess`.fnbuild(self) -> TokioChildProcess<Command>{TokioChildProcess::new(self.command)}}#[tokio::main]asyncfnmain() -> Result<(),Box<dynError>>{let client = ().serve(CmdBuilder::new("npx")?
.arg("-y").arg("@modelcontextprotocol/server-everything").configure(|cmd| {// Optional: add environment variables or other settings
cmd.env("MY_ENV_VAR","123");}).build(),).await?;Ok(())}
⚠️ Disclaimer
This code is provisional and not a production-ready solution. It’s only meant to illustrate an idea for solving cross-platform executable path issues.
Cheers and good luck !
🪟 Windows Path Resolution Problem in
tokio::process::CommandOn Windows, launching a process like this:
can fail, because:
Unlike Linux or macOS, Windows does not reliably resolve executables via the
PATHenvironment variable intokio::process::Command.Many tools like
npxare.cmdshim scripts, often stored in locations like:Without the full executable path, you’ll get runtime errors like:
Why Use
[which](https://docs.rs/which/latest/which/)The
whichcrate resolves the absolute path to an executable:whichcommand in a shell..cmd/.exeresolution and searchesPATHcorrectly.By wrapping this logic in a high-level abstraction, your library can work seamlessly across platforms without requiring users to handle these quirks manually.
💡 Solution — A Builder That Hides
whichInternallyBelow is a minimal
CmdBuilderthat:whichunder the hood..arg(...)chaining for adding arguments..configure(...)for low-level full control over theCommand.TokioChildProcess.This code is provisional and not a production-ready solution. It’s only meant to illustrate an idea for solving cross-platform executable path issues.
Cheers and good luck !