{"record":{"id":"12b4ff0ff18754e8","repo":"koala73/worldmonitor","slug":"no-event-feeds-are-available-for-exposure-enrichme","errorCode":null,"errorMessage":"No event feeds are available for exposure enrichment","messagePattern":"No event feeds are available for exposure enrichment","errorType":"exception","errorClass":"McpSourceUnavailableError","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/analysis-tools.ts","lineNumber":178,"sourceCode":"    items: { type: 'string' },\n    description: 'Required cache keys that were missing or unreadable; their contribution is not treated as quiet.',\n  },\n  failed_inputs: {\n    type: 'array',\n    items: { type: 'string' },\n    description: 'Subset of unavailable_inputs whose Redis read failed rather than returning a genuine miss.',\n  },\n} as const;\n\ntype AnalysisFreshness = Awaited<ReturnType<typeof readCachesWithFreshness>>['freshness'];\n\nfunction requireAnyInput(\n  payloads: unknown[],\n  freshness: AnalysisFreshness,\n  message: string,\n): void {\n  if (payloads.every((value) => value === null)) {\n    throw new McpSourceUnavailableError(\n      message,\n      freshness.unavailable_inputs,\n      freshness.failed_inputs,\n    );\n  }\n}\n\nfunction resolveLimit(raw: unknown, fallback: number): number {\n  if (raw === undefined || raw === null) return fallback;\n  const parsed = Math.round(Number(raw));\n  if (!Number.isFinite(parsed)) return fallback;\n  if (parsed <= 0) return Number.POSITIVE_INFINITY;\n  return parsed;\n}\n\nexport const ANALYSIS_TOOLS: ToolDef[] = [\n  {\n    name: 'get_signal_convergence',","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/mcp/registry/analysis-tools.ts#L160-L196","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","Retry the call after a short delay; transient Redis misses and stampedes self-heal once the seed TTL refreshes.","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.","If persistent, verify Redis connectivity from the edge function and that the seed scripts ran (seed-meta:* keys exist in Redis)."],"exampleFix":"// before — narrows to a single unseeded source\ntool: 'get_population_exposure', params: { mode: 'events', event_source: 'wildfires' }\n// after — lets requireAnyInput see other live feeds\ntool: 'get_population_exposure', params: { mode: 'events', event_source: 'all' }","handlingStrategy":"retry","validationCode":"// Before calling get_population_exposure, check feed health\nconst health = await fetch('/api/health').then(r => r.json());\nconst needed = ['seismology:earthquakes', 'wildfire:fires', 'conflict:ucdp-events'];\nconst live = needed.filter(k => health.seedMeta?.[k] && Date.now() - health.seedMeta[k].ts < health.seedMeta[k].maxStaleMs);\nif (live.length === 0) {\n  // All event feeds down — do not call the tool yet\n  throw new Error('All exposure event feeds are stale; retry after seeder runs');\n}","typeGuard":"function isMcpSourceUnavailableError(e: unknown): e is { unavailableInputs: string[]; failedInputs: string[] } & Error {\n  return e instanceof Error && (e as any).name === 'McpSourceUnavailableError';\n}","tryCatchPattern":"try {\n  const result = await callMcpTool('get_population_exposure', { mode: 'events' });\n} catch (e) {\n  if (isMcpSourceUnavailableError(e)) {\n    // Distinguish retryable (failed_inputs = Redis read errors) from pre-seed (unavailable_inputs only)\n    if (e.failedInputs.length > 0) await sleep(5000).then(retry); // Redis issue — retry\n    else await sleep(60000).then(retry); // Pre-seed — wait for seeder\n  } else throw e;\n}","preventionTips":["Monitor GET /api/health seed-meta timestamps for the three exposure event feeds before peak usage windows.","Use event_source='all' (the default) so a single down feed does not cause a total failure.","Implement circuit-breaker logic that suppresses calls during known seeder outages rather than hammering the tool."],"tags":["mcp","redis","source-unavailable","cache-miss","population-exposure"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}