Hmbown/CodeWhale · error

MCP HTTP destination is a restricted IP address

Error message

MCP HTTP destination is a restricted IP address

What it means

This error is thrown by McpHttpClient::public_dns_pin as an SSRF guard. When the MCP server URL host is a literal IP address, the client checks it against is_restricted_ip (the same guard used by the web-fetch tooling) and refuses to connect if the address is private, loopback, link-local, or otherwise restricted. It exists to stop a configured MCP endpoint from reaching internal network services.

Solutions

  1. Expose the MCP server on a publicly resolvable DNS name (or a routable address that is not in a restricted range) and use that in the server config
  2. If the server must stay local, run it through a tunnel (e.g. a public HTTPS endpoint) the guard permits
  3. Check the URL in the MCP server config for a typo that turned a hostname into an IP literal
  4. If the restriction is genuinely wrong for your deployment, change the config environment rather than bypassing the guard

Example fix

// before
"mcpServers": { "local": { "url": "http://127.0.0.1:8080/mcp" } }
// after
"mcpServers": { "local": { "url": "https://mcp.example.com/mcp" } }
Defensive patterns

Strategy: validation

Validate before calling

fn is_mcp_url_allowed(url: &url::Url) -> bool {
    url.host_str()
        .map(|h| h.parse::<std::net::IpAddr>().map(|ip| !is_restricted_ip(&ip)).unwrap_or(true))
        .unwrap_or(false)
}

Type guard

fn is_literal_restricted_ip(host: &str) -> bool {
    host.trim_start_matches('[').trim_end_matches(']')
        .parse::<std::net::IpAddr>()
        .map(|ip| is_restricted_ip(&ip))
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling client_for_target / public_dns_pin with an MCP server URL whose host is a literal restricted IP (e.g. http://127.0.0.1:8080, http://10.0.0.5, http://192.168.1.10, http://169.254.169.254).

Common situations: Developers pointing an MCP server config at a local dev server (localhost by IP), Docker-internal addresses, or a staging box on a private subnet; CI environments where the MCP server runs on 127.0.0.1.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

fn validate_url(url: &Url) -> Result<()> {
    if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
        bail!("MCP HTTP requires an http:// or https:// URL with a host");
    }
    Ok(())
}

fn url_has_credentials(url: &Url) -> bool {
    !url.username().is_empty() || url.password().is_some()
}

impl McpHttpClient {
    async fn public_dns_pin(&self, url: &Url) -> Result<Option<(String, SocketAddr)>> {
        let host = url.host_str().context("MCP URL has no host")?;
        let literal = host.trim_start_matches('[').trim_end_matches(']');
        if let Ok(ip) = literal.parse::<IpAddr>() {
            if is_restricted_ip(&ip) {
                bail!("MCP HTTP destination is a restricted IP address");
            }
            return Ok(None);
        }
        let port = url.port_or_known_default().context("MCP URL has no port")?;
        #[cfg(test)]
        let injected = self
            .dns_answers
            .lock()
            .unwrap()
            .as_mut()
            .map(|answers| answers.pop_front().expect("DNS fixture answer available"));
        #[cfg(not(test))]
        let injected: Option<Vec<SocketAddr>> = None;
        let addresses: Vec<_> = if let Some(addresses) = injected {
            addresses
        } else {
            tokio::time::timeout(self.connect_timeout, tokio::net::lookup_host((host, port)))
                .await

View on GitHub (pinned to 73e0f67d83)