feat: add a datacenter and dam tool

This commit is contained in:
Sameh Khamis committed 2026-10-01 13:38:38 -07:00
1 parent a8e5ff9390
commit 28cfd39d1a
9 files changed
+285 -4

No files matched your search

+2
View File
@@ -23,6 +23,7 @@ import {
} from './queries/environment.js';
import { getActiveFires, getEarthquakes } from './queries/hazards.js';
import { getRecentImagery } from './queries/imagery.js';
import { findInfrastructure } from './queries/infrastructure.js';
import {
findVessel,
getVesselTrack,
@@ -97,6 +98,7 @@ export const coreTools = Object.freeze([
getWind,
getRecentImagery,
findSubmarineCables,
findInfrastructure,
getRegionalBrief,
getCyclones,
getFirePerimeters,
+70
View File
@@ -0,0 +1,70 @@
/** Bundled OpenStreetMap infrastructure: datacenters and dams. */
import { defineTool } from '../catalog.js';
import {
AREA_SCHEMA,
areaCenter,
areaContains,
distanceKm,
resolveArea,
} from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun } from '../results.js';
const KINDS = {
datacenters: { layerId: 'local-datacenters', noun: 'datacenter' },
dams: { layerId: 'local-dams', noun: 'dam' },
};
const ATTRIBUTION = {
datacenters: '© OpenStreetMap contributors (ODbL 1.0)',
dams: '© OpenStreetMap contributors (ODbL 1.0), via Open Infrastructure Map',
};
const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
export const findInfrastructure = defineTool({
name: 'find_infrastructure',
title: 'Datacenters and dams',
description:
'Datacenters (with operator and capacity) or dams (with river and power ' +
'output) mapped in OpenStreetMap within an area, nearest the center first.',
inputSchema: {
type: 'object',
properties: {
kind: { type: 'string', enum: Object.keys(KINDS) },
area: AREA_SCHEMA,
limit: LIMIT_SCHEMA,
},
required: ['kind', 'area'],
additionalProperties: false,
},
requires: ['infrastructure'],
async run(args, { services, signal }) {
const area = await resolveArea(args.area, { services, signal });
const center = areaCenter(area);
const { layerId, noun } = KINDS[args.kind];
const records = await services.infrastructure.getRecords(layerId, {
signal,
});
const rows = records
.filter((record) => areaContains(area, record))
.map((record) => ({
name: record.name,
operator: record.operator,
...(args.kind === 'datacenters'
? { capacity: record.capacity }
: { river: record.river, output: record.output }),
lat: round(record.lat, 5),
lon: round(record.lon, 5),
distance_km: round(distanceKm(center, record), 1),
}))
.sort((a, b) => a.distance_km - b.distance_km);
return {
summary: `${countNoun(rows.length, `mapped ${noun}`)} in ${area.label}.`,
data: {
...capRows(rows, args.limit),
attribution: ATTRIBUTION[args.kind],
},
};
},
});
+143
View File
@@ -0,0 +1,143 @@
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 {
createInfrastructureSource,
featurePoint,
} from '../../sources/infrastructureData.js';
test('features are placed at their point or average outline vertex', () => {
assert.deepEqual(
featurePoint({ geometry: { type: 'Point', coordinates: [10, 20] } }),
{ lon: 10, lat: 20 },
);
assert.deepEqual(
featurePoint({
geometry: {
type: 'Polygon',
coordinates: [
[
[0, 0],
[2, 0],
[2, 2],
[0, 2],
],
],
},
}),
{ lon: 1, lat: 1 },
);
assert.deepEqual(
featurePoint({
geometry: {
type: 'MultiPolygon',
coordinates: [
[
[
[4, 4],
[6, 6],
],
],
],
},
}),
{ lon: 5, lat: 5 },
);
assert.equal(featurePoint({ geometry: null }), null);
});
test('datacenters and dams are listed nearest first with their own fields', async () => {
const records = {
'local-datacenters': [
{
id: 'a',
name: 'Equinix DC2',
lat: 38.9,
lon: -77.45,
operator: 'Equinix',
capacity: '20 MW',
river: null,
output: null,
},
{
id: 'b',
name: 'Far DC',
lat: 48.8,
lon: 2.35,
operator: null,
capacity: null,
river: null,
output: null,
},
],
'local-dams': [
{
id: 'c',
name: 'Hoover Dam',
lat: 36.016,
lon: -114.737,
operator: 'USBR',
capacity: null,
river: 'Colorado River',
output: '2080 MW',
},
],
};
const infrastructure = { getRecords: async (layerId) => records[layerId] };
const catalog = composeCatalog({
tools: coreTools,
services: { infrastructure },
});
const dc = await catalog.call('find_infrastructure', {
kind: 'datacenters',
area: { lat: 38.95, lon: -77.45, radius_km: 20 },
});
assert.equal(
dc.summary,
'1 mapped datacenter in 20 km around 38.950, -77.450.',
);
assert.deepEqual(dc.data.rows[0], {
name: 'Equinix DC2',
operator: 'Equinix',
capacity: '20 MW',
lat: 38.9,
lon: -77.45,
distance_km: 5.6,
});
assert.match(dc.data.attribution, /OpenStreetMap contributors \(ODbL 1\.0\)/);
const dams = await catalog.call('find_infrastructure', {
kind: 'dams',
area: { lat: 36, lon: -114.7, radius_km: 20 },
});
assert.deepEqual(dams.data.rows[0], {
name: 'Hoover Dam',
operator: 'USBR',
river: 'Colorado River',
output: '2080 MW',
lat: 36.016,
lon: -114.737,
distance_km: 3.8,
});
});
test('the bundled datasets load through the local fetch', async () => {
const fetchImpl = createApiFetch({
apiBase: 'http://127.0.0.1:1',
fetchImpl: async () => assert.fail('no network'),
});
const source = createInfrastructureSource({ fetchImpl });
const dams = await source.getRecords('local-dams');
const datacenters = await source.getRecords('local-datacenters');
assert.ok(dams.length > 500 && datacenters.length > 3000);
assert.ok(
dams.every(
(record) => Number.isFinite(record.lat) && Number.isFinite(record.lon),
),
);
assert.equal(await source.getRecords('local-dams'), dams);
await assert.rejects(
source.getRecords('local-other'),
/Unknown infrastructure layer/,
);
});
+2
View File
@@ -26,6 +26,7 @@ import { createTransitSource } from '../layers/transit/source.js';
import { createWeatherSource } from '../layers/weather/source.js';
import { createWindSource } from '../layers/wind/source.js';
import { createApplicationRequestServices } from '../services/requests.js';
import { createInfrastructureSource } from '../sources/infrastructureData.js';
import {
createAdsbLolSource,
createAisStreamSource,
@@ -70,6 +71,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
imagery: createImageryService({ fetchImpl }),
cables: createBundledCableSource({ fetchImpl }),
alpr: createAlprTileSource({ tileFetchImpl: fetchImpl }),
infrastructure: createInfrastructureSource({ fetchImpl }),
regional: requests.regional,
terrain: requests.terrain,
summary: requests.summary,