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 +21 -0
- package/README.md +2 -2
- package/package.json +4 -3
- package/src/api.js +1 -1
- package/src/index.js +184 -276
- package/src/consolidatedTools.js +0 -602
- package/src/discoverTools.js +0 -211
- package/src/latLngHelper.js +0 -137
- package/src/paramNormalize.js +0 -182
- package/src/responseCap.js +0 -233
- package/src/sources.js +0 -199
- package/src/summaries.js +0 -396
- package/src/tools.js +0 -1222
- package/tests/Spec_Comprehensive_Test_Suite.md +0 -1203
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 **
|
|
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 (
|
|
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": "
|
|
4
|
-
"description": "MCP server for GeoTap —
|
|
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/
|
|
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: '
|
|
28
|
-
description: '
|
|
29
|
-
instructions:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
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
|
-
-
|
|
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
|
-
// ──
|
|
69
|
+
// ── Tool: collect_site_data ──────────────────────────────────────────
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
delete apiParams._latLngConverted;
|
|
88
|
+
// Build the geometry from whatever input was provided
|
|
89
|
+
let siteGeometry = geometry;
|
|
100
90
|
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
const dataQuality = checkDataQuality(legacyToolName, cappedResult);
|
|
137
|
+
const result = await callApi('/site-analysis/data-collect', 'POST', body);
|
|
111
138
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
'
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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();
|