koala73/worldmonitor · error · McpSourceUnavailableError
The submarine-cable catalog is unavailable
Error message
The submarine-cable catalog is unavailable
What it means
McpSourceUnavailableError thrown in the cable-graph analysis tool (api/mcp/registry/analysis-tools.ts:503): the caller passed a source_id starting with 'cable:' that is not present in the built graph, AND the submarine-cable dataset payload itself read null from cache. That combination means the cable cannot be distinguished from an unknown id because the entire cable catalog is missing, so the tool fails with unavailable_inputs/failed_inputs rather than silently returning a cable-less graph.
Solutions
- First call the tool without source_id to fetch the full catalog and see which node ids actually exist
- Check /api/health for the submarine-cable dataset key and its seed-meta marker
- Retry or wait for the cable catalog seed/producer cycle if health shows it stale/missing
- Verify the source_id spelling against the catalog ids returned in the no-arg call
Example fix
// before
const res = await client.callTool('get_cable_graph', { source_id: 'cable:matterhorn' });
// throws: The submarine-cable catalog is unavailable
// after — enumerate first, then drill in
const cat = await client.callTool('get_cable_graph', {});
const cableId = Object.values(cat.data.catalog.cable ?? []).find(c => /matterhorn/i.test(c.name))?.id;
if (cableId) {
const res = await client.callTool('get_cable_graph', { source_id: cableId });
} Defensive patterns
Strategy: fallback
Validate before calling
// Discover valid ids before drilling in: the no-arg call returns the full catalog
const cat = await client.callTool(cableGraphTool, {});
const knownIds = new Set(Object.values(cat.data.catalog ?? {}).flat().map(n => n.id));
if (!knownIds.has(sourceId)) sourceId = matchByName(cat.data.catalog, sourceId); Type guard
function isSourceUnavailable(e) {
return e?.name === 'McpSourceUnavailableError' || Array.isArray(e?.data?.unavailable_inputs);
} Try / catch
try {
return await client.callTool(cableGraphTool, { source_id: id });
} catch (e) {
if (isSourceUnavailable(e) && id.startsWith('cable:')) {
const cat = await client.callTool(cableGraphTool, {}); // catalog without drill-down
return degradeToCatalog(cat); // still useful: full node catalog + stats
}
throw e;
} Prevention
- Always enumerate the catalog (no source_id) once and cache id lists instead of guessing cable ids
- Reserve 'cable:'-prefixed ids for values returned by the catalog, never hand-typed ones
- Check the submarine-cable dataset on /api/health before scheduled drill-down queries
When it happens
Trigger: Calling the cable/chokepoint graph tool with params.source_id = 'cable:<something>' while the submarine-cable catalog cache key is null (never seeded, expired, evicted, or its Redis read failed). Note: a non-'cable:' unknown id does NOT throw — it returns a structured error field with a known_id_sample instead.
Common situations: Environment where the submarine-cable seed script has not run. Cable catalog TTL expired between rare producer runs. Upstash read failure on that single key. Caller assuming cable ids from documentation while the deployed dataset uses different ids.
Related errors
- No convergence input feeds are available
- No digest input feeds are available
- No event feeds are available for exposure enrichment
- No focal-point input feeds are available
- No hotspot-escalation input feeds are available
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21).
Data as JSON: /api/errors/ef548453824486d2.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp/registry/analysis-tools.ts:518
},
required: ['cached_at', 'stale', 'data'],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
_execute: async (params) => {
const checks: FreshnessCheck[] = [
{ key: 'seed-meta:infrastructure:submarine-cables', maxStaleMin: 25200 },
];
const { payloads: [cablesPayload], freshness } = await readCachesWithFreshness(
['infrastructure:submarine-cables:v1'],
checks,
);
const cables = submarineCablesToCableInputs(cablesPayload);
const graph = buildDependencyGraph({ cables, waterways: MCP_CASCADE_WATERWAYS });
const stats = getGraphStats(graph);
const sourceId = typeof params.source_id === 'string' ? params.source_id.trim() : '';
if (!sourceId) {
const catalog: Record<string, Array<{ id: string; name: string }>> = {};
for (const node of graph.nodes.values()) {
if (node.type === 'country') continue;
(catalog[node.type] ??= []).push({ id: node.id, name: node.name });
}
return { ...freshness, data: { catalog, cascade: null, stats } };
}
if (!graph.nodes.has(sourceId)) {
if (cablesPayload === null && sourceId.startsWith('cable:')) {
throw new McpSourceUnavailableError(
'The submarine-cable catalog is unavailable',
freshness.unavailable_inputs,
freshness.failed_inputs,
);
}
const sample = [...graph.nodes.keys()].filter((id) => !id.startsWith('country-')).slice(0, 12);
return {
...freshness,View on GitHub (pinned to 7d06c8633d)