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
- If the server should be stdio, change its config back to command/args (with env) form so it parses as McpClientTransport::Stdio.
- If the server is genuinely remote, route it through the SSE/HTTP client path instead of the stdio spawner.
- 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
- Dispatch on the McpClientTransport enum before spawning — only Stdio belongs in spawn_mcp_stdio_process
- Keep config truth: command/args => stdio, url => remote transport
- After editing MCP config, verify which transport each entry parses into
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
- skill source '{}' must be a directory with SKILL.md or a mar
- unable to derive an installable invocation name from '{}'
- old_string and new_string must differ
- MCP stdio stream closed while reading headers
- missing Content-Length header
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/567a0237fe8d45c2.
Report an issue: GitHub.