Hmbown/CodeWhale · error · anyhow::Error

app-server auth token cannot be empty

Error message

app-server auth token cannot be empty

What it means

The stdio MCP transport reads the server's stdout line by line through read_line_capped with a hard cap (MAX_MCP_RESPONSE_BYTES, 16 MiB in mcp/wire.rs). If a single line accumulates more than max bytes without ever seeing a newline, the read aborts with InvalidData so a misbehaving server cannot turn read_line into unbounded memory use.

Source

Thrown at crates/app-server/src/lib.rs:688

    Ok(AppState {
        config_path,
        config: Arc::new(RwLock::new(config)),
        runtime: Arc::new(RwLock::new(runtime)),
        registry,
        auth_token,
        stdio_bridge: Arc::new(Mutex::new(None)),
        stdio_thread_hints: Arc::new(Mutex::new(HashMap::new())),
        pending_user_input: Arc::new(Mutex::new(std::collections::HashMap::new())),
        in_flight_turns: Arc::new(Mutex::new(HashMap::new())),
    })
}

fn resolve_auth_token(options: &AppServerOptions) -> Result<Option<String>> {
    let configured = options.auth_token.as_ref().map(|token| token.trim());
    if let Some(token) = configured
        && token.is_empty()
    {
        bail!("app-server auth token cannot be empty");
    }
    let has_explicit_token = configured.is_some();

    if options.insecure_no_auth {
        if !options.listen.ip().is_loopback() {
            bail!("refusing unauthenticated app-server bind on non-loopback address");
        }
        eprintln!("warning: app-server HTTP auth disabled by --insecure-no-auth");
        return Ok(None);
    }

    if !has_explicit_token && !options.listen.ip().is_loopback() {
        bail!(
            "refusing non-loopback app-server bind without explicit auth token; pass --auth-token or set CODEWHALE_APP_SERVER_TOKEN"
        );
    }

    let token = configured

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Make the server log to stderr only; stdout must carry newline-terminated JSON-RPC frames
  2. Page or chunk large payloads (resource URIs with offsets) instead of one giant message
  3. Verify the configured command actually speaks MCP stdio: run it manually and confirm it emits one JSON object per line
  4. If you control both ends and truly need larger frames, raise MAX_MCP_RESPONSE_BYTES in your fork

Example fix

// server, before: console.log(JSON.stringify(hugePayload));
// server, after: console.error(JSON.stringify(hugePayload)); // logs to stderr
//                     stdout.write(JSON.stringify(smallEnvelope) + '\n');
Defensive patterns

Strategy: try-catch

Validate before calling

// Smoke-test the server before wiring it into the client:
// spawn it, send an initialize request, and assert the first stdout line
// is newline-terminated, parses as JSON, and is well under 16 MiB.
async fn server_speaks_framed_json(cmd: &str) -> bool {
    // spawn cmd; write initialize; read one line with read_line_capped-like cap;
    // return line.ends_with(b'\n') && serde_json::from_slice::<serde_json::Value>(&line).is_ok()
    true
}

Try / catch

match transport.run().await {
    Err(e) if e.to_string().contains("exceeded") && e.kind() == std::io::ErrorKind::InvalidData => {
        // mark the server transport dead; surface a config error (stdout framing broken),
        // do not retry the same oversized read
    }
    other => other,
}

Prevention

When it happens

Trigger: The MCP server writes one JSON-RPC message larger than 16 MiB with no newline (a giant embedded base64 resource or tool result), or it emits non-line-delimited output on stdout - binary data, a spinner/progress renderer, or logs printed to stdout instead of stderr.

Common situations: Servers that embed whole files in a single response; servers logging to stdout and breaking JSON-RPC framing; a wrong command configured so a non-MCP program's output is parsed as protocol traffic.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/c048fda5fe175d9c. Report an issue: GitHub.