-
Notifications
You must be signed in to change notification settings - Fork 384
Description
🪟 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]
async fn main() -> 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 intokio::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 searchesPATH
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 theCommand
. - Returns a ready-to-use
TokioChildProcess
.
use which::which;
use rmcp::{ServiceExt, transport::{TokioChildProcess, ConfigureCommandExt}};
use tokio::process::Command;
use std::error::Error;
struct CmdBuilder {
command: Command,
}
impl CmdBuilder {
/// Creates the builder and resolves the full program path (important on Windows).
fn new(name: &str) -> Result<Self, Box<dyn Error>> {
let path = which(name)?;
Ok(Self { command: Command::new(path) })
}
/// Adds an argument (chainable).
fn arg(mut self, arg: &str) -> Self {
self.command.arg(arg);
self
}
/// Allows full, low-level access to `Command`.
fn configure<F>(mut self, f: F) -> Self
where
F: FnOnce(&mut Command),
{
f(&mut self.command);
self
}
/// Finalizes and returns a `TokioChildProcess`.
fn build(self) -> TokioChildProcess<Command> {
TokioChildProcess::new(self.command)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
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 !