Hmbown/CodeWhale · error · anyhow

MCP 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 a stable callback identifier by parsing the MCP server URL and hashing it; this requires the URL to have a host component. Url::parse can succeed for URLs without a host (e.g. relative or some non-special schemes), so host_str() returning None triggers this error. The URL is used to route the OAuth callback, so a hostless URL cannot be identified.

Solutions

  1. Set the MCP server URL to a valid absolute http(s) URL including a host, e.g. https://mcp.example.com
  2. Fix typos in the scheme (file://, missing scheme, etc.) in the server configuration
  3. If the server is local and hostless, do not enable OAuth for it

Example fix

// before
"url": "file:///opt/mcp/server"
// after
"url": "https://mcp.example.com"
Defensive patterns

Strategy: validation

Validate before calling

fn has_host(url: &str) -> bool {
    url::Url::parse(url).map(|u| u.host_str().is_some()).unwrap_or(false)
}
// call before OAuth setup: if !has_host(server_url) { fix config }

Prevention

When it happens

Trigger: Calling callback_id_from_server_url with a server URL that parses but has no host — e.g. "file:///path/to/server", a malformed scheme-relative URL, or an empty/whitespace string that still parses.

Common situations: Configuring an MCP server entry with a local file or Unix-socket URL instead of an http(s) URL when OAuth is enabled; typos in the configured URL scheme; template variables left unsubstituted in config.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: source

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

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 73e0f67d83)