{"record":{"id":"ef548453824486d2","repo":"koala73/worldmonitor","slug":"the-submarine-cable-catalog-is-unavailable","errorCode":null,"errorMessage":"The submarine-cable catalog is unavailable","messagePattern":"The submarine-cable catalog is unavailable","errorType":"exception","errorClass":"McpSourceUnavailableError","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/analysis-tools.ts","lineNumber":518,"sourceCode":"      },\n      required: ['cached_at', 'stale', 'data'],\n    },\n    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n    _execute: async (params) => {\n      const checks: FreshnessCheck[] = [\n        { key: 'seed-meta:infrastructure:submarine-cables', maxStaleMin: 25200 },\n      ];\n      const { payloads: [cablesPayload], freshness } = await readCachesWithFreshness(\n        ['infrastructure:submarine-cables:v1'],\n        checks,\n      );\n      const cables = submarineCablesToCableInputs(cablesPayload);\n      const graph = buildDependencyGraph({ cables, waterways: MCP_CASCADE_WATERWAYS });\n      const stats = getGraphStats(graph);\n\n      const sourceId = typeof params.source_id === 'string' ? params.source_id.trim() : '';\n      if (!sourceId) {\n        const catalog: Record<string, Array<{ id: string; name: string }>> = {};\n        for (const node of graph.nodes.values()) {\n          if (node.type === 'country') continue;\n          (catalog[node.type] ??= []).push({ id: node.id, name: node.name });\n        }\n        return { ...freshness, data: { catalog, cascade: null, stats } };\n      }\n\n      if (!graph.nodes.has(sourceId)) {\n        if (cablesPayload === null && sourceId.startsWith('cable:')) {\n          throw new McpSourceUnavailableError(\n            'The submarine-cable catalog is unavailable',\n            freshness.unavailable_inputs,\n            freshness.failed_inputs,\n          );\n        }\n        const sample = [...graph.nodes.keys()].filter((id) => !id.startsWith('country-')).slice(0, 12);\n        return {\n          ...freshness,","sourceCodeStart":500,"sourceCodeEnd":536,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/api/mcp/registry/analysis-tools.ts#L500-L536","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst res = await client.callTool('get_cable_graph', { source_id: 'cable:matterhorn' });\n// throws: The submarine-cable catalog is unavailable\n\n// after — enumerate first, then drill in\nconst cat = await client.callTool('get_cable_graph', {});\nconst cableId = Object.values(cat.data.catalog.cable ?? []).find(c => /matterhorn/i.test(c.name))?.id;\nif (cableId) {\n  const res = await client.callTool('get_cable_graph', { source_id: cableId });\n}","handlingStrategy":"fallback","validationCode":"// Discover valid ids before drilling in: the no-arg call returns the full catalog\nconst cat = await client.callTool(cableGraphTool, {});\nconst knownIds = new Set(Object.values(cat.data.catalog ?? {}).flat().map(n => n.id));\nif (!knownIds.has(sourceId)) sourceId = matchByName(cat.data.catalog, sourceId);","typeGuard":"function isSourceUnavailable(e) {\n  return e?.name === 'McpSourceUnavailableError' || Array.isArray(e?.data?.unavailable_inputs);\n}","tryCatchPattern":"try {\n  return await client.callTool(cableGraphTool, { source_id: id });\n} catch (e) {\n  if (isSourceUnavailable(e) && id.startsWith('cable:')) {\n    const cat = await client.callTool(cableGraphTool, {}); // catalog without drill-down\n    return degradeToCatalog(cat); // still useful: full node catalog + stats\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["mcp","analysis","redis","submarine-cable","data-source"],"backgroundTag":"data-source-unavailable","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}