Hmbown/CodeWhale · error

MCP HTTP discovered URL must not contain credentials

Error message

MCP HTTP discovered URL must not contain credentials

What it means

During redirect handling, discovered (non-operator, non-same-origin) targets are checked for URL-embedded credentials. Only the operator-configured same-origin path is exempt; any other redirect target carrying user:password@ is rejected to avoid sending credentials to an endpoint that was never explicitly configured.

Solutions

  1. Authenticate at the discovered target with headers (via configured proxy/headers), not URL userinfo
  2. Make the redirect target same-origin and operator-configured if credentials-in-URL is unavoidable
  3. Remove credentials from the redirecting server's Location header
Defensive patterns

Strategy: validation

Validate before calling

let next = base.join(location)?;
let same_origin = next.origin().ascii_serialization() == client_origin;
if !same_origin && (!next.username().is_empty() || next.password().is_some()) {
    // discovered target carries credentials -> will be rejected
}

Try / catch

if let Err(e) = client.execute(req).await {
    if e.to_string().contains("discovered URL must not contain credentials") {
        // switch auth to headers or restrict redirect to operator-configured origin
    }
}

Prevention

When it happens

Trigger: client_for_target computes operator_origin = operator_configured && same_origin; when operator_origin is false and url_has_credentials(url) is true (redirect target on another origin, or runtime-added client, with userinfo in the URL).

Common situations: Redirect chain lands on a different-origin host that includes basic-auth in the URL; a plugin bounces to an auth gateway with embedded credentials; legacy staging URL with credentials reached via redirect.

Related errors


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

Appendix: source

Thrown at crates/tui/src/mcp/http_client.rs:171

                    }
                }
                *request.headers_mut() = headers;
            }
            *request.url_mut() = next_url;
        }
        unreachable!("redirect loop is bounded")
    }

    async fn client_for_target(&self, url: &Url) -> Result<reqwest::Client> {
        validate_url(url)?;
        let same_origin = url.origin().ascii_serialization() == self.origin;
        if self.reviewed_plugin && !super::reviewed_redirect_matches_origin(url, &self.origin) {
            bail!("MCP redirect leaves the reviewed plugin origin");
        }
        validate_network_policy(url, self.network_policy.as_ref())?;
        let operator_origin = self.operator_configured && same_origin;
        if !operator_origin && url_has_credentials(url) {
            bail!("MCP HTTP discovered URL must not contain credentials");
        }
        let proxy =
            super::configured_mcp_proxy(url, !operator_origin || self.reviewed_plugin, |key| {
                std::env::var(key)
            })?;
        // A selected operator proxy resolves its own destinations. This is
        // delegated proxy authority, never evidence of a local DNS pin.
        let pin = if (self.private_origin_allowed && same_origin) || proxy.is_some() {
            None
        } else {
            self.public_dns_pin(url).await?
        };
        // Validate DNS before reusing a client too: a new private answer revokes
        // this request. Each cached client itself remains pinned to its old public
        // address, including reconnects after a keep-alive socket expires.
        let key = format!("{}:{pin:?}", url.origin().ascii_serialization());
        if proxy.is_none()
            && let Some(client) = self

View on GitHub (pinned to 73e0f67d83)