geotap-mcp-server 2.2.1 → 3.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/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.0",
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,174 @@
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
31
+ - Infrastructure: hospitals, fire stations, schools, EMS, dams, levees, power plants, airports, railroads, bridges
32
+ - Ecology: species (GBIF), fish habitat, critical habitat, cropland, national forests, BLM lands, historic places
33
+ - Energy: solar potential, utility rates, EV charging stations
34
+ - Demographics: Census ACS (population, income, housing, poverty)
35
+ - Protected lands (PAD-US), wild & scenic rivers, sole source aquifers
68
36
 
69
37
  IMPORTANT:
70
38
  - All data from authoritative US federal sources. Always cite the source agency.
71
- - Responses include _summary with plain-English descriptions — use these in answers.
72
39
  - Data is for informational purposes. Remind users to verify for engineering/regulatory decisions.
73
40
  - 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).`
41
+ - If a data source returns "_noData: true", it was queried but found nothing at that location.`
75
42
  });
76
43
 
77
- // ── Shared tool call handler ────────────────────────────────────────
44
+ // ── Tool: collect_site_data ──────────────────────────────────────────
78
45
 
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);
46
+ server.tool(
47
+ 'collect_site_data',
48
+ `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
49
 
94
- // Convert lat/lng to GeoJSON if needed (existing feature)
95
- const convertedParams = convertLatLng(legacyToolName, normalized);
50
+ Data returned covers: flood zones, wetlands, soils, geology, contamination sites, water quality, seismic risk, rainfall, infrastructure, ecology, energy, demographics, and much more.`,
51
+ {
52
+ 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.'),
53
+ lat: z.number().optional().describe('Latitude of the site (e.g., 34.8441). Use with lng.'),
54
+ lng: z.number().optional().describe('Longitude of the site (e.g., -82.4010). Use with lat.'),
55
+ geometry: z.any().optional().describe('GeoJSON geometry (Point or Polygon). For advanced use — most users should use address or lat/lng instead.'),
56
+ 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.'),
57
+ searchRadiusMiles: z.number().optional().describe('How far to search for nearby features (contamination, infrastructure, etc.). Default: 3 miles. Range: 0.5–10.'),
58
+ },
59
+ async (params) => {
60
+ try {
61
+ const { address, lat, lng, geometry, bufferAcres, searchRadiusMiles } = params;
96
62
 
97
- // Strip internal fields before sending to API
98
- const apiParams = { ...convertedParams };
99
- delete apiParams._latLngConverted;
63
+ // Build the geometry from whatever input was provided
64
+ let siteGeometry = geometry;
100
65
 
101
- const rawResult = await callApi(endpoint, method, apiParams);
66
+ if (address && !siteGeometry) {
67
+ // Geocode the address first
68
+ const geocodeResult = await callApi('/geocode', 'GET', { address });
69
+ if (!geocodeResult?.lat || !geocodeResult?.lng) {
70
+ return {
71
+ content: [{ type: 'text', text: JSON.stringify({
72
+ error: true,
73
+ message: `Could not geocode address: "${address}". Try a more specific address or use lat/lng coordinates.`,
74
+ }, null, 2) }],
75
+ isError: true
76
+ };
77
+ }
78
+ siteGeometry = {
79
+ type: 'Point',
80
+ coordinates: [geocodeResult.lng, geocodeResult.lat]
81
+ };
82
+ } else if (lat != null && lng != null && !siteGeometry) {
83
+ siteGeometry = {
84
+ type: 'Point',
85
+ coordinates: [lng, lat]
86
+ };
87
+ }
102
88
 
103
- // Cap response size
104
- const { data: cappedResult, wasCapped, capInfo } = capResponse(legacyToolName, rawResult);
89
+ if (!siteGeometry) {
90
+ return {
91
+ content: [{ type: 'text', text: JSON.stringify({
92
+ error: true,
93
+ message: 'Provide a site location: address, lat/lng, or geometry.',
94
+ examples: [
95
+ { address: '123 Main St, Houston TX' },
96
+ { lat: 34.8441, lng: -82.4010 },
97
+ { geometry: { type: 'Point', coordinates: [-82.4010, 34.8441] } }
98
+ ]
99
+ }, null, 2) }],
100
+ isError: true
101
+ };
102
+ }
105
103
 
106
- // Generate natural language summary
107
- const summary = generateSummary(legacyToolName, params, cappedResult);
104
+ // Start the data collection job
105
+ const body = { geometry: siteGeometry };
106
+ if (bufferAcres != null) body.bufferAcres = bufferAcres;
107
+ if (searchRadiusMiles != null) body.searchRadiusMiles = searchRadiusMiles;
108
108
 
109
- // Fix #5: Check data quality and add warnings
110
- const dataQuality = checkDataQuality(legacyToolName, cappedResult);
109
+ const result = await callApi('/site-analysis/data-collect', 'POST', body);
111
110
 
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.',
111
+ return {
112
+ content: [{ type: 'text', text: JSON.stringify({
113
+ ...result,
114
+ _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.',
115
+ }, null, 2) }]
116
+ };
117
+ } catch (error) {
118
+ if (error instanceof StructuredApiError) {
119
+ return {
120
+ content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
121
+ isError: true
122
+ };
124
123
  }
125
- };
126
-
127
- return {
128
- content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }]
129
- };
130
- } catch (error) {
131
- if (error instanceof StructuredApiError) {
132
124
  return {
133
- content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
125
+ content: [{ type: 'text', text: JSON.stringify({ error: true, message: error.message }, null, 2) }],
134
126
  isError: true
135
127
  };
136
128
  }
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
- }
216
-
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
129
  }
130
+ );
241
131
 
242
- return warnings.length > 0 ? { warnings, totalWarnings: warnings.length } : null;
243
- }
244
-
245
- // ── Register meta-tools ─────────────────────────────────────────────
132
+ // ── Tool: get_results ────────────────────────────────────────────────
246
133
 
247
134
  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.',
135
+ 'get_results',
136
+ `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.`,
252
137
  {
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)')
138
+ jobId: z.string().describe('Job ID returned from collect_site_data'),
255
139
  },
256
140
  async (params) => {
257
- const result = discoverTools(params.question, params.maxResults || 5);
258
- return {
259
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
260
- };
141
+ try {
142
+ const result = await callApi(`/site-analysis/data-collect/${encodeURIComponent(params.jobId)}`, 'GET', {});
143
+
144
+ return {
145
+ content: [{ type: 'text', text: JSON.stringify({
146
+ ...result,
147
+ _meta: {
148
+ sources: '80+ US federal agencies (FEMA, USGS, NOAA, EPA, NRCS, USFWS, USACE, DOE, DOT, CDC, Census, and more)',
149
+ retrievedAt: new Date().toISOString(),
150
+ disclaimer: 'Data sourced from US federal agencies via GeoTap. Always verify critical data against authoritative sources before making engineering or regulatory decisions.',
151
+ }
152
+ }, null, 2) }]
153
+ };
154
+ } catch (error) {
155
+ if (error instanceof StructuredApiError) {
156
+ return {
157
+ content: [{ type: 'text', text: JSON.stringify(error.details, null, 2) }],
158
+ isError: true
159
+ };
160
+ }
161
+ return {
162
+ content: [{ type: 'text', text: JSON.stringify({ error: true, message: error.message }, null, 2) }],
163
+ isError: true
164
+ };
165
+ }
261
166
  }
262
167
  );
263
168
 
169
+ // ── Tool: get_llms_txt (meta) ────────────────────────────────────────
170
+
264
171
  server.tool(
265
172
  '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.',
173
+ 'Get the GeoTap API discovery document. Returns a structured description of all data sources and capabilities.',
267
174
  {},
268
175
  async () => {
269
176
  try {
@@ -271,67 +178,14 @@ server.tool(
271
178
  return { content: [{ type: 'text', text: content }] };
272
179
  } catch {
273
180
  return {
274
- content: [{ type: 'text', text: 'llms.txt not found. Visit https://geotapdata.com/llms.txt for API documentation.' }],
181
+ content: [{ type: 'text', text: 'llms.txt not found. Visit https://geotapdata.com/llms.txt for documentation.' }],
275
182
  isError: true
276
183
  };
277
184
  }
278
185
  }
279
186
  );
280
187
 
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
- }
188
+ console.error(`[geotap] v3.0.0 2 tools (collect_site_data, get_results) + 1 meta-tool (get_llms_txt)`);
335
189
 
336
190
  // Start server
337
191
  const transport = new StdioServerTransport();