koala73/worldmonitor · error · McpSourceUnavailableError

No event feeds are available for exposure enrichment

Error message

No event feeds are available for exposure enrichment

What it means

Thrown by requireAnyInput() inside the get_population_exposure MCP tool (mode 'events'). The tool reads several hazard event feeds from Redis (earthquakes, wildfires, conflicts — selected by the event_source param, default 'all') via readCachesWithFreshness, and if EVERY selected feed payload came back null the tool cannot compute any exposure, so it throws McpSourceUnavailableError rather than returning a misleading empty result. The error carries unavailable_inputs (genuine misses) and failed_inputs (Redis read failures) so the caller can tell a stampede/pre-seed gap from a Redis outage.

Source

Thrown at api/mcp/registry/analysis-tools.ts:178

    items: { type: 'string' },
    description: 'Required cache keys that were missing or unreadable; their contribution is not treated as quiet.',
  },
  failed_inputs: {
    type: 'array',
    items: { type: 'string' },
    description: 'Subset of unavailable_inputs whose Redis read failed rather than returning a genuine miss.',
  },
} as const;

type AnalysisFreshness = Awaited<ReturnType<typeof readCachesWithFreshness>>['freshness'];

function requireAnyInput(
  payloads: unknown[],
  freshness: AnalysisFreshness,
  message: string,
): void {
  if (payloads.every((value) => value === null)) {
    throw new McpSourceUnavailableError(
      message,
      freshness.unavailable_inputs,
      freshness.failed_inputs,
    );
  }
}

function resolveLimit(raw: unknown, fallback: number): number {
  if (raw === undefined || raw === null) return fallback;
  const parsed = Math.round(Number(raw));
  if (!Number.isFinite(parsed)) return fallback;
  if (parsed <= 0) return Number.POSITIVE_INFINITY;
  return parsed;
}

export const ANALYSIS_TOOLS: ToolDef[] = [
  {
    name: 'get_signal_convergence',

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Wait for the seeder to populate the missing caches — check GET /api/health for the seed-meta:* keys of the affected feeds (seed-meta:seismology:earthquakes, seed-meta:wildfire:fires, seed-meta:conflict:ucdp-events).
  2. Retry the call after a short delay; transient Redis misses and stampedes self-heal once the seed TTL refreshes.
  3. If only one event source is down, broaden event_source to 'all' (or pick a source that IS seeded) so requireAnyInput sees at least one non-null payload.
  4. If persistent, verify Redis connectivity from the edge function and that the seed scripts ran (seed-meta:* keys exist in Redis).

Example fix

// before — narrows to a single unseeded source
tool: 'get_population_exposure', params: { mode: 'events', event_source: 'wildfires' }
// after — lets requireAnyInput see other live feeds
tool: 'get_population_exposure', params: { mode: 'events', event_source: 'all' }
Defensive patterns

Strategy: retry

Validate before calling

// Before calling get_population_exposure, check feed health
const health = await fetch('/api/health').then(r => r.json());
const needed = ['seismology:earthquakes', 'wildfire:fires', 'conflict:ucdp-events'];
const live = needed.filter(k => health.seedMeta?.[k] && Date.now() - health.seedMeta[k].ts < health.seedMeta[k].maxStaleMs);
if (live.length === 0) {
  // All event feeds down — do not call the tool yet
  throw new Error('All exposure event feeds are stale; retry after seeder runs');
}

Type guard

function isMcpSourceUnavailableError(e: unknown): e is { unavailableInputs: string[]; failedInputs: string[] } & Error {
  return e instanceof Error && (e as any).name === 'McpSourceUnavailableError';
}

Try / catch

try {
  const result = await callMcpTool('get_population_exposure', { mode: 'events' });
} catch (e) {
  if (isMcpSourceUnavailableError(e)) {
    // Distinguish retryable (failed_inputs = Redis read errors) from pre-seed (unavailable_inputs only)
    if (e.failedInputs.length > 0) await sleep(5000).then(retry); // Redis issue — retry
    else await sleep(60000).then(retry); // Pre-seed — wait for seeder
  } else throw e;
}

Prevention

When it happens

Trigger: Calling get_population_exposure with mode='events' (the default) when all selected event caches miss in Redis simultaneously — e.g. a fresh deploy before the seeder has written seismology:earthquakes:v1, wildfire:fires:v1, and conflict:ucdp-events:v1; or a Redis outage/flush where readJsonFromUpstash returns null for every key. Narrowing event_source to a single source that is unseeded (e.g. event_source='conflicts' when conflict:ucdp-events:v1 is null) also triggers it because the single selected feed is null.

Common situations: First request after a cold deploy or Redis migration before seeders complete their first cycle; a stampede where concurrent cold reads all see null; narrowing to event_source='wildfires' during a seeder outage for that one domain; regional Redis failover causing transient null reads.

Related errors


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