geotap-mcp-server 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/consolidatedTools.js +480 -0
- package/src/index.js +266 -103
- package/src/paramNormalize.js +123 -0
- package/src/responseCap.js +16 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geotap-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MCP server for GeoTap — access 37 US federal environmental and infrastructure data sources from Claude, Cursor, and other AI tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Consolidated Tool Definitions — 12 smart tools replacing 85 legacy tools
|
|
5
|
+
*
|
|
6
|
+
* Each tool uses an `action` parameter to route to the correct backend endpoint.
|
|
7
|
+
* This dramatically improves LLM tool selection accuracy (research shows accuracy
|
|
8
|
+
* drops from 95% at 5 tools to <14% at 40+ tools).
|
|
9
|
+
*
|
|
10
|
+
* Legacy tools remain available via GEOTAP_LEGACY_TOOLS=true env var.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const consolidatedTools = [
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
15
|
+
// 1. QUERY LOCATION — The starting point for most queries
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
{
|
|
18
|
+
name: 'query_location',
|
|
19
|
+
description: `Query environmental data for any US location. This is the primary tool — start here for most questions.
|
|
20
|
+
|
|
21
|
+
Actions:
|
|
22
|
+
- "address" — Geocode a US address AND query environmental data in one call. Always start here when user gives an address. Returns flood zones, wetlands, soils, habitat, contamination, with plain-English interpretations. Response <5KB.
|
|
23
|
+
- "point" — Same as address but when you already have lat/lng coordinates. Response <5KB.
|
|
24
|
+
- "nearby" — Find environmental features within a radius of a point. Set geometry="none" and specify layers to keep response small.
|
|
25
|
+
- "bbox" — Query features in a bounding box. Set geometry="none" and specify layers.
|
|
26
|
+
- "polygon" — Query features in a polygon area. Set geometry="none" and specify layers.
|
|
27
|
+
- "summary" — Quick feature counts for an area (no geometry, just numbers). Fastest option for "how many?" questions.
|
|
28
|
+
- "geocode" — Convert address/place name to coordinates. Use only when you need coordinates for other tools.
|
|
29
|
+
- "list_layers" — List all 37 available data layers.
|
|
30
|
+
- "layer_details" — Get metadata about a specific layer.
|
|
31
|
+
- "layer_features" — Get features from one specific layer in a bbox.`,
|
|
32
|
+
parameters: {
|
|
33
|
+
action: z.enum(['address', 'point', 'nearby', 'bbox', 'polygon', 'summary', 'geocode', 'list_layers', 'layer_details', 'layer_features'])
|
|
34
|
+
.describe('Which query type to perform'),
|
|
35
|
+
// Address actions
|
|
36
|
+
address: z.string().optional().describe('US street address (for action: address, geocode)'),
|
|
37
|
+
// Coordinate actions
|
|
38
|
+
lat: z.number().optional().describe('Latitude WGS84 (for action: point, nearby)'),
|
|
39
|
+
lng: z.number().optional().describe('Longitude WGS84 (for action: point, nearby)'),
|
|
40
|
+
// Area actions
|
|
41
|
+
bbox: z.string().optional().describe('Bounding box "west,south,east,north" (for action: bbox, layer_features)'),
|
|
42
|
+
polygon: z.object({
|
|
43
|
+
type: z.literal('Polygon'),
|
|
44
|
+
coordinates: z.array(z.array(z.array(z.number())))
|
|
45
|
+
}).optional().describe('GeoJSON Polygon (for action: polygon, summary)'),
|
|
46
|
+
// Filtering
|
|
47
|
+
layers: z.string().optional().describe('Comma-separated layer names (e.g., "flood_zones,wetlands")'),
|
|
48
|
+
geometry: z.enum(['none', 'simplified', 'full']).optional().describe('Geometry detail level. Default: none for MCP. Use "none" unless user specifically needs coordinates.'),
|
|
49
|
+
radius: z.number().optional().describe('Search radius in km for action: nearby (default: 1)'),
|
|
50
|
+
layerName: z.string().optional().describe('Layer identifier for action: layer_details, layer_features'),
|
|
51
|
+
},
|
|
52
|
+
_actionMap: {
|
|
53
|
+
address: 'query_address',
|
|
54
|
+
point: 'identify_features_at_point',
|
|
55
|
+
nearby: 'get_environmental_data_near_point',
|
|
56
|
+
bbox: 'get_environmental_data_in_bbox',
|
|
57
|
+
polygon: 'get_environmental_data_for_area',
|
|
58
|
+
summary: 'get_environmental_summary',
|
|
59
|
+
geocode: 'geocode_address',
|
|
60
|
+
list_layers: 'list_data_layers',
|
|
61
|
+
layer_details: 'get_layer_details',
|
|
62
|
+
layer_features: 'get_layer_features',
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
67
|
+
// 2. RAINFALL — NOAA Atlas 14, design storms, climate projections
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
69
|
+
{
|
|
70
|
+
name: 'get_rainfall',
|
|
71
|
+
description: `Get rainfall and precipitation data from NOAA Atlas 14 for stormwater engineering, flood analysis, and hydrologic design.
|
|
72
|
+
|
|
73
|
+
Actions:
|
|
74
|
+
- "atlas14" — Precipitation frequency estimates (depths & intensities for all durations/return periods). The standard rainfall data for US engineering.
|
|
75
|
+
- "idf" — Intensity-Duration-Frequency curve data for charting.
|
|
76
|
+
- "hyetograph" — Generate a design storm hyetograph (rainfall over time) for hydrologic modeling.
|
|
77
|
+
- "export_hyetograph" — Export hyetograph as CSV/JSON for HEC-HMS, SWMM, etc.
|
|
78
|
+
- "distributions" — List available rainfall distribution types (SCS Type I/IA/II/III, Huff, etc.).
|
|
79
|
+
- "recommend_distribution" — Determine which distribution type applies at a location.
|
|
80
|
+
- "climate_scenarios" — List available SSP scenarios and time horizons.
|
|
81
|
+
- "climate_factors" — Get climate change adjustment multipliers for a location.
|
|
82
|
+
- "climate_projection" — Apply climate change projections to Atlas 14 data.
|
|
83
|
+
- "uncertainty" — Get confidence interval bounds for a specific return period/duration.
|
|
84
|
+
- "uncertainty_envelope" — Monte Carlo uncertainty envelope for risk-based design.
|
|
85
|
+
- "sensitivity" — Sensitivity analysis on storm parameters.
|
|
86
|
+
- "design_approaches" — List design approaches for handling uncertainty.
|
|
87
|
+
- "status" — Check NOAA Atlas 14 service availability.`,
|
|
88
|
+
parameters: {
|
|
89
|
+
action: z.enum(['atlas14', 'idf', 'hyetograph', 'export_hyetograph', 'distributions', 'recommend_distribution', 'climate_scenarios', 'climate_factors', 'climate_projection', 'uncertainty', 'uncertainty_envelope', 'sensitivity', 'design_approaches', 'status'])
|
|
90
|
+
.describe('Which rainfall query to perform'),
|
|
91
|
+
lat: z.number().optional().describe('Latitude WGS84'),
|
|
92
|
+
lng: z.number().optional().describe('Longitude WGS84'),
|
|
93
|
+
units: z.enum(['english', 'metric']).optional().describe('Unit system (default: english)'),
|
|
94
|
+
series: z.enum(['pds', 'ams']).optional().describe('Statistical series (default: pds)'),
|
|
95
|
+
returnPeriod: z.string().optional().describe('Return period with "yr" suffix (e.g., "100yr")'),
|
|
96
|
+
returnPeriods: z.string().optional().describe('Comma-separated return periods (e.g., "2,5,10,25,50,100") for IDF'),
|
|
97
|
+
duration: z.number().optional().describe('Storm duration in hours'),
|
|
98
|
+
timeInterval: z.number().optional().describe('Time step in minutes'),
|
|
99
|
+
distribution: z.string().optional().describe('Rainfall distribution type (e.g., "SCS Type II")'),
|
|
100
|
+
horizon: z.string().optional().describe('Climate time horizon: "current", "mid-century", "late-century"'),
|
|
101
|
+
scenario: z.string().optional().describe('Climate scenario: "SSP2-4.5" or "SSP5-8.5"'),
|
|
102
|
+
format: z.enum(['csv', 'json']).optional().describe('Export format for export_hyetograph'),
|
|
103
|
+
nSamples: z.number().optional().describe('Monte Carlo samples for uncertainty_envelope (default: 500)'),
|
|
104
|
+
},
|
|
105
|
+
_actionMap: {
|
|
106
|
+
atlas14: 'get_rainfall_data',
|
|
107
|
+
idf: 'get_idf_curves',
|
|
108
|
+
hyetograph: 'generate_hyetograph',
|
|
109
|
+
export_hyetograph: 'export_hyetograph',
|
|
110
|
+
distributions: 'list_rainfall_distributions',
|
|
111
|
+
recommend_distribution: 'get_rainfall_distribution',
|
|
112
|
+
climate_scenarios: 'get_climate_scenarios',
|
|
113
|
+
climate_factors: 'get_climate_change_factors',
|
|
114
|
+
climate_projection: 'get_climate_change_rainfall_projection',
|
|
115
|
+
uncertainty: 'get_rainfall_uncertainty_bounds',
|
|
116
|
+
uncertainty_envelope: 'generate_uncertainty_envelope',
|
|
117
|
+
sensitivity: 'run_rainfall_sensitivity_analysis',
|
|
118
|
+
design_approaches: 'get_design_approaches',
|
|
119
|
+
status: 'check_rainfall_service_status',
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
124
|
+
// 3. WATERSHED — Delineation, flow statistics, HUC boundaries
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
126
|
+
{
|
|
127
|
+
name: 'get_watershed',
|
|
128
|
+
description: `Watershed delineation, flow statistics, stream networks, water quality, HUC boundaries, and FEMA flood map panels.
|
|
129
|
+
|
|
130
|
+
Actions:
|
|
131
|
+
- "delineate" — Trace watershed boundary for a pour point using USGS StreamStats. Returns drainage area polygon + basin characteristics.
|
|
132
|
+
- "characteristics" — Get physical/hydrologic basin characteristics (area, slope, precip, impervious %).
|
|
133
|
+
- "flow_statistics" — Estimated peak flows (2yr-500yr) and low flows from USGS regional regression.
|
|
134
|
+
- "flowlines" — Stream network (rivers, creeks) in a bounding box from NHD.
|
|
135
|
+
- "water_quality" — Water quality impairments (303d listed) for a watershed extent.
|
|
136
|
+
- "huc_boundaries" — HUC-8/10/12 watershed boundaries for a bounding box.
|
|
137
|
+
- "huc_by_code" — Get a specific HUC watershed boundary by its code.
|
|
138
|
+
- "firm_panels" — FEMA FIRM panel numbers for an area.`,
|
|
139
|
+
parameters: {
|
|
140
|
+
action: z.enum(['delineate', 'characteristics', 'flow_statistics', 'flowlines', 'water_quality', 'huc_boundaries', 'huc_by_code', 'firm_panels'])
|
|
141
|
+
.describe('Which watershed query to perform'),
|
|
142
|
+
lat: z.number().optional().describe('Latitude WGS84 (for delineate, characteristics, flow_statistics)'),
|
|
143
|
+
lng: z.number().optional().describe('Longitude WGS84'),
|
|
144
|
+
bbox: z.string().optional().describe('Bounding box "west,south,east,north" (for flowlines, water_quality, huc_boundaries, firm_panels)'),
|
|
145
|
+
region: z.string().optional().describe('StreamStats region code (auto-detected if omitted)'),
|
|
146
|
+
drainageArea: z.number().optional().describe('Known drainage area in sq mi (improves flow_statistics accuracy)'),
|
|
147
|
+
huc12: z.string().optional().describe('HUC-12 code (for water_quality if known)'),
|
|
148
|
+
hucCode: z.string().optional().describe('HUC code for huc_by_code'),
|
|
149
|
+
hucLevel: z.enum(['8', '10', '12']).optional().describe('HUC level for huc_boundaries (default: 12)'),
|
|
150
|
+
},
|
|
151
|
+
_actionMap: {
|
|
152
|
+
delineate: 'delineate_watershed',
|
|
153
|
+
characteristics: 'get_watershed_characteristics',
|
|
154
|
+
flow_statistics: 'get_flow_statistics',
|
|
155
|
+
flowlines: 'get_flowlines',
|
|
156
|
+
water_quality: 'get_watershed_water_quality',
|
|
157
|
+
huc_boundaries: 'get_huc_watersheds',
|
|
158
|
+
huc_by_code: 'get_huc_watershed_by_code',
|
|
159
|
+
firm_panels: 'get_firm_panels',
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
164
|
+
// 4. HYDROLOGY — Curve numbers, runoff, engineering calculations
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
166
|
+
{
|
|
167
|
+
name: 'get_hydrology',
|
|
168
|
+
description: `Hydrologic engineering calculations: curve numbers, runoff, time of concentration, peak discharge.
|
|
169
|
+
|
|
170
|
+
Actions:
|
|
171
|
+
- "analyze" — Comprehensive hydrologic analysis on catchments: composite CN, Tc, SCS runoff depth, peak discharge (Rational, TR-55, regression). The all-in-one hydrology tool.
|
|
172
|
+
- "distributions" — List rainfall distribution types for hydrologic analysis.
|
|
173
|
+
- "distribution_for_location" — Recommended SCS distribution for a specific location.
|
|
174
|
+
- "lookup_cn" — Look up SCS curve number for a specific land use (NLCD code) + soil type (HSG).
|
|
175
|
+
- "cn_tables" — Full SCS curve number reference tables.
|
|
176
|
+
- "analyze_cn" — Calculate weighted curve numbers for catchments using NLCD + SSURGO data.`,
|
|
177
|
+
parameters: {
|
|
178
|
+
action: z.enum(['analyze', 'distributions', 'distribution_for_location', 'lookup_cn', 'cn_tables', 'analyze_cn'])
|
|
179
|
+
.describe('Which hydrology calculation to perform'),
|
|
180
|
+
lat: z.number().optional().describe('Latitude WGS84 (for distribution_for_location)'),
|
|
181
|
+
lng: z.number().optional().describe('Longitude WGS84'),
|
|
182
|
+
catchments: z.any().optional().describe('GeoJSON FeatureCollection of catchment polygons (for analyze, analyze_cn)'),
|
|
183
|
+
options: z.any().optional().describe('Analysis options: {hydrologicCondition, dualHSGTreatment, returnPeriods, durations}'),
|
|
184
|
+
nlcd: z.number().optional().describe('NLCD land cover code for lookup_cn (e.g., 21=Developed Open Space)'),
|
|
185
|
+
hsg: z.string().optional().describe('Hydrologic Soil Group A/B/C/D for lookup_cn'),
|
|
186
|
+
condition: z.string().optional().describe('Antecedent moisture: "good", "fair", "poor" for lookup_cn'),
|
|
187
|
+
},
|
|
188
|
+
_actionMap: {
|
|
189
|
+
analyze: 'analyze_hydrology',
|
|
190
|
+
distributions: 'get_hydrology_distributions',
|
|
191
|
+
distribution_for_location: 'get_hydrology_distribution_for_location',
|
|
192
|
+
lookup_cn: 'lookup_curve_number',
|
|
193
|
+
cn_tables: 'get_curve_number_tables',
|
|
194
|
+
analyze_cn: 'analyze_curve_numbers',
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
199
|
+
// 5. WATER QUALITY — EPA ATTAINS, impairments, receiving waters
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
201
|
+
{
|
|
202
|
+
name: 'get_water_quality',
|
|
203
|
+
description: `Water quality impairment data from EPA ATTAINS — 303(d) listed impaired waters, pollutants, designated uses.
|
|
204
|
+
|
|
205
|
+
Actions:
|
|
206
|
+
- "assessment" — Full water quality assessment for a location: impaired waters, pollutants, downstream receiving water trace.
|
|
207
|
+
- "impairments" — Quick impairment check by HUC-12 code. Faster when you know the HUC.
|
|
208
|
+
- "find_watershed" — Identify which HUC-12 watershed a point falls within.`,
|
|
209
|
+
parameters: {
|
|
210
|
+
action: z.enum(['assessment', 'impairments', 'find_watershed'])
|
|
211
|
+
.describe('Which water quality query to perform'),
|
|
212
|
+
lat: z.number().optional().describe('Latitude WGS84 (for find_watershed)'),
|
|
213
|
+
lng: z.number().optional().describe('Longitude WGS84'),
|
|
214
|
+
location: z.any().optional().describe('GeoJSON Point or Polygon (for assessment)'),
|
|
215
|
+
huc12: z.string().optional().describe('12-digit HUC code (for impairments)'),
|
|
216
|
+
options: z.object({
|
|
217
|
+
includeDownstream: z.boolean().optional(),
|
|
218
|
+
radiusKm: z.number().optional(),
|
|
219
|
+
}).optional().describe('Assessment options'),
|
|
220
|
+
},
|
|
221
|
+
_actionMap: {
|
|
222
|
+
assessment: 'get_water_quality',
|
|
223
|
+
impairments: 'get_water_impairments',
|
|
224
|
+
find_watershed: 'get_watershed_for_point',
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
229
|
+
// 6. ELEVATION — USGS 3DEP, contours, DEM, land use, imagery
|
|
230
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
231
|
+
{
|
|
232
|
+
name: 'get_elevation',
|
|
233
|
+
description: `Elevation, terrain, land use, and satellite imagery from USGS 3DEP, NLCD, and NAIP.
|
|
234
|
+
|
|
235
|
+
Actions:
|
|
236
|
+
- "stats" — Elevation statistics (min, max, mean, range) for a bounding box.
|
|
237
|
+
- "contours" — Generate contour lines at specified intervals for a bbox.
|
|
238
|
+
- "contour_options" — Available contour interval options.
|
|
239
|
+
- "export_dem" — Export DEM as GeoTIFF (1m/10m/30m resolution).
|
|
240
|
+
- "export_contours" — Export contour lines as GeoJSON for a polygon.
|
|
241
|
+
- "availability" — Check which DEM resolutions are available for an area.
|
|
242
|
+
- "resolution_options" — List supported DEM resolution options.
|
|
243
|
+
- "export_land_use" — Export NLCD land cover data as GeoTIFF or polygons.
|
|
244
|
+
- "export_satellite" — Export aerial photography as GeoTIFF.
|
|
245
|
+
- "satellite_options" — Available satellite imagery resolutions.`,
|
|
246
|
+
parameters: {
|
|
247
|
+
action: z.enum(['stats', 'contours', 'contour_options', 'export_dem', 'export_contours', 'availability', 'resolution_options', 'export_land_use', 'export_satellite', 'satellite_options'])
|
|
248
|
+
.describe('Which elevation/terrain query to perform'),
|
|
249
|
+
bbox: z.string().optional().describe('Bounding box "west,south,east,north" (for stats, contours, availability)'),
|
|
250
|
+
polygon: z.any().optional().describe('GeoJSON Polygon (for export_dem, export_contours, export_land_use, export_satellite)'),
|
|
251
|
+
interval: z.number().optional().describe('Contour interval in feet'),
|
|
252
|
+
intervalMeters: z.number().optional().describe('Contour interval in meters'),
|
|
253
|
+
resolution: z.string().optional().describe('DEM resolution "1m"/"10m"/"30m" or satellite "high"/"medium"/"low"'),
|
|
254
|
+
targetCrs: z.string().optional().describe('Target CRS (e.g., "EPSG:2277")'),
|
|
255
|
+
convertToFeet: z.boolean().optional().describe('Convert elevations to feet'),
|
|
256
|
+
clipToPolygon: z.boolean().optional().describe('Clip raster to polygon boundary'),
|
|
257
|
+
format: z.enum(['geotiff', 'polygons']).optional().describe('Land use export format'),
|
|
258
|
+
demResolution: z.enum(['1m', '10m', '30m']).optional().describe('DEM resolution for contour export'),
|
|
259
|
+
},
|
|
260
|
+
_actionMap: {
|
|
261
|
+
stats: 'get_elevation_stats',
|
|
262
|
+
contours: 'get_contour_lines',
|
|
263
|
+
contour_options: 'get_contour_interval_options',
|
|
264
|
+
export_dem: 'export_dem',
|
|
265
|
+
export_contours: 'export_contours',
|
|
266
|
+
availability: 'check_dem_availability',
|
|
267
|
+
resolution_options: 'get_dem_resolution_options',
|
|
268
|
+
export_land_use: 'export_land_use',
|
|
269
|
+
export_satellite: 'export_satellite_imagery',
|
|
270
|
+
satellite_options: 'get_satellite_resolution_options',
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
275
|
+
// 7. GAGE ANALYSIS — Stream gage data, flood frequency, storm events
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
277
|
+
{
|
|
278
|
+
name: 'analyze_gage',
|
|
279
|
+
description: `Analyze USGS stream gage data: flood frequency, flow duration, low flow, storm events, and published statistics.
|
|
280
|
+
|
|
281
|
+
Actions:
|
|
282
|
+
- "summary" — Quick overview of a gage: period of record, drainage area, key flows.
|
|
283
|
+
- "flood_frequency" — Bulletin 17C flood frequency analysis (2yr-500yr peak flows).
|
|
284
|
+
- "flow_duration" — Flow duration curve and percentiles (Q1 through Q99).
|
|
285
|
+
- "low_flow" — Low flow statistics: 7Q10, 7Q2, harmonic mean. Critical for NPDES permits.
|
|
286
|
+
- "storm_events" — Detect storm events from flow record with peak, volume, duration.
|
|
287
|
+
- "storm_detail" — Detailed hydrograph for a specific storm event.
|
|
288
|
+
- "export_storm" — Export storm hydrograph for HEC-HMS or other models.
|
|
289
|
+
- "published_stats" — Official USGS GageStats published values (peer-reviewed).
|
|
290
|
+
- "compare_stats" — Compare computed vs. published statistics for QA.`,
|
|
291
|
+
parameters: {
|
|
292
|
+
action: z.enum(['summary', 'flood_frequency', 'flow_duration', 'low_flow', 'storm_events', 'storm_detail', 'export_storm', 'published_stats', 'compare_stats'])
|
|
293
|
+
.describe('Which gage analysis to perform'),
|
|
294
|
+
siteId: z.string().describe('USGS station ID (e.g., "08158000")'),
|
|
295
|
+
eventId: z.string().optional().describe('Storm event ID (for storm_detail, export_storm)'),
|
|
296
|
+
minYears: z.number().optional().describe('Minimum years of record required'),
|
|
297
|
+
startDate: z.string().optional().describe('Start date YYYY-MM-DD (for flow_duration)'),
|
|
298
|
+
endDate: z.string().optional().describe('End date YYYY-MM-DD (for flow_duration)'),
|
|
299
|
+
period: z.string().optional().describe('Time period "1y"/"5y"/"10y" (for storm_events)'),
|
|
300
|
+
minPeak: z.number().optional().describe('Minimum peak flow in cfs (for storm_events)'),
|
|
301
|
+
format: z.string().optional().describe('Export format (default: "hec-hms")'),
|
|
302
|
+
},
|
|
303
|
+
_actionMap: {
|
|
304
|
+
summary: 'get_gage_summary',
|
|
305
|
+
flood_frequency: 'get_flood_frequency_analysis',
|
|
306
|
+
flow_duration: 'get_flow_duration_curve',
|
|
307
|
+
low_flow: 'get_low_flow_statistics',
|
|
308
|
+
storm_events: 'get_storm_events',
|
|
309
|
+
storm_detail: 'get_storm_event_detail',
|
|
310
|
+
export_storm: 'export_storm_event_for_modeling',
|
|
311
|
+
published_stats: 'get_published_gage_statistics',
|
|
312
|
+
compare_stats: 'compare_computed_vs_published_stats',
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
317
|
+
// 8. UNGAGED ESTIMATION — Flow estimates at sites without gages
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
319
|
+
{
|
|
320
|
+
name: 'estimate_ungaged',
|
|
321
|
+
description: `Estimate flows at ungaged sites using USGS regional regression, watershed similarity, and drainage area transfer methods.
|
|
322
|
+
|
|
323
|
+
Actions:
|
|
324
|
+
- "flood_frequency" — Estimate flood flows using NSS regional regression equations. Requires state, region, and basin characteristics.
|
|
325
|
+
- "all_statistics" — Estimate ALL available flow statistics (peak, low, duration) for an ungaged site.
|
|
326
|
+
- "nss_regions" — List available NSS regions for a state.
|
|
327
|
+
- "required_parameters" — What basin characteristics are needed for a state/region.
|
|
328
|
+
- "find_similar" — Find gauged watersheds with similar physical characteristics.
|
|
329
|
+
- "find_similar_with_stats" — Similar watersheds + their published flow statistics.
|
|
330
|
+
- "recommend_index" — Find the best reference gage for flow transfer.
|
|
331
|
+
- "transfer_stats" — Transfer flood statistics from a reference gage using drainage area ratio.`,
|
|
332
|
+
parameters: {
|
|
333
|
+
action: z.enum(['flood_frequency', 'all_statistics', 'nss_regions', 'required_parameters', 'find_similar', 'find_similar_with_stats', 'recommend_index', 'transfer_stats'])
|
|
334
|
+
.describe('Which estimation method to use'),
|
|
335
|
+
lat: z.number().optional().describe('Latitude of ungaged site'),
|
|
336
|
+
lng: z.number().optional().describe('Longitude of ungaged site'),
|
|
337
|
+
state: z.string().optional().describe('US state code (e.g., "TX")'),
|
|
338
|
+
region: z.string().optional().describe('NSS region code'),
|
|
339
|
+
parameters: z.any().optional().describe('Basin characteristics object (e.g., {drainageArea: 10.5, meanBasinSlope: 3.2})'),
|
|
340
|
+
characteristics: z.any().optional().describe('Known basin characteristics for similarity matching'),
|
|
341
|
+
maxDistance: z.number().optional().describe('Max search distance in km for similarity'),
|
|
342
|
+
limit: z.number().optional().describe('Max results'),
|
|
343
|
+
indexSiteId: z.string().optional().describe('Reference gage ID for transfer_stats'),
|
|
344
|
+
targetDrainageArea: z.number().optional().describe('Ungaged site drainage area in sq mi for transfer_stats'),
|
|
345
|
+
drainageArea: z.number().optional().describe('Drainage area for recommend_index'),
|
|
346
|
+
},
|
|
347
|
+
_actionMap: {
|
|
348
|
+
flood_frequency: 'estimate_ungaged_flood_frequency',
|
|
349
|
+
all_statistics: 'estimate_all_ungaged_statistics',
|
|
350
|
+
nss_regions: 'get_ungaged_nss_regions',
|
|
351
|
+
required_parameters: 'get_ungaged_required_parameters',
|
|
352
|
+
find_similar: 'find_similar_watersheds',
|
|
353
|
+
find_similar_with_stats: 'find_similar_watersheds_with_stats',
|
|
354
|
+
recommend_index: 'recommend_index_gage',
|
|
355
|
+
transfer_stats: 'transfer_flood_statistics',
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
360
|
+
// 9. GENERATE REPORT — Site analysis, constraints, developability
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
362
|
+
{
|
|
363
|
+
name: 'generate_report',
|
|
364
|
+
description: `Generate comprehensive environmental reports for development sites.
|
|
365
|
+
|
|
366
|
+
Actions:
|
|
367
|
+
- "site_analysis" — Full environmental site analysis: flood risk, wetlands, soils, hazards, habitat, contamination. Returns developability score 0-100.
|
|
368
|
+
- "site_analysis_status" — Check status of a site analysis job (can take 30-60s).
|
|
369
|
+
- "constraints" — Environmental constraints report: floodway, flood zones, wetlands, hydric soils, steep slopes. Calculates constrained vs. developable area.
|
|
370
|
+
- "constraints_status" — Check status of constraints report job.
|
|
371
|
+
- "constraints_config" — Available constraint report options.
|
|
372
|
+
- "developability" — Site developability assessment with 0-100 score and penalty breakdown.
|
|
373
|
+
- "developability_config" — Available developability assessment options.`,
|
|
374
|
+
parameters: {
|
|
375
|
+
action: z.enum(['site_analysis', 'site_analysis_status', 'constraints', 'constraints_status', 'constraints_config', 'developability', 'developability_config'])
|
|
376
|
+
.describe('Which report to generate or check'),
|
|
377
|
+
geometry: z.any().optional().describe('GeoJSON Point or Polygon for the site'),
|
|
378
|
+
projectName: z.string().optional().describe('Project name for report'),
|
|
379
|
+
clientName: z.string().optional().describe('Client name for report'),
|
|
380
|
+
jobId: z.string().optional().describe('Job ID for status checks'),
|
|
381
|
+
options: z.any().optional().describe('Report options'),
|
|
382
|
+
},
|
|
383
|
+
_actionMap: {
|
|
384
|
+
site_analysis: 'generate_site_analysis',
|
|
385
|
+
site_analysis_status: 'get_site_analysis_status',
|
|
386
|
+
constraints: 'generate_constraints_report',
|
|
387
|
+
constraints_status: 'get_constraints_report_status',
|
|
388
|
+
constraints_config: 'get_constraints_config',
|
|
389
|
+
developability: 'generate_developability_report',
|
|
390
|
+
developability_config: 'get_developability_config',
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
395
|
+
// 10. EXPORT DATA — Multi-format GIS export
|
|
396
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
397
|
+
{
|
|
398
|
+
name: 'export_data',
|
|
399
|
+
description: `Export environmental data layers to GIS formats (GeoJSON, Shapefile, KML, CSV, GeoPackage).
|
|
400
|
+
|
|
401
|
+
Actions:
|
|
402
|
+
- "export" — Export layers to a file format. Supports CRS transformation and clipping.
|
|
403
|
+
- "options" — List available export formats and CRS options.
|
|
404
|
+
- "status" — Check export job status.`,
|
|
405
|
+
parameters: {
|
|
406
|
+
action: z.enum(['export', 'options', 'status'])
|
|
407
|
+
.describe('Which export operation'),
|
|
408
|
+
layers: z.array(z.string()).optional().describe('Layer names to export'),
|
|
409
|
+
format: z.enum(['geojson', 'shapefile', 'kml', 'csv', 'geopackage']).optional().describe('Output format'),
|
|
410
|
+
crs: z.string().optional().describe('Target CRS (e.g., "EPSG:4326")'),
|
|
411
|
+
geometry: z.any().optional().describe('GeoJSON geometry to clip export area'),
|
|
412
|
+
options: z.any().optional().describe('Additional options: {dem, satellite, nlcd, contours}'),
|
|
413
|
+
jobId: z.string().optional().describe('Job ID for status check'),
|
|
414
|
+
},
|
|
415
|
+
_actionMap: {
|
|
416
|
+
export: 'export_data',
|
|
417
|
+
options: 'get_export_options',
|
|
418
|
+
status: 'get_export_status',
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
423
|
+
// 11. FIND STATIONS — Monitoring stations & permits
|
|
424
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
425
|
+
{
|
|
426
|
+
name: 'find_stations',
|
|
427
|
+
description: `Search for environmental monitoring stations (USGS streamgages, groundwater wells, weather stations, tide gauges) and analyze waterway permit requirements.
|
|
428
|
+
|
|
429
|
+
Actions:
|
|
430
|
+
- "search_area" — Find stations near a location or in a bounding box.
|
|
431
|
+
- "search_name" — Search stations by name or ID.
|
|
432
|
+
- "station_types" — List all available station types.
|
|
433
|
+
- "find_water_features" — Find streams, wetlands, waterbodies for permit analysis.
|
|
434
|
+
- "analyze_permits" — Determine required permits (Section 404, NPDES, etc.) for an activity near water.`,
|
|
435
|
+
parameters: {
|
|
436
|
+
action: z.enum(['search_area', 'search_name', 'station_types', 'find_water_features', 'analyze_permits'])
|
|
437
|
+
.describe('Which search to perform'),
|
|
438
|
+
// Station search params
|
|
439
|
+
bbox: z.string().optional().describe('Bounding box for area search'),
|
|
440
|
+
source: z.string().optional().describe('Data source filter: "usgs", "noaa"'),
|
|
441
|
+
type: z.string().optional().describe('Station type: "stream_gage", "groundwater", "tide", "precipitation"'),
|
|
442
|
+
state: z.string().optional().describe('US state code'),
|
|
443
|
+
q: z.string().optional().describe('Search query for search_name'),
|
|
444
|
+
limit: z.number().optional().describe('Max results'),
|
|
445
|
+
// Permit params
|
|
446
|
+
polygon: z.any().optional().describe('GeoJSON Polygon for find_water_features'),
|
|
447
|
+
selectedFeatures: z.any().optional().describe('Water features from find_water_features for analyze_permits'),
|
|
448
|
+
activityType: z.enum(['crossing', 'utility_crossing', 'stormwater_discharge', 'wetland_fill', 'bank_stabilization', 'adjacent_construction']).optional()
|
|
449
|
+
.describe('Activity type for analyze_permits'),
|
|
450
|
+
location: z.any().optional().describe('GeoJSON Point for activity location'),
|
|
451
|
+
},
|
|
452
|
+
_actionMap: {
|
|
453
|
+
search_area: 'find_monitoring_stations',
|
|
454
|
+
search_name: 'search_stations',
|
|
455
|
+
station_types: 'get_station_types',
|
|
456
|
+
find_water_features: 'find_water_features',
|
|
457
|
+
analyze_permits: 'analyze_permit_requirements',
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
462
|
+
// 12. CHECK STATUS — API health
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
464
|
+
{
|
|
465
|
+
name: 'check_status',
|
|
466
|
+
description: `Check GeoTap API health and federal data source connectivity.
|
|
467
|
+
|
|
468
|
+
Actions:
|
|
469
|
+
- "all" — Check all connected federal APIs (FEMA, USGS, EPA, NOAA, etc.).
|
|
470
|
+
- "specific" — Check one specific API.`,
|
|
471
|
+
parameters: {
|
|
472
|
+
action: z.enum(['all', 'specific']).describe('Check all APIs or a specific one'),
|
|
473
|
+
apiName: z.string().optional().describe('API name for specific check: "fema", "usgs", "epa", "noaa", "nrcs"'),
|
|
474
|
+
},
|
|
475
|
+
_actionMap: {
|
|
476
|
+
all: 'check_api_status',
|
|
477
|
+
specific: 'check_specific_api_status',
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
];
|
package/src/index.js
CHANGED
|
@@ -4,72 +4,246 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { tools } from './tools.js';
|
|
7
|
+
import { consolidatedTools } from './consolidatedTools.js';
|
|
7
8
|
import { callApi, StructuredApiError } from './api.js';
|
|
8
9
|
import { toolSources } from './sources.js';
|
|
9
10
|
import { capResponse } from './responseCap.js';
|
|
10
11
|
import { generateSummary } from './summaries.js';
|
|
11
12
|
import { convertLatLng } from './latLngHelper.js';
|
|
13
|
+
import { normalizeParams } from './paramNormalize.js';
|
|
12
14
|
import { discoverTools } from './discoverTools.js';
|
|
13
15
|
import { readFileSync } from 'fs';
|
|
14
16
|
import { fileURLToPath } from 'url';
|
|
15
17
|
import { dirname, join } from 'path';
|
|
16
18
|
|
|
17
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const useLegacyTools = process.env.GEOTAP_LEGACY_TOOLS === 'true';
|
|
21
|
+
|
|
22
|
+
// Build lookup map for legacy tools (used by both modes)
|
|
23
|
+
const legacyToolMap = new Map(tools.map(t => [t.name, t]));
|
|
18
24
|
|
|
19
25
|
const server = new McpServer({
|
|
20
26
|
name: 'geotap',
|
|
21
|
-
version: '
|
|
27
|
+
version: '2.0.0',
|
|
22
28
|
description: 'Access 37 US federal environmental and infrastructure data sources. Query flood zones, wetlands, soils, rainfall, watersheds, water quality, endangered species, elevation, land use, and more for any location in the United States.',
|
|
23
|
-
instructions:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
29
|
+
instructions: useLegacyTools
|
|
30
|
+
? `You have access to GeoTap with 85 individual tools. Use discover_tools to find the right one.`
|
|
31
|
+
: `You have access to GeoTap, which provides real-time data from 37 US federal agencies (FEMA, USGS, NOAA, EPA, NRCS, USFWS, USACE, and more).
|
|
32
|
+
|
|
33
|
+
TOOL OVERVIEW (12 tools + 2 meta-tools):
|
|
34
|
+
1. query_location — Start here. Query environmental data by address, coordinates, bbox, polygon, or radius.
|
|
35
|
+
2. get_rainfall — NOAA Atlas 14 precipitation, IDF curves, hyetographs, climate projections.
|
|
36
|
+
3. get_watershed — Watershed delineation, flow statistics, flowlines, HUC boundaries, FIRM panels.
|
|
37
|
+
4. get_hydrology — Curve numbers, runoff calculations, time of concentration, peak discharge.
|
|
38
|
+
5. get_water_quality — EPA water quality impairments, 303(d) listings, receiving waters.
|
|
39
|
+
6. get_elevation — USGS 3DEP elevation, contours, DEM export, land use, satellite imagery.
|
|
40
|
+
7. analyze_gage — USGS stream gage analysis: flood frequency, flow duration, storm events.
|
|
41
|
+
8. estimate_ungaged — Flow estimation at ungaged sites: regression, similarity, transfer methods.
|
|
42
|
+
9. generate_report — Site analysis, constraints, and developability reports with scoring.
|
|
43
|
+
10. export_data — Export layers to GeoJSON, Shapefile, KML, CSV, GeoPackage.
|
|
44
|
+
11. find_stations — Find monitoring stations (USGS, NOAA) and analyze waterway permit requirements.
|
|
45
|
+
12. check_status — API health and federal data source connectivity.
|
|
46
|
+
|
|
47
|
+
Every tool uses an "action" parameter to select the specific operation. Read the tool description to see available actions.
|
|
48
|
+
|
|
49
|
+
COORDINATE FLEXIBILITY:
|
|
50
|
+
- All tools accept lat/lng, lat/lon, or latitude/longitude — they are automatically normalized.
|
|
51
|
+
- POST tools accept flat lat/lng instead of GeoJSON — auto-converted to the correct format.
|
|
52
|
+
|
|
53
|
+
RESPONSE SIZE:
|
|
54
|
+
- geometry defaults to "none" for spatial queries (prevents context overflow).
|
|
55
|
+
- Responses auto-capped at 40KB (~10K tokens). Use specific layers to get smaller responses.
|
|
56
|
+
- Always specify layers (e.g., layers="flood_zones,wetlands") instead of querying all layers.
|
|
49
57
|
|
|
50
58
|
COMMON WORKFLOWS:
|
|
51
|
-
- "What flood zone is this address in?" →
|
|
52
|
-
- "
|
|
53
|
-
- "
|
|
54
|
-
- "
|
|
55
|
-
- "
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
- This data is for informational purposes. Remind users to verify critical data for engineering/regulatory decisions.
|
|
63
|
-
- Coordinates must be within the United States (including territories).
|
|
64
|
-
- Some tools (watershed delineation, hydrology) can take 10-60 seconds.
|
|
65
|
-
- Layer names use underscores: flood_zones, wetlands, dem_elevation, building_footprints, etc.`
|
|
59
|
+
- "What flood zone is this address in?" → query_location(action: "address", address: "...")
|
|
60
|
+
- "What's the 100-year rainfall?" → get_rainfall(action: "atlas14", lat, lng)
|
|
61
|
+
- "Delineate the watershed" → get_watershed(action: "delineate", lat, lng)
|
|
62
|
+
- "Environmental site analysis" → generate_report(action: "site_analysis", geometry: ...)
|
|
63
|
+
- "What permits do I need?" → find_stations(action: "find_water_features") → find_stations(action: "analyze_permits")
|
|
64
|
+
|
|
65
|
+
IMPORTANT:
|
|
66
|
+
- All data from authoritative US federal sources. Always cite the source agency.
|
|
67
|
+
- Responses include _summary with plain-English descriptions — use these in answers.
|
|
68
|
+
- Data is for informational purposes. Remind users to verify for engineering/regulatory decisions.
|
|
69
|
+
- Coordinates must be within the United States (including territories).`
|
|
66
70
|
});
|
|
67
71
|
|
|
68
|
-
// ──
|
|
72
|
+
// ── Shared tool call handler ────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle a tool call through the standard pipeline:
|
|
76
|
+
* normalize → convertLatLng → callApi → capResponse → generateSummary → checkDataQuality → enrich
|
|
77
|
+
*
|
|
78
|
+
* @param {string} legacyToolName - The original tool name (for routing summaries/sources)
|
|
79
|
+
* @param {string} endpoint - API endpoint path
|
|
80
|
+
* @param {string} method - HTTP method (GET/POST)
|
|
81
|
+
* @param {object} params - Parameters from the LLM (after action extraction)
|
|
82
|
+
* @returns {object} MCP tool response
|
|
83
|
+
*/
|
|
84
|
+
async function handleToolCall(legacyToolName, endpoint, method, params) {
|
|
85
|
+
try {
|
|
86
|
+
// Fix #1: Normalize coordinate params to what backend expects
|
|
87
|
+
const normalized = normalizeParams(legacyToolName, params);
|
|
88
|
+
|
|
89
|
+
// Convert lat/lng to GeoJSON if needed (existing feature)
|
|
90
|
+
const convertedParams = convertLatLng(legacyToolName, normalized);
|
|
91
|
+
|
|
92
|
+
// Strip internal fields before sending to API
|
|
93
|
+
const apiParams = { ...convertedParams };
|
|
94
|
+
delete apiParams._latLngConverted;
|
|
95
|
+
|
|
96
|
+
const rawResult = await callApi(endpoint, method, apiParams);
|
|
97
|
+
|
|
98
|
+
// Cap response size
|
|
99
|
+
const { data: cappedResult, wasCapped, capInfo } = capResponse(legacyToolName, rawResult);
|
|
100
|
+
|
|
101
|
+
// Generate natural language summary
|
|
102
|
+
const summary = generateSummary(legacyToolName, params, cappedResult);
|
|
103
|
+
|
|
104
|
+
// Fix #5: Check data quality and add warnings
|
|
105
|
+
const dataQuality = checkDataQuality(legacyToolName, cappedResult);
|
|
106
|
+
|
|
107
|
+
// Enrich response with source attribution, summary, and metadata
|
|
108
|
+
const sources = toolSources[legacyToolName] || [];
|
|
109
|
+
const enriched = {
|
|
110
|
+
...(summary ? { _summary: summary } : {}),
|
|
111
|
+
...cappedResult,
|
|
112
|
+
...(convertedParams._latLngConverted ? { _latLngConverted: convertedParams._latLngConverted } : {}),
|
|
113
|
+
...(wasCapped ? { _responseCapped: capInfo } : {}),
|
|
114
|
+
...(dataQuality ? { _dataQuality: dataQuality } : {}),
|
|
115
|
+
_meta: {
|
|
116
|
+
sources,
|
|
117
|
+
retrievedAt: new Date().toISOString(),
|
|
118
|
+
disclaimer: 'Data sourced from US federal agencies via GeoTap. Always verify critical data against authoritative sources before making engineering or regulatory decisions.',
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }]
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof StructuredApiError) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
135
|
+
error: true,
|
|
136
|
+
message: error.message,
|
|
137
|
+
fix: ['Check that all required parameters are provided and valid.', 'Use discover_tools or check the tool description for available actions.'],
|
|
138
|
+
relatedTools: ['discover_tools', 'check_status']
|
|
139
|
+
}, null, 2) }],
|
|
140
|
+
isError: true
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Fix #5: Data quality warnings ───────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check API response for signs of degraded or incomplete data.
|
|
149
|
+
* Returns a _dataQuality object with warnings, or null if no issues detected.
|
|
150
|
+
*/
|
|
151
|
+
function checkDataQuality(toolName, result) {
|
|
152
|
+
if (!result || typeof result !== 'object') return null;
|
|
153
|
+
|
|
154
|
+
const warnings = [];
|
|
155
|
+
|
|
156
|
+
// Check for empty layer results in spatial queries
|
|
157
|
+
if (result.layers && typeof result.layers === 'object') {
|
|
158
|
+
const emptyLayers = [];
|
|
159
|
+
const populatedLayers = [];
|
|
160
|
+
for (const [name, data] of Object.entries(result.layers)) {
|
|
161
|
+
if (data?.features?.length === 0 || data?.featureCount === 0) {
|
|
162
|
+
emptyLayers.push(name);
|
|
163
|
+
} else if (data?.features?.length > 0) {
|
|
164
|
+
populatedLayers.push(name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (emptyLayers.length > 0 && populatedLayers.length > 0) {
|
|
168
|
+
// Only warn if some layers returned data but others didn't
|
|
169
|
+
// (if ALL are empty, that's a valid "nothing here" result)
|
|
170
|
+
warnings.push({
|
|
171
|
+
type: 'partial_results',
|
|
172
|
+
message: `${emptyLayers.length} layer(s) returned no features: ${emptyLayers.join(', ')}. This may be expected (no features at this location) or may indicate a data gap.`,
|
|
173
|
+
layers: emptyLayers,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for empty watershed geometry (the 82% failure pattern)
|
|
179
|
+
if (toolName === 'delineate_watershed' || toolName === 'get_watershed') {
|
|
180
|
+
const geom = result.data?.geometry || result.geometry || result.boundary?.geometry;
|
|
181
|
+
if (geom && geom.coordinates && geom.coordinates.length === 0) {
|
|
182
|
+
warnings.push({
|
|
183
|
+
type: 'empty_geometry',
|
|
184
|
+
message: 'Watershed delineation returned empty geometry. USGS StreamStats may not have coverage for this location. Try a nearby point on a mapped stream.',
|
|
185
|
+
severity: 'error',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (result.data?.characteristics?.drainageArea === null || result.data?.characteristics?.drainageArea === undefined) {
|
|
189
|
+
if (result.data?.characteristics) {
|
|
190
|
+
warnings.push({
|
|
191
|
+
type: 'missing_field',
|
|
192
|
+
message: 'Drainage area not returned by StreamStats. This basin characteristic may not be available for this region.',
|
|
193
|
+
field: 'drainageArea',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check for null key fields in gage data
|
|
200
|
+
if (toolName.startsWith('get_gage') || toolName.startsWith('get_flood') || toolName.startsWith('get_flow') || toolName.startsWith('get_storm')) {
|
|
201
|
+
if (result.drainageArea === null || result.drainageArea === undefined) {
|
|
202
|
+
if (result.siteName || result.siteId) {
|
|
203
|
+
warnings.push({
|
|
204
|
+
type: 'missing_field',
|
|
205
|
+
message: 'Drainage area is null for this gage. USGS NWIS may not have this metadata. Cross-reference with StreamStats if drainage area is needed.',
|
|
206
|
+
field: 'drainageArea',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for stale NLCD data
|
|
213
|
+
if (toolName === 'export_land_use' || toolName === 'analyze_curve_numbers') {
|
|
214
|
+
warnings.push({
|
|
215
|
+
type: 'data_currency',
|
|
216
|
+
message: 'NLCD land cover data is from 2021 (latest available). For rapidly developing areas, ground-truth current conditions before relying on land use classifications.',
|
|
217
|
+
severity: 'info',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check for Atlas 14 metadata issues (Ohio River Basin bug)
|
|
222
|
+
if (toolName === 'get_rainfall_data' || toolName === 'get_idf_curves') {
|
|
223
|
+
const loc = result.location || result.metadata?.location;
|
|
224
|
+
if (loc?.region && /ohio\s*river\s*basin/i.test(loc.region)) {
|
|
225
|
+
// Check if the actual coordinates suggest a different region
|
|
226
|
+
const lat = result.metadata?.lat || result.lat;
|
|
227
|
+
if (lat && lat < 36) {
|
|
228
|
+
warnings.push({
|
|
229
|
+
type: 'metadata_mismatch',
|
|
230
|
+
message: 'NOAA Atlas 14 reports "Ohio River Basin" as the region, but the coordinates may be in a different region. The rainfall data values are still correct — the region label is a known NOAA metadata issue for some locations.',
|
|
231
|
+
severity: 'info',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return warnings.length > 0 ? { warnings, totalWarnings: warnings.length } : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Register meta-tools ─────────────────────────────────────────────
|
|
69
241
|
|
|
70
242
|
server.tool(
|
|
71
243
|
'discover_tools',
|
|
72
|
-
|
|
244
|
+
useLegacyTools
|
|
245
|
+
? 'Find the best GeoTap tools for your question. Describe what you need in plain English and get back the 3-5 most relevant tools with their parameters.'
|
|
246
|
+
: 'Find the best GeoTap tool and action for your question. Describe what you need in plain English. Helpful when you\'re unsure which tool or action to use.',
|
|
73
247
|
{
|
|
74
248
|
question: z.string().describe('Natural language description of what you want to do (e.g., "What flood zone is this property in?" or "I need rainfall data for stormwater design")'),
|
|
75
249
|
maxResults: z.number().optional().describe('Maximum tools to return (default: 5)')
|
|
@@ -82,11 +256,9 @@ server.tool(
|
|
|
82
256
|
}
|
|
83
257
|
);
|
|
84
258
|
|
|
85
|
-
// ── Register get_llms_txt tool ──────────────────────────────────────
|
|
86
|
-
|
|
87
259
|
server.tool(
|
|
88
260
|
'get_llms_txt',
|
|
89
|
-
'Get the GeoTap API discovery document (llms.txt). Returns a structured description of all API endpoints, data sources, and usage tips optimized for AI agents.
|
|
261
|
+
'Get the GeoTap API discovery document (llms.txt). Returns a structured description of all API endpoints, data sources, and usage tips optimized for AI agents.',
|
|
90
262
|
{},
|
|
91
263
|
async () => {
|
|
92
264
|
try {
|
|
@@ -101,68 +273,59 @@ server.tool(
|
|
|
101
273
|
}
|
|
102
274
|
);
|
|
103
275
|
|
|
104
|
-
// ── Register
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
tool
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }]
|
|
144
|
-
};
|
|
145
|
-
} catch (error) {
|
|
146
|
-
// Improvement #5: Structured error messages
|
|
147
|
-
if (error instanceof StructuredApiError) {
|
|
276
|
+
// ── Register tools based on mode ────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
if (useLegacyTools) {
|
|
279
|
+
// Legacy mode: register all 85 individual tools (for existing consumers)
|
|
280
|
+
for (const tool of tools) {
|
|
281
|
+
server.tool(
|
|
282
|
+
tool.name,
|
|
283
|
+
tool.description,
|
|
284
|
+
tool.parameters,
|
|
285
|
+
async (params) => handleToolCall(tool.name, tool.endpoint, tool.method, params)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
console.error(`[geotap] Legacy mode: registered ${tools.length} individual tools`);
|
|
289
|
+
} else {
|
|
290
|
+
// Default: register 12 consolidated tools
|
|
291
|
+
for (const ctool of consolidatedTools) {
|
|
292
|
+
server.tool(
|
|
293
|
+
ctool.name,
|
|
294
|
+
ctool.description,
|
|
295
|
+
ctool.parameters,
|
|
296
|
+
async (params) => {
|
|
297
|
+
const { action, ...restParams } = params;
|
|
298
|
+
|
|
299
|
+
// Resolve consolidated action → legacy tool
|
|
300
|
+
const legacyName = ctool._actionMap[action];
|
|
301
|
+
if (!legacyName) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
304
|
+
error: true,
|
|
305
|
+
message: `Unknown action "${action}" for tool "${ctool.name}".`,
|
|
306
|
+
validActions: Object.keys(ctool._actionMap),
|
|
307
|
+
fix: [`Use one of: ${Object.keys(ctool._actionMap).join(', ')}`],
|
|
308
|
+
}, null, 2) }],
|
|
309
|
+
isError: true
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const legacyTool = legacyToolMap.get(legacyName);
|
|
314
|
+
if (!legacyTool) {
|
|
148
315
|
return {
|
|
149
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
316
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
317
|
+
error: true,
|
|
318
|
+
message: `Internal routing error: legacy tool "${legacyName}" not found.`,
|
|
319
|
+
}, null, 2) }],
|
|
150
320
|
isError: true
|
|
151
321
|
};
|
|
152
322
|
}
|
|
153
323
|
|
|
154
|
-
return
|
|
155
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
156
|
-
error: true,
|
|
157
|
-
message: error.message,
|
|
158
|
-
fix: ['Check that all required parameters are provided and valid.', 'Use discover_tools to find the right tool for your question.'],
|
|
159
|
-
relatedTools: ['discover_tools', 'check_api_status']
|
|
160
|
-
}, null, 2) }],
|
|
161
|
-
isError: true
|
|
162
|
-
};
|
|
324
|
+
return handleToolCall(legacyName, legacyTool.endpoint, legacyTool.method, restParams);
|
|
163
325
|
}
|
|
164
|
-
|
|
165
|
-
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
console.error(`[geotap] Consolidated mode: registered ${consolidatedTools.length} tools (set GEOTAP_LEGACY_TOOLS=true for 85 individual tools)`);
|
|
166
329
|
}
|
|
167
330
|
|
|
168
331
|
// Start server
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter Normalization for MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Fixes the #1 LLM error pattern: inconsistent coordinate parameter naming.
|
|
5
|
+
* The backend has 3 conventions: lat/lng, lat/lon, latitude/longitude.
|
|
6
|
+
* This normalizer accepts ANY variant from the LLM and converts to what
|
|
7
|
+
* each specific endpoint expects.
|
|
8
|
+
*
|
|
9
|
+
* Also handles:
|
|
10
|
+
* - Default geometry="none" for MCP consumers (prevents context blow-up)
|
|
11
|
+
* - Removal of undefined/null optional params
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-tool coordinate format expected by the backend.
|
|
16
|
+
* 'lat_lng' = { lat, lng }
|
|
17
|
+
* 'lat_lon' = { lat, lon }
|
|
18
|
+
* 'latitude_longitude' = { latitude, longitude }
|
|
19
|
+
*/
|
|
20
|
+
const COORD_FORMAT = {
|
|
21
|
+
// lat/lng tools
|
|
22
|
+
identify_features_at_point: 'lat_lng',
|
|
23
|
+
get_environmental_data_near_point: 'lat_lng',
|
|
24
|
+
delineate_watershed: 'lat_lng',
|
|
25
|
+
get_watershed_characteristics: 'lat_lng',
|
|
26
|
+
get_flow_statistics: 'lat_lng',
|
|
27
|
+
find_similar_watersheds: 'lat_lng',
|
|
28
|
+
find_similar_watersheds_with_stats: 'lat_lng',
|
|
29
|
+
recommend_index_gage: 'lat_lng',
|
|
30
|
+
transfer_flood_statistics: 'lat_lng',
|
|
31
|
+
|
|
32
|
+
// lat/lon tools
|
|
33
|
+
get_rainfall_data: 'lat_lon',
|
|
34
|
+
get_idf_curves: 'lat_lon',
|
|
35
|
+
get_rainfall_distribution: 'lat_lon',
|
|
36
|
+
get_climate_change_factors: 'lat_lon',
|
|
37
|
+
get_rainfall_uncertainty_bounds: 'lat_lon',
|
|
38
|
+
get_hydrology_distribution_for_location: 'lat_lon',
|
|
39
|
+
get_watershed_for_point: 'lat_lon',
|
|
40
|
+
|
|
41
|
+
// latitude/longitude tools
|
|
42
|
+
generate_hyetograph: 'latitude_longitude',
|
|
43
|
+
export_hyetograph: 'latitude_longitude',
|
|
44
|
+
get_climate_change_rainfall_projection: 'latitude_longitude',
|
|
45
|
+
generate_uncertainty_envelope: 'latitude_longitude',
|
|
46
|
+
run_rainfall_sensitivity_analysis: 'latitude_longitude',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tools that support a `geometry` parameter for controlling response size.
|
|
51
|
+
* For MCP consumers, default to "none" to prevent context window overflow.
|
|
52
|
+
*/
|
|
53
|
+
const GEOMETRY_PARAM_TOOLS = new Set([
|
|
54
|
+
'get_environmental_data_for_area',
|
|
55
|
+
'get_environmental_data_near_point',
|
|
56
|
+
'get_environmental_data_in_bbox',
|
|
57
|
+
'get_layer_features',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Normalize parameters for a tool call.
|
|
62
|
+
*
|
|
63
|
+
* 1. Converts any lat/lng/lon/latitude/longitude variant to what the backend expects
|
|
64
|
+
* 2. Defaults geometry="none" for tools that support it (MCP context protection)
|
|
65
|
+
* 3. Strips undefined/null optional params
|
|
66
|
+
*
|
|
67
|
+
* @param {string} toolName - The legacy tool name (after action routing for consolidated tools)
|
|
68
|
+
* @param {object} params - Raw parameters from the LLM
|
|
69
|
+
* @returns {object} Normalized parameters
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeParams(toolName, params) {
|
|
72
|
+
if (!params || typeof params !== 'object') return params;
|
|
73
|
+
|
|
74
|
+
let p = { ...params };
|
|
75
|
+
|
|
76
|
+
// ── 1. Coordinate normalization ──────────────────────────────────
|
|
77
|
+
const format = COORD_FORMAT[toolName];
|
|
78
|
+
if (format) {
|
|
79
|
+
// Extract lat from any variant
|
|
80
|
+
const lat = p.lat ?? p.latitude;
|
|
81
|
+
// Extract lng from any variant
|
|
82
|
+
const lng = p.lng ?? p.lon ?? p.longitude;
|
|
83
|
+
|
|
84
|
+
// Remove all coordinate variants
|
|
85
|
+
delete p.lat;
|
|
86
|
+
delete p.latitude;
|
|
87
|
+
delete p.lng;
|
|
88
|
+
delete p.lon;
|
|
89
|
+
delete p.longitude;
|
|
90
|
+
|
|
91
|
+
// Set the correct format if we have values
|
|
92
|
+
if (lat !== undefined && lng !== undefined) {
|
|
93
|
+
switch (format) {
|
|
94
|
+
case 'lat_lng':
|
|
95
|
+
p.lat = Number(lat);
|
|
96
|
+
p.lng = Number(lng);
|
|
97
|
+
break;
|
|
98
|
+
case 'lat_lon':
|
|
99
|
+
p.lat = Number(lat);
|
|
100
|
+
p.lon = Number(lng);
|
|
101
|
+
break;
|
|
102
|
+
case 'latitude_longitude':
|
|
103
|
+
p.latitude = Number(lat);
|
|
104
|
+
p.longitude = Number(lng);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 2. Default geometry="none" for MCP ───────────────────────────
|
|
111
|
+
if (GEOMETRY_PARAM_TOOLS.has(toolName) && p.geometry === undefined) {
|
|
112
|
+
p.geometry = 'none';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 3. Strip undefined/null optional params ──────────────────────
|
|
116
|
+
for (const [key, value] of Object.entries(p)) {
|
|
117
|
+
if (value === undefined || value === null) {
|
|
118
|
+
delete p[key];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return p;
|
|
123
|
+
}
|
package/src/responseCap.js
CHANGED
|
@@ -7,21 +7,35 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const MAX_FEATURES = 50;
|
|
10
|
-
const MAX_RESPONSE_CHARS =
|
|
10
|
+
const MAX_RESPONSE_CHARS = 40_000; // ~10K tokens — keeps responses within LLM context budget
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Tools that return feature collections and can be very large.
|
|
14
14
|
* These get smart feature capping with layer summaries.
|
|
15
15
|
*/
|
|
16
16
|
const FEATURE_HEAVY_TOOLS = new Set([
|
|
17
|
+
// Spatial queries
|
|
17
18
|
'get_environmental_data_for_area',
|
|
18
19
|
'get_environmental_data_near_point',
|
|
19
20
|
'get_environmental_data_in_bbox',
|
|
20
21
|
'get_layer_features',
|
|
22
|
+
'query_address',
|
|
23
|
+
'identify_features_at_point',
|
|
24
|
+
// Hydro/watershed
|
|
21
25
|
'get_flowlines',
|
|
22
|
-
'find_water_features',
|
|
23
26
|
'get_huc_watersheds',
|
|
27
|
+
'get_watershed_water_quality',
|
|
28
|
+
'delineate_watershed',
|
|
29
|
+
// Stations & water features
|
|
30
|
+
'find_water_features',
|
|
24
31
|
'find_monitoring_stations',
|
|
32
|
+
'search_stations',
|
|
33
|
+
// Gage intelligence
|
|
34
|
+
'get_storm_events',
|
|
35
|
+
'find_similar_watersheds',
|
|
36
|
+
'find_similar_watersheds_with_stats',
|
|
37
|
+
// Water quality
|
|
38
|
+
'get_water_quality',
|
|
25
39
|
]);
|
|
26
40
|
|
|
27
41
|
/**
|