Hmbown/CodeWhale · error

MCP server URL '{server_url}' must include a host

Error message

MCP server URL '{server_url}' must include a host

What it means

callback_id_from_server_url() derives the OAuth callback path by hashing the normalized server URL, which requires a host component. Url::parse() accepted the string but host_str() returned None — true for hostless, non-hierarchical URLs (file:, data:, about:, mailto:, unix:) or URLs like http:///path with an empty authority.

Source

Thrown at crates/tui/src/mcp/oauth.rs:1103

fn callback_bind_host(callback_url: Option<&str>) -> &'static str {
    let Some(callback_url) = callback_url else {
        return "127.0.0.1";
    };
    let Ok(parsed) = Url::parse(callback_url) else {
        return "127.0.0.1";
    };
    match parsed.host_str() {
        Some("localhost" | "127.0.0.1" | "::1") | None => "127.0.0.1",
        Some(_) => "0.0.0.0",
    }
}

fn callback_id_from_server_url(server_url: &str) -> Result<String> {
    let mut parsed =
        Url::parse(server_url).with_context(|| format!("invalid MCP server URL '{server_url}'"))?;
    parsed
        .host_str()
        .ok_or_else(|| anyhow!("MCP server URL '{server_url}' must include a host"))?;
    parsed.set_fragment(None);
    let digest = Sha256::digest(parsed.as_str().as_bytes());
    Ok(URL_SAFE_NO_PAD.encode(&digest[..9]))
}

fn append_callback_id_to_redirect_uri(redirect_uri: &str, callback_id: &str) -> Result<String> {
    let mut parsed = Url::parse(redirect_uri)
        .with_context(|| format!("invalid redirect URI '{redirect_uri}'"))?;
    let path = parsed.path();
    let new_path = if path.ends_with('/') {
        format!("{path}{callback_id}")
    } else {
        format!("{path}/{callback_id}")
    };
    parsed.set_path(&new_path);
    Ok(parsed.to_string())
}

View on GitHub (pinned to 8880682c63)

Solutions

  1. Set the MCP server URL to a hierarchical http(s) URL with an explicit host, e.g. https://mcp.example.com/sse
  2. For local servers use http://127.0.0.1:PORT/... — this code explicitly recognizes loopback hosts
  3. Use stdio transport for local command servers instead of HTTP/OAuth
  4. Validate candidate URLs with url::Url::parse(...).host_str() before saving the config

Example fix

// before
let server_url = "file:///opt/mcp/server";

// after
let server_url = "https://mcp.internal.example.com/sse";
Defensive patterns

Strategy: validation

Validate before calling

```rust
fn validate_mcp_server_url(server_url: &str) -> anyhow::Result<()> {
    let url = url::Url::parse(server_url).context("invalid MCP server URL")?;
    anyhow::ensure!(url.host_str().is_some(), "MCP server URL must include a host");
    Ok(())
}
```

Type guard

```rust
fn mcp_url_has_host(server_url: &str) -> bool {
    url::Url::parse(server_url)
        .ok()
        .and_then(|u| u.host_str().map(|_| true))
        .unwrap_or(false)
}
```

Prevention

When it happens

Trigger: Starting MCP OAuth callback-id derivation with a server URL such as "file:///opt/mcp", "unix:/run/mcp.sock", "about:blank", or "http:///api" — parseable but hostless.

Common situations: Placeholder or stdio-style paths pasted into an HTTP/OAuth MCP server entry; local socket endpoints where an http(s) URL is required; typos dropping the host after the scheme.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/67e710bd719f4181. Report an issue: GitHub.