Repository navigation
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
PATHenvironment variable intokio::process::Command. -
Many tools like
npxare.cmdshim 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
whichcommand in a shell. - On Windows, it handles
.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 which Internally
Below is a minimal CmdBuilder that:
- Automatically resolves the executable path using
whichunder 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 !
Activity
jokemanfire commentedon Sep 30, 2025
Could this idea be proposed in the tokio:: process:: Command library?
adsqx commentedon Sep 30, 2025
I think definitely yes, especially since Rust was designed to be maximally portable.
Normally, at the system level, you need where for Windows and which for Unix/Linux, etc.
Of course, you can always implement this handling yourself if you don’t want to rely on the existing which.
But overall, the situation is quite simple, so I’m not sure it makes sense to reinvent the wheel—just use which.
If you want to write truly multiplatform code, you practically have to repeat this manually anyway, so I think it would make sense to have it included in Tokio.
I just noticed that with many libraries, for example Codex, there’s a problem where calling MCP doesn’t work on Windows, and instead of using npx users have to provide the full path. That’s why it seems to be a common issue that could be solved quite easily.
However, it’s hard for me to clearly determine what exactly the authors of Tokio are guided by and what their goals are, or whether this is in some way contradictory.
It simply seems neutral here, or even helpful.
It’s just that in that case low-level implementation might be justified. Here, however, I don’t really see a reason why anyone would want to intervene on such a low level. More likely, whenever someone builds this kind of MCP handling, they’ll want it to be multiplatform rather than tied to a dedicated system. Otherwise, it just becomes a gap for errors if someone doesn’t know how to do it properly. And in practice, it’s almost always required for things to actually work.
So Tokio is a general-purpose library, and someone might not want to use it for certain reasons in a specific project. This library, however, is a high-level tool for building MCP, so there’s really no reason to operate on that level here
alexhancock commentedon Feb 27, 2026
I'd entertain a PR!
kakarot-dev commentedon Mar 18, 2026
Hey, I'd like to take a crack at this!
I'm thinking a single helper function would be cleaner than a full builder — keeps things minimal and doesn't break existing usage:
So instead of Command::new("npx") you'd do which_command("npx")? — same API from there, works with .configure() and everything. Just resolves the path first so Windows .cmd shims don't blow up.
Would add which as an optional dep behind transport-child-process, update the npx example, and add a test.
Does this work for you @alexhancock
feat(transport): add which_command for cross-platform executable reso…