Skip to content

The abstract method uses which under the hood. #456

Description

@adsqx

🪟 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:

  1. Automatically resolves the executable path using which under the hood.
  2. Provides a fluent API with .arg(...) chaining for adding arguments.
  3. Offers .configure(...) for low-level full control over the Command.
  4. 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 !

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium: important but non-blocking improvementT-transportTransport layer changesbugSomething is not workingneeds reproBug report that needs a minimal reproduction case

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions