Hmbown/CodeWhale · error · anyhow::Error

MCP stdio spawn failed

Error message

MCP stdio spawn failed (transport=stdio server={server_name} cmd={command:?} args={:?} env_keys={env_keys:?})

What it means

When spawning an MCP stdio server process fails, spawn builds a diagnostic message embedding the transport, server name, command, args, and only the env variable NAMES (never values) before attaching the original error as context. This gives enough detail to debug the launch without leaking secrets.

Solutions

  1. Run the configured command and args by hand in a shell to see the OS-level error.
  2. Install the server binary or fix its absolute path in the MCP server config.
  3. Verify required env vars referenced by the config are set (only their keys appear in the message).
  4. Check execute permissions on the command file.

Example fix

// before: config
"command": "my-mcp-server"
// after: install it or use an absolute path
"command": "/usr/local/bin/my-mcp-server"
Defensive patterns

Strategy: validation

Validate before calling

let which = which(&command);
if which.is_none() { eprintln!("command not found: {command}"); }
// also verify execute bit: metadata.permissions().mode() & 0o111 != 0

Try / catch

match spawn().await {
    Err(e) if e.to_string().contains("MCP stdio spawn failed") => {
        eprintln!("{e:#}; install the server binary or fix the configured path");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Child-process spawn fails for an MCP server configured with transport=stdio: the command binary does not exist, is not executable, the working directory is missing, or env expansion yields an unusable command.

Common situations: Server command not installed or not on PATH (e.g. npx/uvx missing); wrong path in config; missing execute bit; typo in command name; interpreter version too old.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/81e589ab1e51ed41. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/stdio.rs:184

            let message = if error.kind() == std::io::ErrorKind::NotFound
                && super::is_node_command(command)
                && launch_cwd.is_none_or(|directory| directory.is_dir())
            {
                format!("MCP server {server_name} could not start because Node.js was not found. Install Node.js from https://nodejs.org/ and restart Codewhale with node on PATH. Built-in Computer Use requires Node.js 20 or newer.")
            } else if config.reviewed_plugin.is_some() {
                format!(
                    "MCP stdio spawn failed (transport=stdio server={server_name} reviewed-plugin argv_count={} env_count={})",
                    config.args.len(),
                    expanded_env.len(),
                )
            } else {
                let env_keys: Vec<&str> = expanded_env.keys().map(String::as_str).collect();
                format!(
                    "MCP stdio spawn failed (transport=stdio server={server_name} cmd={command:?} args={:?} env_keys={env_keys:?})",
                    config.args,
                )
            };
            anyhow::Error::new(error).context(message)
        })?;

        let stdin = child.stdin.take().context("Failed to get MCP stdin")?;
        let stdout = child.stdout.take().context("Failed to get MCP stdout")?;
        let stderr = child.stderr.take().context("Failed to get MCP stderr")?;

        // Drain stderr into a bounded ring buffer so a crash mid-run leaves
        // diagnostic breadcrumbs instead of disappearing into `Stdio::null`.
        // The task exits naturally when the child closes its stderr
        // (kill_on_drop / exit / explicit shutdown).
        let stderr_tail = StderrTail::new();
        {
            let tail = Arc::clone(&stderr_tail);
            // A reviewed plugin child receives environment-backed values that
            // are intentionally absent from its manifest. Still drain its
            // stderr to avoid blocking, but do not retain or surface arbitrary
            // child output that could echo those credentials into a chat or
            // persisted transcript.

View on GitHub (pinned to 73e0f67d83)