koala73/worldmonitor · error · McpProxySsrfError

serverUrl hostname is blocked

Error message

serverUrl hostname is blocked

What it means

assertServerUrlSafe performs SSRF checks on user-supplied serverUrl values before the proxy fetches them. If the hostname is in BLOCKED_HOSTNAMES (localhost, metadata endpoints, etc.) it throws McpProxySsrfError('serverUrl hostname is blocked'). The proxy deliberately refuses to fetch internal/reserved hosts.

Solutions

  1. Use a public, externally reachable URL for the MCP server (not localhost/127.0.0.1/internal hostnames)
  2. Expose your local MCP server via a public tunnel (e.g. ngrok-style) if you must reach a dev server
  3. If this is your own infrastructure, add the required hostname to the allow path via the sanctioned config mechanism rather than bypassing the check
  4. Treat the error as a security guardrail — do not try to evade it

Example fix

// before
registerMcpServer({ serverUrl: 'http://localhost:8080/mcp' }); // blocked
// after
registerMcpServer({ serverUrl: 'https://mcp.example.com/mcp' }); // public host
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED = new Set(['localhost','127.0.0.1','0.0.0.0','169.254.169.254','metadata.google.internal']);
function serverUrlIsPublic(u) { const h = new URL(u).hostname.toLowerCase(); return !BLOCKED.has(h) && !/(^|\.)local$|\.internal$/.test(h); }

Type guard

function isPublicHttpUrl(v) { try { const u = new URL(v); return (u.protocol === 'https:' || u.protocol === 'http:') && !/(^|\.)local$|\.internal$|^localhost$/.test(u.hostname.toLowerCase()); } catch { return false; } }

Try / catch

try { await validateServerUrl(serverUrl); } catch (e) { if (e instanceof McpProxySsrfError) return { error: 'blocked_server_url', detail: e.message }; throw e; }

Prevention

When it happens

Trigger: Registering or fetching an MCP server whose serverUrl hostname resolves to a blocked literal (localhost, 127.0.0.1 names, cloud metadata hosts like 169.254.169.254's hostname forms, *.internal entries) — at validateServerUrl time or revalidateBeforeFetch time on each request.

Common situations: Developers pointing serverUrl at a local MCP server (http://localhost:3000) that works locally but is blocked in the deployed proxy; attackers probing SSRF; config copied from internal network docs.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/50e535f7fa1db266. Report an issue: GitHub.

Appendix: source

Thrown at api/mcp-proxy.ts:333

    .filter(answer => answer?.type === expectedType && typeof answer?.data === 'string')
    .map(answer => answer.data);
}

async function defaultResolveHostname(hostname, signal) {
  const resolveHostnameForTest = getResolveHostnameForTest();
  if (resolveHostnameForTest) return resolveHostnameForTest(hostname, signal);
  const records = await Promise.all([
    resolveDnsJson(hostname, 'A', signal),
    resolveDnsJson(hostname, 'AAAA', signal),
  ]);
  return records.flat();
}

async function assertServerUrlSafe(url, signal) {
  signal?.throwIfAborted();
  const hostname = url.hostname.toLowerCase();
  if (BLOCKED_HOSTNAMES.has(hostname)) {
    throw new McpProxySsrfError('serverUrl hostname is blocked');
  }
  if (isBlockedResolvedAddress(hostname)) {
    throwBlockedAddress(hostname);
  }

  let resolvedAddresses;
  try {
    resolvedAddresses = await defaultResolveHostname(hostname, signal);
  } catch (error) {
    signal?.throwIfAborted();
    throw new McpProxySsrfError('serverUrl DNS resolution failed', { cause: error });
  }
  signal?.throwIfAborted();

  if (!resolvedAddresses.length) {
    throw new McpProxySsrfError('serverUrl DNS resolution returned no addresses');
  }

View on GitHub (pinned to 7d06c8633d)