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

  1. First call the tool without source_id to fetch the full catalog and see which node ids actually exist
  2. Check /api/health for the submarine-cable dataset key and its seed-meta marker
  3. Retry or wait for the cable catalog seed/producer cycle if health shows it stale/missing
  4. 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

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


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)