Skip to content

The abstract method uses which under the hood. #456

@AdsQnn

Description

@AdsQnn

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

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 !

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions