koala73/worldmonitor · error · ToolFetchError

HTTP

Error message

${operation} HTTP ${status}: ${safeCode}

What it means

ToolFetchError is thrown by assertMcpToolFetchOk in api/mcp/downstream.ts after classifyFailure() has safely extracted a bounded error code from the response body and emitDownstreamTelemetry has recorded the operation. It covers every non-ok downstream response on this path that is not a billing denial (those re-throw as BillingDenialError first). The message shape '<operation> HTTP <status>: <safeCode>' gives the calling tool name, the HTTP status, and a sanitized body-derived code; the typed fields also carry responseMarker (json/html/other/empty_error/method_not_allowed/etc.) for programmatic handling.

Solutions

  1. Read err.status and err.safeCode — safeCode often names the exact gateway check that failed (e.g. invalid_internal_mcp_signature)
  2. For 401/auth codes, audit buildAuthHeaders: the signed method, canonical path including query, and body must match the actual request byte-for-byte
  3. For 404/405, diff the tool's _apiPaths against the deployed routes (docs/api/ OpenAPI per service is the parity source of truth)
  4. For 5xx codes, retry with backoff; the telemetry emitted before the throw already recorded the failure for Sentry
Defensive patterns

Strategy: try-catch

Type guard

function isToolFetchError(e) {
  return e instanceof Error && e.name === 'ToolFetchError'
    && typeof e.status === 'number' && typeof e.safeCode === 'string';
}

Try / catch

try {
  const payload = await fetchTool(context, execution);
} catch (e) {
  if (isToolFetchError(e)) {
    if (e.status >= 500) return retryWithBackoff(() => fetchTool(context, execution), 3);
    if (e.safeCode.includes('signature')) throw new Error('Auth signing bug — fix buildAuthHeaders inputs', { cause: e });
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: An MCP tool _execute that goes through assertMcpToolFetchOk fetching e.g. /api/intelligence/v1/get-china-decision-signals and receiving any non-ok status: 401 from a mis-signed internal MCP signature, 404 when the registry's _apiPaths drift from deployed routes, 405 when method routing breaks, or 5xx/timeout-shaped upstream failures.

Common situations: Version skew: the MCP registry declares a path the gateway deployment does not serve. Auth canonicalization bugs (signing GET but issuing POST, or omitting the query string from the signed path). Upstream dependency outages. Method drift like the documented get-country-intel-brief GET/POST OpenAPI mismatch.

Related errors


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

Appendix: source

Thrown at api/mcp/downstream.ts:296

  emitTelemetry('mcp.downstream', {
    tool,
    auth_kind: auth.kind,
    inbound_host_class: execution.inboundHostClass,
    downstream_origin: execution.downstreamOriginTag,
    downstream_operation: operation,
    status: response.status,
    ok: response.ok,
    error_code: errorCode,
    response_marker: responseMarker,
  });
}

/**
 * Validate one MCP sibling fetch while recording only bounded routing/auth
 * diagnostics. Error response bodies are consumed solely to map a closed set
 * of gateway codes; raw text, unknown values, headers, URLs, and credentials
 * never leave this module.
 */
export async function assertMcpToolFetchOk(
  response: ToolFetchResponse,
  observation: DownstreamObservation,
): Promise<void> {
  const { operation, tool, auth, execution } = observation;
  if (response.ok) {
    emitDownstreamTelemetry(
      tool,
      operation,
      auth,
      execution,
      response,
      null,
      successMarker(response),
    );
    return;
  }

View on GitHub (pinned to e586b8b4b8)