geotap-mcp-server 2.2.1 → 3.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GeoTap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -449,7 +449,7 @@ curl -X POST "https://geotapdata.com/api/v1/export" \
449
449
 
450
450
  ## MCP Server
451
451
 
452
- The MCP (Model Context Protocol) server wraps the REST API into **83 AI-native tools** that Claude, Cursor, Windsurf, and other AI assistants can call directly.
452
+ The MCP (Model Context Protocol) server wraps the REST API into **16 consolidated tools** (or 109 legacy tools) that Claude, Cursor, Windsurf, and other AI assistants can call directly.
453
453
 
454
454
  ### Installation
455
455
 
@@ -529,7 +529,7 @@ With API key:
529
529
  - *"What permits do I need to build near this stream?"*
530
530
  - *"Export this data as a shapefile"*
531
531
 
532
- ### Available Tools (85)
532
+ ### Available Tools (109 legacy / 16 consolidated)
533
533
 
534
534
  **Core tools (start here):**
535
535
  - **query_address** — Geocode + environmental query in one call. Returns properties with plain-English interpretations. (<5KB response)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "geotap-mcp-server",
3
- "version": "2.2.1",
4
- "description": "MCP server for GeoTap — access 37 US federal environmental and infrastructure data sources from Claude, Cursor, and other AI tools",
3
+ "version": "3.0.1",
4
+ "description": "MCP server for GeoTap — collect comprehensive environmental data from 80+ US federal sources for any site. One tool, all the data.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -45,7 +45,8 @@
45
45
  "author": "GeoTap",
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.12.1"
48
+ "@modelcontextprotocol/sdk": "^1.12.1",
49
+ "zod": "^4.3.6"
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=18.0.0"
package/src/api.js CHANGED
@@ -102,7 +102,7 @@ function buildStructuredError(status, errorText, endpoint, method, params) {
102
102
  export async function callApi(endpoint, method, params) {
103
103
  const headers = {
104
104
  'Content-Type': 'application/json',
105
- 'User-Agent': 'geotap-mcp-server/1.0.0'
105
+ 'User-Agent': 'geotap-mcp-server/2.2.1'
106
106
  };
107
107
 
108
108
  if (API_KEY) {
package/src/index.js CHANGED
@@ -3,267 +3,228 @@
3
3
  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
- import { tools } from './tools.js';
7
- import { consolidatedTools } from './consolidatedTools.js';
8
6
  import { callApi, StructuredApiError } from './api.js';
9
- import { toolSources } from './sources.js';
10
- import { capResponse } from './responseCap.js';
11
- import { generateSummary } from './summaries.js';
12
- import { convertLatLng } from './latLngHelper.js';
13
- import { normalizeParams } from './paramNormalize.js';
14
- import { discoverTools } from './discoverTools.js';
15
7
  import { readFileSync } from 'fs';
16
8
  import { fileURLToPath } from 'url';
17
9
  import { dirname, join } from 'path';
18
10
 
19
11
  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]));
24
12
 
25
13
  const server = new McpServer({
26
14
  name: 'geotap',
27
- version: '2.0.0',
28
- description: 'Access US federal environmental and infrastructure data layers from 64+ agencies. Query flood zones, wetlands, soils, rainfall, watersheds, water quality, endangered species, elevation, land use, hazards, energy, infrastructure, transportation, and more for any location in the United States.',
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 (16 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, CSV.
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
- 13. get_hazards FEMA risk index, seismic design values, wildfires, landslides, coastal vulnerability, flood insurance claims, social vulnerability.
47
- 14. get_energy Solar resource, PVWatts production estimates, utility rates, EV charging stations.
48
- 15. get_infrastructure Hospitals, fire stations, schools, power plants, airports, railroad crossings, bridges, historic places.
49
- 16. get_ecology Species occurrences, essential fish habitat.
50
-
51
- Every tool uses an "action" parameter to select the specific operation. Read the tool description to see available actions.
52
-
53
- COORDINATE FLEXIBILITY:
54
- - All tools accept lat/lng, lat/lon, or latitude/longitude they are automatically normalized.
55
- - POST tools accept flat lat/lng instead of GeoJSON auto-converted to the correct format.
56
-
57
- RESPONSE SIZE:
58
- - geometry defaults to "none" for spatial queries (prevents context overflow).
59
- - Responses auto-capped at 40KB (~10K tokens). Use specific layers to get smaller responses.
60
- - Always specify layers (e.g., layers="flood_zones,wetlands") instead of querying all layers.
61
-
62
- COMMON WORKFLOWS:
63
- - "What flood zone is this address in?" → query_location(action: "address", address: "...")
64
- - "What's the 100-year rainfall?" get_rainfall(action: "atlas14", lat, lng)
65
- - "Delineate the watershed" get_watershed(action: "delineate", lat, lng)
66
- - "Environmental site analysis" generate_report(action: "site_analysis", geometry: ...)
67
- - "What permits do I need?" find_stations(action: "find_water_features") find_stations(action: "analyze_permits")
15
+ version: '3.0.0',
16
+ description: 'Collect comprehensive environmental and infrastructure data from 80+ US federal sources for any site in the United States. Returns raw data from FEMA, USGS, NOAA, EPA, NRCS, USFWS, USACE, DOE, DOT, CDC, Census, and more.',
17
+ instructions: `You have access to GeoTap, which collects data from 80+ US federal agencies for any site in the United States.
18
+
19
+ HOW IT WORKS:
20
+ 1. Call collect_site_data with a site location (address, coordinates, or GeoJSON geometry)
21
+ 2. You get back a jobId — the backend queries all 80+ federal sources (takes 60-120 seconds)
22
+ 3. Poll get_results every 10 seconds with the jobId until status is "completed"
23
+ 4. When complete, you receive ALL available data for that site — present it to the user
24
+
25
+ DATA INCLUDES:
26
+ - Flood zones (FEMA NFHL), wetlands (NWI), soils (NRCS SSURGO), geology
27
+ - Contamination: Superfund, brownfields, USTs, EPA-regulated facilities (RCRA, GHG, FRS)
28
+ - Water: streams, watershed, water quality impairments (ATTAINS), NPDES outfalls, groundwater
29
+ - Hazards: seismic design (ASCE 7-22), earthquakes, wildfires, landslides, coastal vulnerability, NRI
30
+ - Rainfall: NOAA Atlas 14 precipitation frequency data (IDF curves, design storms)
31
+ - Land cover: NLCD 2021 classification (developed, forest, wetlands, etc.)
32
+ - Elevation: USGS 3DEP (min/max/mean elevation, relief)
33
+ - Infrastructure: hospitals, fire stations, schools, EMS, dams, levees, power plants, airports, railroads, bridges
34
+ - Ecology: species (GBIF), fish habitat, critical habitat, cropland, national forests, BLM lands, historic places
35
+ - Energy: solar potential, utility rates, EV charging stations
36
+ - Demographics: Census ACS (population, income, housing, poverty, vacancy rates)
37
+ - Protected lands (PAD-US), wild & scenic rivers, sole source aquifers
38
+
39
+ HOW TO PRESENT RESULTS:
40
+ When you receive completed data, organize your response using these sections (skip sections with no data):
41
+
42
+ 1. **Site Overview** Location, area, county, state, elevation range, land cover
43
+ 2. **Environmental Constraints** Flood zones, wetlands, soils (drainage class, hydrologic group), impaired waters, critical habitat. Flag any development constraints.
44
+ 3. **Contamination & Regulated Sites** — Superfund, brownfields, USTs, NPDES, EPA-regulated facilities within the search radius. Include distances and directions.
45
+ 4. **Water Resources** — Streams, watershed (HUC), water quality stations, groundwater wells, Atlas 14 rainfall (cite 2yr/10yr/100yr 24-hour depths).
46
+ 5. **Natural Hazards** Flood risk (FEMA zones), seismic design values, NRI risk index, earthquake history, landslide susceptibility, wildfire history.
47
+ 6. **Infrastructure & Services** Nearby hospitals, fire stations, schools, EMS, dams, airports, bridges. Include counts and distances.
48
+ 7. **Demographics & Energy** Population, median income, housing, solar potential, utility rates.
49
+ 8. **Key Findings & Considerations** — Summarize the most important findings. Highlight anything that would affect site development, permitting, or engineering design.
50
+
51
+ FORMATTING RULES:
52
+ - Use markdown tables for structured data (flood zones, soils, nearby facilities)
53
+ - Bold key values and findings that matter most
54
+ - Always cite the source agency for each data point (e.g., "FEMA NFHL", "NRCS SSURGO", "NOAA Atlas 14")
55
+ - Include distances and cardinal directions for nearby features (e.g., "0.8 miles NW")
56
+ - For Atlas 14 rainfall, present as a compact table: return period × duration
57
+ - For soils, include drainage class and hydrologic soil group — these drive stormwater design
58
+ - Note when a data source was queried but returned no data ("_noData: true") — this is informative (e.g., no Superfund sites nearby is good news)
59
+ - End with a disclaimer: "Data sourced from US federal agencies via GeoTap. Verify critical findings against authoritative sources before making engineering or regulatory decisions."
68
60
 
69
61
  IMPORTANT:
70
- - All data from authoritative US federal sources. Always cite the source agency.
71
- - Responses include _summary with plain-English descriptions — use these in answers.
62
+ - All data from authoritative US federal sources. Always cite the source agency, not just "GeoTap."
72
63
  - Data is for informational purposes. Remind users to verify for engineering/regulatory decisions.
73
64
  - Coordinates must be within the United States (including territories).
74
- - API key required. Users can get a free key at https://geotapdata.com/developers (one-click signup).`
65
+ - If a data source returns "_noData: true", it was queried but found nothing at that location — mention this where relevant (no contamination nearby is positive).
66
+ - If a data source returns "_error", note that the source was unavailable and the user should check back.`
75
67
  });
76
68
 
77
- // ── Shared tool call handler ────────────────────────────────────────
69
+ // ── Tool: collect_site_data ──────────────────────────────────────────
78
70
 
79
- /**
80
- * Handle a tool call through the standard pipeline:
81
- * normalize convertLatLng callApi capResponse generateSummary checkDataQuality enrich
82
- *
83
- * @param {string} legacyToolName - The original tool name (for routing summaries/sources)
84
- * @param {string} endpoint - API endpoint path
85
- * @param {string} method - HTTP method (GET/POST)
86
- * @param {object} params - Parameters from the LLM (after action extraction)
87
- * @returns {object} MCP tool response
88
- */
89
- async function handleToolCall(legacyToolName, endpoint, method, params) {
90
- try {
91
- // Fix #1: Normalize coordinate params to what backend expects
92
- const normalized = normalizeParams(legacyToolName, params);
71
+ server.tool(
72
+ 'collect_site_data',
73
+ `Collect comprehensive environmental data from ALL 80+ federal data sources for a site. Accepts an address, lat/lng coordinates, or a GeoJSON geometry (Point or Polygon). Returns a jobId — poll with get_results until complete (60-120 seconds).
93
74
 
94
- // Convert lat/lng to GeoJSON if needed (existing feature)
95
- const convertedParams = convertLatLng(legacyToolName, normalized);
75
+ Data returned covers: flood zones, wetlands, soils, geology, contamination sites, water quality, seismic risk, rainfall, infrastructure, ecology, energy, demographics, and much more.`,
76
+ {
77
+ address: z.string().optional().describe('US street address (e.g., "123 Main St, Houston TX"). If provided, the site is geocoded automatically. Use this OR lat/lng OR geometry.'),
78
+ lat: z.number().optional().describe('Latitude of the site (e.g., 34.8441). Use with lng.'),
79
+ lng: z.number().optional().describe('Longitude of the site (e.g., -82.4010). Use with lat.'),
80
+ geometry: z.any().optional().describe('GeoJSON geometry (Point or Polygon). For advanced use — most users should use address or lat/lng instead.'),
81
+ bufferAcres: z.number().optional().describe('Site area in acres when using a point location. Creates a circular buffer. Default: 1 acre. Range: 0.1–640.'),
82
+ searchRadiusMiles: z.number().optional().describe('How far to search for nearby features (contamination, infrastructure, etc.). Default: 3 miles. Range: 0.5–10.'),
83
+ },
84
+ async (params) => {
85
+ try {
86
+ const { address, lat, lng, geometry, bufferAcres, searchRadiusMiles } = params;
96
87
 
97
- // Strip internal fields before sending to API
98
- const apiParams = { ...convertedParams };
99
- delete apiParams._latLngConverted;
88
+ // Build the geometry from whatever input was provided
89
+ let siteGeometry = geometry;
100
90
 
101
- const rawResult = await callApi(endpoint, method, apiParams);
91
+ if (address && !siteGeometry) {
92
+ // Geocode the address first
93
+ const geocodeResult = await callApi('/geocode', 'GET', { address });
94
+ // API returns { success, results: [{ lat, lon, displayName, source }] }
95
+ const match = geocodeResult?.results?.[0];
96
+ if (!match?.lat || !match?.lon) {
97
+ return {
98
+ content: [{ type: 'text', text: JSON.stringify({
99
+ error: true,
100
+ message: `Could not geocode address: "${address}". Try a more specific address with city/state, or use lat/lng coordinates directly.`,
101
+ suggestion: 'Example: { lat: 34.8779, lng: -82.3313 }',
102
+ }, null, 2) }],
103
+ isError: true
104
+ };
105
+ }
106
+ siteGeometry = {
107
+ type: 'Point',
108
+ coordinates: [match.lon, match.lat]
109
+ };
110
+ } else if (lat != null && lng != null && !siteGeometry) {
111
+ siteGeometry = {
112
+ type: 'Point',
113
+ coordinates: [lng, lat]
114
+ };
115
+ }
102
116
 
103
- // Cap response size
104
- const { data: cappedResult, wasCapped, capInfo } = capResponse(legacyToolName, rawResult);
117
+ if (!siteGeometry) {
118
+ return {
119
+ content: [{ type: 'text', text: JSON.stringify({
120
+ error: true,
121
+ message: 'Provide a site location: address, lat/lng, or geometry.',
122
+ examples: [
123
+ { address: '123 Main St, Houston TX' },
124
+ { lat: 34.8441, lng: -82.4010 },
125
+ { geometry: { type: 'Point', coordinates: [-82.4010, 34.8441] } }
126
+ ]
127
+ }, null, 2) }],
128
+ isError: true
129
+ };
130
+ }
105
131
 
106
- // Generate natural language summary
107
- const summary = generateSummary(legacyToolName, params, cappedResult);
132
+ // Start the data collection job
133
+ const body = { geometry: siteGeometry };
134
+ if (bufferAcres != null) body.bufferAcres = bufferAcres;
135
+ if (searchRadiusMiles != null) body.searchRadiusMiles = searchRadiusMiles;
108
136
 
109
- // Fix #5: Check data quality and add warnings
110
- const dataQuality = checkDataQuality(legacyToolName, cappedResult);
137
+ const result = await callApi('/site-analysis/data-collect', 'POST', body);
111
138
 
112
- // Enrich response with source attribution, summary, and metadata
113
- const sources = toolSources[legacyToolName] || [];
114
- const enriched = {
115
- ...(summary ? { _summary: summary } : {}),
116
- ...cappedResult,
117
- ...(convertedParams._latLngConverted ? { _latLngConverted: convertedParams._latLngConverted } : {}),
118
- ...(wasCapped ? { _responseCapped: capInfo } : {}),
119
- ...(dataQuality ? { _dataQuality: dataQuality } : {}),
120
- _meta: {
121
- sources,
122
- retrievedAt: new Date().toISOString(),
123
- disclaimer: 'Data sourced from US federal agencies via GeoTap. Always verify critical data against authoritative sources before making engineering or regulatory decisions.',
139
+ return {
140
+ content: [{ type: 'text', text: JSON.stringify({
141
+ ...result,
142
+ _instructions: 'Job started. Poll get_results with this jobId every 10 seconds until status is "completed". Data collection queries 80+ federal sources and takes 60-120 seconds.',
143
+ }, null, 2) }]
144
+ };
145
+ } catch (error) {
146
+ if (error instanceof StructuredApiError) {
147
+ return {
148
+ content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
149
+ isError: true
150
+ };
124
151
  }
125
- };
126
-
127
- return {
128
- content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }]
129
- };
130
- } catch (error) {
131
- if (error instanceof StructuredApiError) {
132
152
  return {
133
- content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
153
+ content: [{ type: 'text', text: JSON.stringify({ error: true, message: error.message }, null, 2) }],
134
154
  isError: true
135
155
  };
136
156
  }
137
-
138
- return {
139
- content: [{ type: 'text', text: JSON.stringify({
140
- error: true,
141
- message: error.message,
142
- fix: ['Check that all required parameters are provided and valid.', 'Use discover_tools or check the tool description for available actions.'],
143
- relatedTools: ['discover_tools', 'check_status']
144
- }, null, 2) }],
145
- isError: true
146
- };
147
- }
148
- }
149
-
150
- // ── Fix #5: Data quality warnings ───────────────────────────────────
151
-
152
- /**
153
- * Check API response for signs of degraded or incomplete data.
154
- * Returns a _dataQuality object with warnings, or null if no issues detected.
155
- */
156
- function checkDataQuality(toolName, result) {
157
- if (!result || typeof result !== 'object') return null;
158
-
159
- const warnings = [];
160
-
161
- // Check for empty layer results in spatial queries
162
- if (result.layers && typeof result.layers === 'object') {
163
- const emptyLayers = [];
164
- const populatedLayers = [];
165
- for (const [name, data] of Object.entries(result.layers)) {
166
- if (data?.features?.length === 0 || data?.featureCount === 0) {
167
- emptyLayers.push(name);
168
- } else if (data?.features?.length > 0) {
169
- populatedLayers.push(name);
170
- }
171
- }
172
- if (emptyLayers.length > 0 && populatedLayers.length > 0) {
173
- // Only warn if some layers returned data but others didn't
174
- // (if ALL are empty, that's a valid "nothing here" result)
175
- warnings.push({
176
- type: 'partial_results',
177
- 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.`,
178
- layers: emptyLayers,
179
- });
180
- }
181
- }
182
-
183
- // Check for empty watershed geometry (the 82% failure pattern)
184
- if (toolName === 'delineate_watershed' || toolName === 'get_watershed') {
185
- const geom = result.data?.geometry || result.geometry || result.boundary?.geometry;
186
- if (geom && geom.coordinates && geom.coordinates.length === 0) {
187
- warnings.push({
188
- type: 'empty_geometry',
189
- message: 'Watershed delineation returned empty geometry. USGS StreamStats may not have coverage for this location. Try a nearby point on a mapped stream.',
190
- severity: 'error',
191
- });
192
- }
193
- if (result.data?.characteristics?.drainageArea === null || result.data?.characteristics?.drainageArea === undefined) {
194
- if (result.data?.characteristics) {
195
- warnings.push({
196
- type: 'missing_field',
197
- message: 'Drainage area not returned by StreamStats. This basin characteristic may not be available for this region.',
198
- field: 'drainageArea',
199
- });
200
- }
201
- }
202
- }
203
-
204
- // Check for null key fields in gage data
205
- if (toolName.startsWith('get_gage') || toolName.startsWith('get_flood') || toolName.startsWith('get_flow') || toolName.startsWith('get_storm')) {
206
- if (result.drainageArea === null || result.drainageArea === undefined) {
207
- if (result.siteName || result.siteId) {
208
- warnings.push({
209
- type: 'missing_field',
210
- message: 'Drainage area is null for this gage. USGS NWIS may not have this metadata. Cross-reference with StreamStats if drainage area is needed.',
211
- field: 'drainageArea',
212
- });
213
- }
214
- }
215
157
  }
158
+ );
216
159
 
217
- // Check for stale NLCD data
218
- if (toolName === 'export_land_use' || toolName === 'analyze_curve_numbers') {
219
- warnings.push({
220
- type: 'data_currency',
221
- message: 'NLCD land cover data is from 2021 (latest available). For rapidly developing areas, ground-truth current conditions before relying on land use classifications.',
222
- severity: 'info',
223
- });
224
- }
225
-
226
- // Check for Atlas 14 metadata issues (Ohio River Basin bug)
227
- if (toolName === 'get_rainfall_data' || toolName === 'get_idf_curves') {
228
- const loc = result.location || result.metadata?.location;
229
- if (loc?.region && /ohio\s*river\s*basin/i.test(loc.region)) {
230
- // Check if the actual coordinates suggest a different region
231
- const lat = result.metadata?.lat || result.lat;
232
- if (lat && lat < 36) {
233
- warnings.push({
234
- type: 'metadata_mismatch',
235
- 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.',
236
- severity: 'info',
237
- });
238
- }
239
- }
240
- }
241
-
242
- return warnings.length > 0 ? { warnings, totalWarnings: warnings.length } : null;
243
- }
244
-
245
- // ── Register meta-tools ─────────────────────────────────────────────
160
+ // ── Tool: get_results ────────────────────────────────────────────────
246
161
 
247
162
  server.tool(
248
- 'discover_tools',
249
- useLegacyTools
250
- ? '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.'
251
- : '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.',
163
+ 'get_results',
164
+ `Check the status of a data collection job and retrieve results. Poll every 10 seconds until status is "completed". When complete, returns the full data summary from all 80+ federal sources. Present the results to the user following the formatting instructions in the server description.`,
252
165
  {
253
- 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")'),
254
- maxResults: z.number().optional().describe('Maximum tools to return (default: 5)')
166
+ jobId: z.string().describe('Job ID returned from collect_site_data'),
255
167
  },
256
168
  async (params) => {
257
- const result = discoverTools(params.question, params.maxResults || 5);
258
- return {
259
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
260
- };
169
+ try {
170
+ const result = await callApi(`/site-analysis/data-collect/${encodeURIComponent(params.jobId)}`, 'GET', {});
171
+
172
+ const response = { ...result };
173
+
174
+ // Add presentation guidance when results are complete
175
+ if (result.status === 'completed') {
176
+ response._meta = {
177
+ sources: '80+ US federal agencies (FEMA, USGS, NOAA, EPA, NRCS, USFWS, USACE, DOE, DOT, CDC, Census, and more)',
178
+ retrievedAt: new Date().toISOString(),
179
+ disclaimer: 'Data sourced from US federal agencies via GeoTap. Always verify critical data against authoritative sources before making engineering or regulatory decisions.',
180
+ };
181
+ response._presentationGuide = {
182
+ instructions: 'Present these results to the user in a well-organized report. Follow the formatting instructions in the server description (HOW TO PRESENT RESULTS section).',
183
+ sections: [
184
+ 'Site Overview (location, area, elevation, land cover)',
185
+ 'Environmental Constraints (flood zones, wetlands, soils, impaired waters)',
186
+ 'Contamination & Regulated Sites (Superfund, brownfields, USTs, NPDES — with distances)',
187
+ 'Water Resources (streams, watershed, water quality, rainfall IDF data)',
188
+ 'Natural Hazards (flood risk, seismic, NRI, earthquakes, wildfires)',
189
+ 'Infrastructure & Services (hospitals, fire stations, schools, dams, bridges)',
190
+ 'Demographics & Energy (Census ACS, solar potential, utility rates)',
191
+ 'Key Findings & Considerations (summarize what matters most for this site)',
192
+ ],
193
+ tips: [
194
+ 'Use markdown tables for structured comparisons',
195
+ 'Bold the most critical findings (e.g., site is in a FEMA AE flood zone)',
196
+ 'Cite source agencies, not just GeoTap',
197
+ 'Mention when sources returned no data — "no Superfund sites" is useful info',
198
+ 'End with the disclaimer from _meta',
199
+ ]
200
+ };
201
+ } else {
202
+ response._instructions = `Job status: ${result.status}. Poll again in 10 seconds until status is "completed".`;
203
+ }
204
+
205
+ return {
206
+ content: [{ type: 'text', text: JSON.stringify(response, null, 2) }]
207
+ };
208
+ } catch (error) {
209
+ if (error instanceof StructuredApiError) {
210
+ return {
211
+ content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
212
+ isError: true
213
+ };
214
+ }
215
+ return {
216
+ content: [{ type: 'text', text: JSON.stringify({ error: true, message: error.message }, null, 2) }],
217
+ isError: true
218
+ };
219
+ }
261
220
  }
262
221
  );
263
222
 
223
+ // ── Tool: get_llms_txt (meta) ────────────────────────────────────────
224
+
264
225
  server.tool(
265
226
  'get_llms_txt',
266
- '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.',
227
+ 'Get the GeoTap API discovery document. Returns a structured description of all data sources and capabilities.',
267
228
  {},
268
229
  async () => {
269
230
  try {
@@ -271,67 +232,14 @@ server.tool(
271
232
  return { content: [{ type: 'text', text: content }] };
272
233
  } catch {
273
234
  return {
274
- content: [{ type: 'text', text: 'llms.txt not found. Visit https://geotapdata.com/llms.txt for API documentation.' }],
235
+ content: [{ type: 'text', text: 'llms.txt not found. Visit https://geotapdata.com/llms.txt for documentation.' }],
275
236
  isError: true
276
237
  };
277
238
  }
278
239
  }
279
240
  );
280
241
 
281
- // ── Register tools based on mode ────────────────────────────────────
282
-
283
- if (useLegacyTools) {
284
- // Legacy mode: register all 85 individual tools (for existing consumers)
285
- for (const tool of tools) {
286
- server.tool(
287
- tool.name,
288
- tool.description,
289
- tool.parameters,
290
- async (params) => handleToolCall(tool.name, tool.endpoint, tool.method, params)
291
- );
292
- }
293
- console.error(`[geotap] Legacy mode: registered ${tools.length} individual tools`);
294
- } else {
295
- // Default: register 12 consolidated tools
296
- for (const ctool of consolidatedTools) {
297
- server.tool(
298
- ctool.name,
299
- ctool.description,
300
- ctool.parameters,
301
- async (params) => {
302
- const { action, ...restParams } = params;
303
-
304
- // Resolve consolidated action → legacy tool
305
- const legacyName = ctool._actionMap[action];
306
- if (!legacyName) {
307
- return {
308
- content: [{ type: 'text', text: JSON.stringify({
309
- error: true,
310
- message: `Unknown action "${action}" for tool "${ctool.name}".`,
311
- validActions: Object.keys(ctool._actionMap),
312
- fix: [`Use one of: ${Object.keys(ctool._actionMap).join(', ')}`],
313
- }, null, 2) }],
314
- isError: true
315
- };
316
- }
317
-
318
- const legacyTool = legacyToolMap.get(legacyName);
319
- if (!legacyTool) {
320
- return {
321
- content: [{ type: 'text', text: JSON.stringify({
322
- error: true,
323
- message: `Internal routing error: legacy tool "${legacyName}" not found.`,
324
- }, null, 2) }],
325
- isError: true
326
- };
327
- }
328
-
329
- return handleToolCall(legacyName, legacyTool.endpoint, legacyTool.method, restParams);
330
- }
331
- );
332
- }
333
- console.error(`[geotap] Consolidated mode: registered ${consolidatedTools.length} tools (set GEOTAP_LEGACY_TOOLS=true for 85 individual tools)`);
334
- }
242
+ console.error(`[geotap] v3.0.0 2 tools (collect_site_data, get_results) + 1 meta-tool (get_llms_txt)`);
335
243
 
336
244
  // Start server
337
245
  const transport = new StdioServerTransport();