feat: add a submarine cable tool

Add find_submarine_cables over the bundled TeleGeography data: cables
passing through or landing in an area, or matching a name, with landing
points and the dataset's attribution. The local MCP server's fetch reads
the bundled data files a browser would load by URL, and nothing outside
that directory.
This commit is contained in:
Sameh Khamis committed 2026-10-01 13:28:02 -07:00
1 parent 9be90fff44
commit c1957d884e
9 files changed
+226 -7

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ import {
getAircraftTrack,
} from './queries/aviation.js';
import { getWeatherMap, getWind } from './queries/atmosphere.js';
import { findSubmarineCables } from './queries/cables.js';
import { getHudCaption, situationBrief } from './queries/brief.js';
import {
findMilitaryInstallations,
@@ -93,6 +94,7 @@ export const coreTools = Object.freeze([
getWeatherMap,
getWind,
getRecentImagery,
findSubmarineCables,
getRegionalBrief,
getCyclones,
getFirePerimeters,
+86
View File
@@ -0,0 +1,86 @@
/** Submarine cables and their landing points. */
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun } from '../results.js';
const ATTRIBUTION = '© TeleGeography — submarinecablemap.com (CC BY-NC-SA 3.0)';
function cableLines(feature) {
const geometry = feature?.geometry;
if (geometry?.type === 'LineString') return [geometry.coordinates];
if (geometry?.type === 'MultiLineString') return geometry.coordinates;
return [];
}
export const findSubmarineCables = defineTool({
name: 'find_submarine_cables',
title: 'Submarine cables',
description:
'Undersea telecommunication cables and landing points from the ' +
'TeleGeography map: those passing through or landing in an area, ' +
'and/or those whose name matches.',
inputSchema: {
type: 'object',
properties: {
area: AREA_SCHEMA,
name: { type: 'string', minLength: 2, maxLength: 80 },
limit: LIMIT_SCHEMA,
},
additionalProperties: false,
},
requires: ['cables'],
async run(args, { services, signal }) {
if (!args.area && !args.name)
throw new ToolError('invalid_arguments', 'Give an area, a name or both');
const area = args.area
? await resolveArea(args.area, { services, signal })
: null;
const wanted = args.name?.toLowerCase();
const { cables, landingPoints } = await services.cables.fetch(signal);
const landingRows = (landingPoints?.features || [])
.map((feature) => ({
id: feature.properties?.id ?? null,
name: feature.properties?.name ?? null,
lon: feature.geometry?.coordinates?.[0],
lat: feature.geometry?.coordinates?.[1],
}))
.filter(
(point) =>
point.name &&
Number.isFinite(point.lat) &&
(!area || areaContains(area, point)),
);
const rows = (cables?.features || [])
.filter((feature) => {
const name = String(feature.properties?.name || '');
if (!name || (wanted && !name.toLowerCase().includes(wanted)))
return false;
return (
!area ||
cableLines(feature).some((line) =>
line.some(([lon, lat]) => areaContains(area, { lat, lon })),
)
);
})
.map((feature) => ({
id: feature.properties.id ?? null,
name: feature.properties.name,
}))
.sort((a, b) => a.name.localeCompare(b.name));
const where = area ? ` in ${area.label}` : '';
const what = wanted ? ` matching "${args.name}"` : '';
return {
summary:
`${countNoun(rows.length, 'submarine cable')}${what}${where}` +
(area
? `, with ${countNoun(landingRows.length, 'landing point')}.`
: '.'),
data: {
...capRows(rows, args.limit),
landing_points: area ? capRows(landingRows, args.limit) : null,
attribution: ATTRIBUTION,
},
};
},
});
+95
View File
@@ -0,0 +1,95 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog, coreTools } from '../index.js';
import { createApiFetch } from '../../../server/mcp/services.js';
import { createBundledCableSource } from '../../layers/submarineCables/bundledSource.js';
const cables = {
async fetch() {
return {
cables: {
features: [
{
properties: { id: 'marea', name: 'MAREA' },
geometry: {
type: 'MultiLineString',
coordinates: [
[
[-76, 36.8],
[-40, 40],
[-2.9, 43.4],
],
],
},
},
{
properties: { id: 'unity', name: 'Unity' },
geometry: {
type: 'LineString',
coordinates: [
[-118, 34],
[139, 35],
],
},
},
],
},
landingPoints: {
features: [
{
properties: { id: 'vb', name: 'Virginia Beach, VA, United States' },
geometry: { type: 'Point', coordinates: [-75.98, 36.85] },
},
{
properties: { id: 'bilbao', name: 'Bilbao, Spain' },
geometry: { type: 'Point', coordinates: [-2.93, 43.26] },
},
],
},
};
},
};
test('cables are found by area and name, with landing points and attribution', async () => {
const catalog = composeCatalog({ tools: coreTools, services: { cables } });
const virginia = await catalog.call('find_submarine_cables', {
area: { lat: 36.85, lon: -75.98, radius_km: 50 },
});
assert.equal(
virginia.summary,
'1 submarine cable in 50 km around 36.850, -75.980, with 1 landing point.',
);
assert.deepEqual(virginia.data.rows, [{ id: 'marea', name: 'MAREA' }]);
assert.equal(
virginia.data.landing_points.rows[0].name,
'Virginia Beach, VA, United States',
);
assert.match(virginia.data.attribution, /TeleGeography.*CC BY-NC-SA 3\.0/);
const named = await catalog.call('find_submarine_cables', { name: 'uni' });
assert.equal(named.summary, '1 submarine cable matching "uni".');
assert.equal(named.data.landing_points, null);
await assert.rejects(
catalog.call('find_submarine_cables', {}),
/area, a name or both/,
);
});
test('the local fetch serves bundled data files and nothing outside them', async () => {
const fetchImpl = createApiFetch({
apiBase: 'http://127.0.0.1:1',
fetchImpl: async () => assert.fail('no network'),
});
const { cables: collection, landingPoints } = await createBundledCableSource({
fetchImpl,
}).fetch();
assert.ok(collection.features.length > 500);
assert.ok(landingPoints.features.length > 1000);
const outside = await fetchImpl(
new URL('../../../package.json', import.meta.url).href,
);
assert.equal(outside.status, 404);
const missing = await fetchImpl(
new URL('../../data/local_data/missing.json', import.meta.url).href,
);
assert.equal(missing.status, 404);
});
+2
View File
@@ -16,6 +16,7 @@ import { createInstallationSource } from '../layers/installations/source.js';
import { createLaunchSource } from '../layers/launches/source.js';
import { createWfigsPerimeterSource } from '../layers/perimeters/source.js';
import { createRadioSource } from '../layers/radio/source.js';
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.js';
import { searchHls } from '../layers/recentImagery/catalog.js';
import { rankLatest, wvsSnapshotUrl } from '../layers/recentImagery/model.js';
import { createSatelliteSource } from '../layers/satellites/source.js';
@@ -66,6 +67,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
weatherMaps: createWeatherSource({ fetchImpl }),
wind: createWindSource({ fetchImpl }),
imagery: createImageryService({ fetchImpl }),
cables: createBundledCableSource({ fetchImpl }),
regional: requests.regional,
terrain: requests.terrain,
summary: requests.summary,