koala73/worldmonitor · error · McpProxySsrfError

serverUrl hostname is blocked

Error message

serverUrl hostname is blocked: ${hostname}

What it means

SSRF guard error in the MCP proxy's assertServerUrlSafe: the serverUrl hostname (lowercased) matches the BLOCKED_HOSTNAMES denylist verbatim (e.g. 'localhost' or metadata-service names). Unlike resolved-IP blocks, the hostname itself is echoed because the caller already knows it.

Solutions

  1. Use the service's public hostname instead of a blocked literal name
  2. If a legitimate service shares a blocked name, expose it via an allowlisted public alias
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at api/mcp-proxy.ts:157 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21). Data as JSON: /api/errors/6bb817e301945a8d. Report an issue: GitHub.

Appendix: source

Thrown at api/mcp-proxy.ts:159

 * Emit one wm_api_usage RequestEvent per proxied call.
 *
 * Before this, `logProxyCall` was the ONLY record of a proxy request and it is
 * `console.log` — Vercel runtime logs are a live tail with no historical query,
 * so `/api/mcp-proxy` had ZERO rows in Axiom and its failure rate could not be
 * asked about after the fact. That is the same hole #4866 closed for `/mcp`;
 * this reuses the gateway's builders so rows are byte-compatible and joinable
 * on customer_id. `logProxyCall` stays: it carries target_host/header_names,
 * which the usage envelope has no field for, and existing log-ingest tooling
 * parses its shape.
 *
 * OPTIONS preflights are deliberately NOT emitted — a static 204 that cannot
 * fail would double row volume for no diagnostic value. /mcp skips them too
 * (McpUsage.skip).
 */
function emitProxyUsage(req, status: number, durationMs: number, ctx, callerIdentity = null): void {
  if (!ctx) return;
  try {
    const usageIdentity = proxyUsageIdentityFor(req, callerIdentity);
    emitUsageEvents(ctx, [buildRequestEvent({
      requestId: deriveRequestId(req),
      domain: 'mcp',
      route: '/api/mcp-proxy',
      method: req.method,
      status,
      // Measured from handler entry, so this INCLUDES the auth/rate-limit
      // gates — unlike logProxyCall's `started`, which begins after auth.
      // The usage row is the end-to-end caller-visible latency.
      durationMs,
      reqBytes: deriveReqBytes(req),
      // Not tracked: the proxy streams upstream bodies through bounded readers
      // and jsonResponse Content-Length reflects only the local denial/error
      // envelopes — never the proxied upstream size. Size questions belong to
      // MAX_MCP_PROXY_RESPONSE_BYTES, not to this row. null (not 0) so unknown
      // is not confused with an empty body (#8403).
      resBytes: null,
      customerId: usageIdentity.customer_id,

View on GitHub (pinned to e586b8b4b8)