windmill-labs/windmill · error

MCP server URL is not allowed: {}

Error message

MCP server URL is not allowed: {}

What it means

Before connecting to an MCP server defined by a resource, the client validates the server URL with the SSRF validator (validate_mcp_server_url). Because a (potentially secret) bearer token is sent to the URL, URLs resolving to private/loopback/metadata addresses or otherwise disallowed targets are rejected and mapped to this message.

Source

Thrown at backend/windmill-mcp/src/client/mod.rs:49

    available_tools: Vec<McpTool>,
}

impl McpClient {
    /// Create a new MCP client from a resource configuration.
    ///
    /// `token`, when present, is the already-resolved bearer token sent as an
    /// `Authorization` header. It MUST be resolved by the caller through the
    /// permissioned (RLS + audit) variable path — `from_resource` never reads
    /// secrets itself, so a caller cannot trick it into decrypting a variable
    /// they are not allowed to read.
    pub async fn from_resource(resource: McpResource, token: Option<String>) -> Result<Self> {
        // The resource URL is author-controlled and we send a (potentially
        // secret) bearer token to it, so it must be validated against SSRF
        // before we connect (e.g. cloud metadata endpoints, internal services).
        let validated = windmill_common::ssrf::validate_mcp_server_url(&resource.url)
            .await
            .map_err(|e| {
                anyhow::anyhow!(
                    "MCP server URL is not allowed: {}",
                    windmill_common::ssrf::mcp_ssrf_error_message(&e)
                )
            })?;

        // Build custom reqwest client with headers if provided
        let mut headers = HeaderMap::new();
        if let Some(token) = token {
            let token = token.trim();
            if !token.is_empty() {
                headers.insert(
                    HeaderName::from_static("authorization"),
                    HeaderValue::from_str(format!("Bearer {}", token).as_str())?,
                );
            }
        }
        if let Some(resource_headers) = &resource.headers {
            for (key, value) in resource_headers {

View on GitHub (pinned to e474e8803c)

Solutions

  1. Change the resource URL to a publicly reachable HTTPS endpoint
  2. If self-hosting, expose the MCP server via a public domain/TLS ingress instead of an internal address
  3. Check the embedded mcp_ssrf_error_message for the exact rejected reason (scheme, IP range, redirect)

Example fix

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

Strategy: validation

Validate before calling

const url = new URL(resource.url);
if (!['https:', 'http:'].includes(url.protocol)) throw new Error('MCP url must be http(s)');
const res = await fetch(`https://dns.google/resolve?name=${url.hostname}&type=A`);
const ips = (await res.json()).Answer?.map(a => a.data) ?? [];
const blocked = ip => ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('169.254.') || /^127\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip);
if (ips.some(blocked)) throw new Error('MCP url resolves to a private/metadata IP');

Type guard

function isPublicHttpsUrl(raw) {
  try { const u = new URL(raw); return (u.protocol === 'https:' || u.protocol === 'http:') && !['localhost','127.0.0.1'].includes(u.hostname); }
  catch { return false; }
}

Try / catch

try {
  const client = McpClient::from_resource(resource).await;
} catch (e) {
  if (String(e).includes('MCP server URL is not allowed')) {
    // surface the embedded SSRF reason; ask user for a public URL
  }
  throw e;
}

Prevention

When it happens

Trigger: Creating an MCP resource whose url points at localhost, 169.254.169.254 (cloud metadata), an internal RFC1918 address, a non-http(s) scheme, or a hostname resolving to a blocked IP; self-hosted MCP servers on private networks in strict environments.

Common situations: Pointing an MCP resource at a dev server on localhost; a DNS record that now resolves to an internal IP; cloud deployments where internal ranges are hard-blocked.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/46cc91be1fccb724. Report an issue: GitHub.