ultraworkers/claw-code · error · std::io::Error

MCP bootstrap transport for {} is not stdio: {other:?}

Error message

MCP bootstrap transport for {} is not stdio: {other:?}

What it means

`spawn_mcp_stdio_process` (runtime/src/mcp_stdio.rs:1394) only knows how to spawn stdio servers: it matches `McpClientTransport::Stdio` and rejects everything else — Sse, Http, WebSocket, Sdk, ManagedProxy (mcp_client.rs:9) — with `InvalidInput` naming the configured server. Reaching it with a remote-style server means the caller routed a non-stdio config into the stdio spawner.

Source

Thrown at rust/crates/runtime/src/mcp_stdio.rs:1394

    }

    async fn shutdown(&mut self) -> io::Result<()> {
        if self.child.try_wait()?.is_none() {
            match self.child.kill().await {
                Ok(()) => {}
                Err(error) if error.kind() == io::ErrorKind::InvalidInput => {}
                Err(error) => return Err(error),
            }
        }
        let _ = self.child.wait().await?;
        Ok(())
    }
}

pub fn spawn_mcp_stdio_process(bootstrap: &McpClientBootstrap) -> io::Result<McpStdioProcess> {
    match &bootstrap.transport {
        McpClientTransport::Stdio(transport) => McpStdioProcess::spawn(transport),
        other => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "MCP bootstrap transport for {} is not stdio: {other:?}",
                bootstrap.server_name
            ),
        )),
    }
}

fn apply_env(command: &mut Command, env: &BTreeMap<String, String>) {
    for (key, value) in env {
        command.env(key, value);
    }
}

fn encode_frame(payload: &[u8]) -> Vec<u8> {
    let header = format!("Content-Length: {}\r\n\r\n", payload.len());
    let mut framed = header.into_bytes();

View on GitHub (pinned to 08106b0c37)

Solutions

  1. If the server should be stdio, change its config back to command/args (with env) form so it parses as McpClientTransport::Stdio.
  2. If the server is genuinely remote, route it through the SSE/HTTP client path instead of the stdio spawner.
  3. Dispatch on the transport enum before spawning so each transport reaches its own client.

Example fix

# before (config uses a url, but this path only spawns stdio)
[mcp.docs]
url = "https://mcp.example.com/sse"

# after (either make it stdio)
[mcp.docs]
command = "npx"
args = ["-y", "@mcp/docs-server"]
# ...or keep the url and use the SSE/HTTP MCP client instead of spawn_mcp_stdio_process
Defensive patterns

Strategy: type-guard

Validate before calling

if !matches!(bootstrap.transport, McpClientTransport::Stdio(_)) {
    // route to the SSE/HTTP/SDK client path instead of the stdio spawner
}

Type guard

fn is_stdio_bootstrap(b: &McpClientBootstrap) -> bool {
    matches!(b.transport, McpClientTransport::Stdio(_))
}

Try / catch

match spawn_mcp_stdio_process(bootstrap) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput
        && e.to_string().contains("is not stdio") => { /* dispatch remote transports to their own client */ }
    other => other,
}

Prevention

When it happens

Trigger: An MCP config entry declared with a `url` (SSE/HTTP/WebSocket transport) passed to a code path that calls spawn_mcp_stdio_process; config migration renaming stdio fields so the entry no longer parses as Stdio; a managed-proxy/SDK entry fed to the generic spawn helper.

Common situations: Mixed MCP fleets (some `command:` servers, some `url:` servers) where the dispatcher assumes all are stdio; switching a server entry from `command/args` to `url` without updating the client path that consumed it.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/567a0237fe8d45c2. Report an issue: GitHub.