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
- 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).
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
- 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.
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
- No digest input feeds are available
- No hotspot-escalation input feeds are available
- The submarine-cable catalog is unavailable
- Feed digest unavailable for ${variant}/en
- cache_all_null
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/12b4ff0ff18754e8.
Report an issue: GitHub.