@driftwest/mcp-server 1.1.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/index.js ADDED
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DriftWest MCP Server
4
+ *
5
+ * Provides 21 environmental datasets to AI agents via Model Context Protocol.
6
+ * Connects to the DriftWest.XYZ API for real-time EMF sensor data, space weather,
7
+ * coastal environmental indices for 355 beaches, wildfire monitoring, air quality,
8
+ * and seismic activity.
9
+ *
10
+ * Usage with Claude Desktop:
11
+ * Add to claude_desktop_config.json:
12
+ * {
13
+ * "mcpServers": {
14
+ * "driftwest": {
15
+ * "command": "npx",
16
+ * "args": ["@driftwest/mcp-server"],
17
+ * "env": { "DRIFTWEST_API_KEY": "dw_your_key" }
18
+ * }
19
+ * }
20
+ * }
21
+ */
22
+
23
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
24
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
25
+ const { z } = require('zod');
26
+
27
+ const BASE_URL = process.env.DRIFTWEST_BASE_URL || 'https://driftwest.xyz';
28
+ const API_KEY = process.env.DRIFTWEST_API_KEY || '';
29
+
30
+ async function apiCall(path, options = {}) {
31
+ const url = `${BASE_URL}${path}`;
32
+ const headers = { 'Content-Type': 'application/json' };
33
+ if (API_KEY) headers['X-API-Key'] = API_KEY;
34
+
35
+ const res = await fetch(url, { ...options, headers: { ...headers, ...options.headers } });
36
+ if (!res.ok) {
37
+ const text = await res.text();
38
+ throw new Error(`API error ${res.status}: ${text}`);
39
+ }
40
+ return res.json();
41
+ }
42
+
43
+ const server = new McpServer({
44
+ name: 'driftwest',
45
+ version: '1.0.0',
46
+ description: 'DriftWest Environmental Data — EMF sensors, space weather, coastal environment, wildfire monitoring, air quality, and seismic activity'
47
+ });
48
+
49
+ // ── EMF Sensor Tools ──
50
+
51
+ server.tool(
52
+ 'emf_latest',
53
+ 'Get the latest reading from an EMF sensor node. Available nodes: NodeAir1 (air EMF), NodeStem1 (plant stem mV), NodeRoot1 (plant root mV), NodeSoil1 (soil EMF), HouseMonitor-Temp (°C), HouseMonitor-Humidity (%), GOES-Xray (W/m²), GOES-Proton (particles/cm²/s/sr)',
54
+ { nodeId: z.string().describe('Sensor node ID, e.g. NodeAir1, GOES-Xray') },
55
+ async ({ nodeId }) => {
56
+ const data = await apiCall(`/api/data/latest/${encodeURIComponent(nodeId)}`);
57
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
58
+ }
59
+ );
60
+
61
+ server.tool(
62
+ 'emf_stats',
63
+ 'Get 24-hour statistics (min, max, mean, count) for all EMF sensor nodes',
64
+ {},
65
+ async () => {
66
+ const data = await apiCall('/api/stats/ranges');
67
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
68
+ }
69
+ );
70
+
71
+ server.tool(
72
+ 'emf_history',
73
+ 'Get historical time-series readings from a sensor node',
74
+ {
75
+ nodeId: z.string().describe('Sensor node ID'),
76
+ hours: z.number().optional().default(6).describe('Hours of history (default 6)')
77
+ },
78
+ async ({ nodeId, hours }) => {
79
+ const start = Date.now() - hours * 3600000;
80
+ const data = await apiCall(`/api/data/${encodeURIComponent(nodeId)}?start=${start}&limit=500`);
81
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
82
+ }
83
+ );
84
+
85
+ server.tool(
86
+ 'sensor_nodes',
87
+ 'List all sensor nodes with their current online/offline status',
88
+ {},
89
+ async () => {
90
+ const data = await apiCall('/api/nodes');
91
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
92
+ }
93
+ );
94
+
95
+ // ── Space Weather Tools ──
96
+
97
+ server.tool(
98
+ 'space_weather',
99
+ 'Get current space weather conditions: Kp/Dst indices, solar wind, GOES flux, composite disturbance score (0-100)',
100
+ {},
101
+ async () => {
102
+ const data = await apiCall('/api/datasets/space-weather/current');
103
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
104
+ }
105
+ );
106
+
107
+ server.tool(
108
+ 'space_weather_alerts',
109
+ 'Get active NOAA space weather alerts (geomagnetic storms, solar flares, radiation storms)',
110
+ {},
111
+ async () => {
112
+ const data = await apiCall('/api/datasets/space-weather/alerts');
113
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
114
+ }
115
+ );
116
+
117
+ // ── Beach & Coastal Tools ──
118
+
119
+ server.tool(
120
+ 'search_beaches',
121
+ 'Search for beaches by location, province, or water type. Returns profiles with water quality, biodiversity, recreation, and risk indices.',
122
+ {
123
+ lat: z.number().optional().describe('Latitude for proximity search'),
124
+ lon: z.number().optional().describe('Longitude for proximity search'),
125
+ radius: z.number().optional().default(50).describe('Search radius in km'),
126
+ province: z.string().optional().describe('Province filter: British Columbia, Alberta, Oregon'),
127
+ type: z.string().optional().describe('Water type: tidal, lake, river'),
128
+ limit: z.number().optional().default(20).describe('Max results')
129
+ },
130
+ async ({ lat, lon, radius, province, type, limit }) => {
131
+ const params = new URLSearchParams();
132
+ if (lat !== undefined) params.set('lat', lat);
133
+ if (lon !== undefined) params.set('lon', lon);
134
+ if (radius) params.set('radius', radius);
135
+ if (province) params.set('province', province);
136
+ if (type) params.set('type', type);
137
+ if (limit) params.set('limit', limit);
138
+ const data = await apiCall(`/api/datasets/coastal-atlas/search?${params}`);
139
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
140
+ }
141
+ );
142
+
143
+ server.tool(
144
+ 'beach_detail',
145
+ 'Get complete beach dossier: all indices, satellite data, community metrics, descriptions, and environmental analysis',
146
+ { id: z.string().describe('Beach ID') },
147
+ async ({ id }) => {
148
+ const data = await apiCall(`/api/datasets/coastal-atlas/beach/${encodeURIComponent(id)}`);
149
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
150
+ }
151
+ );
152
+
153
+ server.tool(
154
+ 'nearby_beaches',
155
+ 'Find beaches near a location sorted by recreation score. Great for trip planning.',
156
+ {
157
+ lat: z.number().describe('Latitude'),
158
+ lon: z.number().describe('Longitude'),
159
+ radius: z.number().optional().default(30).describe('Radius in km')
160
+ },
161
+ async ({ lat, lon, radius }) => {
162
+ const data = await apiCall(`/api/datasets/recreation/nearby?lat=${lat}&lon=${lon}&radius=${radius}`);
163
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
164
+ }
165
+ );
166
+
167
+ server.tool(
168
+ 'water_quality_rankings',
169
+ 'Get beaches ranked by water quality index (0-100). Filterable by province and water type.',
170
+ {
171
+ province: z.string().optional(),
172
+ type: z.string().optional().describe('tidal, lake, river'),
173
+ limit: z.number().optional().default(20)
174
+ },
175
+ async ({ province, type, limit }) => {
176
+ const params = new URLSearchParams();
177
+ if (province) params.set('province', province);
178
+ if (type) params.set('type', type);
179
+ if (limit) params.set('limit', limit);
180
+ const data = await apiCall(`/api/datasets/water-quality/rankings?${params}`);
181
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
182
+ }
183
+ );
184
+
185
+ server.tool(
186
+ 'water_quality_summary',
187
+ 'Aggregate water quality statistics by province and water type across all 355 beaches',
188
+ {},
189
+ async () => {
190
+ const data = await apiCall('/api/datasets/water-quality/summary');
191
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
192
+ }
193
+ );
194
+
195
+ server.tool(
196
+ 'biodiversity_hotspots',
197
+ 'Top biodiversity hotspots: beaches with highest fauna and flora richness',
198
+ {},
199
+ async () => {
200
+ const data = await apiCall('/api/datasets/biodiversity/hotspots');
201
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
202
+ }
203
+ );
204
+
205
+ server.tool(
206
+ 'shoreline_risk_alerts',
207
+ 'Active environmental risk alerts: beaches with high algae bloom risk, erosion, or human pressure',
208
+ {},
209
+ async () => {
210
+ const data = await apiCall('/api/datasets/shoreline-risk/alerts');
211
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
212
+ }
213
+ );
214
+
215
+ server.tool(
216
+ 'compare_beaches',
217
+ 'Compare up to 10 beaches side-by-side across all indices with automatic winner identification',
218
+ { ids: z.string().describe('Comma-separated beach IDs, e.g. "beach1,beach2,beach3"') },
219
+ async ({ ids }) => {
220
+ const data = await apiCall(`/api/datasets/beach-compare?ids=${encodeURIComponent(ids)}`);
221
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
222
+ }
223
+ );
224
+
225
+ server.tool(
226
+ 'algae_risk',
227
+ 'Algae bloom risk analysis: high/medium/low breakdown with chlorophyll and cyanobacteria data',
228
+ {
229
+ province: z.string().optional(),
230
+ type: z.string().optional().describe('tidal, lake, river')
231
+ },
232
+ async ({ province, type }) => {
233
+ const params = new URLSearchParams();
234
+ if (province) params.set('province', province);
235
+ if (type) params.set('type', type);
236
+ const data = await apiCall(`/api/datasets/water-trends/algae-risk?${params}`);
237
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
238
+ }
239
+ );
240
+
241
+ // ── Wildfire Tools ──
242
+
243
+ server.tool(
244
+ 'wildfire_current',
245
+ 'Get current Canadian wildfire conditions: active fire count, national danger rating (Low to Extreme), city fire proximity scores, and province breakdown',
246
+ {},
247
+ async () => {
248
+ const data = await apiCall('/api/datasets/wildfire/current');
249
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
250
+ }
251
+ );
252
+
253
+ server.tool(
254
+ 'wildfire_hotspots',
255
+ 'Get active fire hotspot locations with brightness, fire radiative power, and confidence from NASA FIRMS satellite detection',
256
+ {
257
+ province: z.string().optional().describe('Province code: BC, AB, SK, MB, ON, QC'),
258
+ minConfidence: z.string().optional().describe('Filter: low, nominal, high'),
259
+ limit: z.number().optional().default(200)
260
+ },
261
+ async ({ province, minConfidence, limit }) => {
262
+ const params = new URLSearchParams();
263
+ if (province) params.set('province', province);
264
+ if (minConfidence) params.set('minConfidence', minConfidence);
265
+ if (limit) params.set('limit', limit);
266
+ const data = await apiCall(`/api/datasets/wildfire/hotspots?${params}`);
267
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
268
+ }
269
+ );
270
+
271
+ server.tool(
272
+ 'wildfire_smoke_impact',
273
+ 'Get per-city smoke impact with AQI, PM2.5, and fire proximity score for 10 Canadian cities',
274
+ {},
275
+ async () => {
276
+ const data = await apiCall('/api/datasets/wildfire/smoke-impact');
277
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
278
+ }
279
+ );
280
+
281
+ // ── Air Quality Tools ──
282
+
283
+ server.tool(
284
+ 'air_quality_current',
285
+ 'Get current air quality for 12 cities (Calgary, Edmonton, Vancouver, Toronto, Montreal, Ottawa, Winnipeg, Saskatoon, Victoria, Halifax, Portland, Seattle). Includes AQI, health risk score (0-100), and safety rating.',
286
+ {},
287
+ async () => {
288
+ const data = await apiCall('/api/datasets/air-quality/current');
289
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
290
+ }
291
+ );
292
+
293
+ server.tool(
294
+ 'air_quality_city',
295
+ 'Get detailed air quality for one city: full pollutant breakdown (PM2.5, O3, NO2, PM10, SO2, CO), UV index, health risk score, and recommendations',
296
+ { cityId: z.string().describe('City ID: calgary, edmonton, vancouver, toronto, montreal, ottawa, winnipeg, saskatoon, victoria, halifax, portland, seattle') },
297
+ async ({ cityId }) => {
298
+ const data = await apiCall(`/api/datasets/air-quality/city/${encodeURIComponent(cityId)}`);
299
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
300
+ }
301
+ );
302
+
303
+ server.tool(
304
+ 'air_quality_rankings',
305
+ 'Get cities ranked by health risk score (lowest = best air quality)',
306
+ {},
307
+ async () => {
308
+ const data = await apiCall('/api/datasets/air-quality/rankings');
309
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
310
+ }
311
+ );
312
+
313
+ server.tool(
314
+ 'air_quality_alerts',
315
+ 'Get cities currently exceeding AQI 100 (unhealthy for sensitive groups) with PM2.5 values and health recommendations',
316
+ {},
317
+ async () => {
318
+ const data = await apiCall('/api/datasets/air-quality/alerts');
319
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
320
+ }
321
+ );
322
+
323
+ // ── Seismic Tools ──
324
+
325
+ server.tool(
326
+ 'seismic_recent',
327
+ 'Get recent global earthquakes (M2.5+) from USGS sorted by time',
328
+ {
329
+ days: z.number().optional().default(1).describe('Days of history (max 30)'),
330
+ minMagnitude: z.number().optional().default(2.5),
331
+ limit: z.number().optional().default(100)
332
+ },
333
+ async ({ days, minMagnitude, limit }) => {
334
+ const params = new URLSearchParams();
335
+ if (days) params.set('days', days);
336
+ if (minMagnitude) params.set('minMagnitude', minMagnitude);
337
+ if (limit) params.set('limit', limit);
338
+ const data = await apiCall(`/api/datasets/seismic/recent?${params}`);
339
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
340
+ }
341
+ );
342
+
343
+ server.tool(
344
+ 'seismic_canada',
345
+ 'Get Canadian earthquakes only (M1.0+) with comprehensive coverage',
346
+ {},
347
+ async () => {
348
+ const data = await apiCall('/api/datasets/seismic/canada');
349
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
350
+ }
351
+ );
352
+
353
+ server.tool(
354
+ 'seismic_activity_score',
355
+ 'Get seismic activity scores (0-100) for 5 Canadian risk zones: BC Coast (Cascadia Subduction), St. Lawrence, Ottawa-Gatineau, Alberta (induced seismicity), Arctic. Includes population exposure analysis.',
356
+ {},
357
+ async () => {
358
+ const data = await apiCall('/api/datasets/seismic/activity-score');
359
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
360
+ }
361
+ );
362
+
363
+ server.tool(
364
+ 'seismic_nearby',
365
+ 'Find earthquakes near a geographic point sorted by distance',
366
+ {
367
+ lat: z.number().describe('Latitude'),
368
+ lon: z.number().describe('Longitude'),
369
+ radius: z.number().optional().default(200).describe('Search radius in km (max 1000)')
370
+ },
371
+ async ({ lat, lon, radius }) => {
372
+ const data = await apiCall(`/api/datasets/seismic/nearby?lat=${lat}&lon=${lon}&radius=${radius}`);
373
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
374
+ }
375
+ );
376
+
377
+ // ── Cross-Domain Tools ──
378
+
379
+ server.tool(
380
+ 'alberta_environment',
381
+ 'Alberta environmental dashboard: combines EMF sensors, water quality for 56 water bodies, space weather, and algae risk',
382
+ {},
383
+ async () => {
384
+ const data = await apiCall('/api/datasets/alberta-environment/current');
385
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
386
+ }
387
+ );
388
+
389
+ // ── Agent Decision Support Tools ──
390
+
391
+ server.tool(
392
+ 'safety_check',
393
+ 'Composite environmental safety assessment for a location. Returns go/no-go verdict combining air quality, wildfire proximity, and seismic activity. Designed for autonomous agents making location-dependent decisions.',
394
+ {
395
+ lat: z.number().describe('Latitude of location to assess'),
396
+ lon: z.number().describe('Longitude of location to assess')
397
+ },
398
+ async ({ lat, lon }) => {
399
+ const data = await apiCall(`/api/datasets/safety-check?lat=${lat}&lon=${lon}`);
400
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
401
+ }
402
+ );
403
+
404
+ // ── Resources ──
405
+
406
+ server.resource(
407
+ 'emf-overview',
408
+ 'driftwest://emf/overview',
409
+ { description: 'Overview of all EMF sensor nodes and their current readings', mimeType: 'application/json' },
410
+ async () => {
411
+ const [nodes, stats] = await Promise.all([
412
+ apiCall('/api/nodes'),
413
+ apiCall('/api/stats/ranges')
414
+ ]);
415
+ return {
416
+ contents: [{
417
+ uri: 'driftwest://emf/overview',
418
+ text: JSON.stringify({ nodes: nodes.nodes, stats24h: stats.ranges }, null, 2),
419
+ mimeType: 'application/json'
420
+ }]
421
+ };
422
+ }
423
+ );
424
+
425
+ server.resource(
426
+ 'dataset-catalog',
427
+ 'driftwest://datasets/catalog',
428
+ { description: 'Complete catalog of all 21 DriftWest datasets with descriptions and endpoints', mimeType: 'text/plain' },
429
+ async () => {
430
+ return {
431
+ contents: [{
432
+ uri: 'driftwest://datasets/catalog',
433
+ text: `DriftWest Dataset Catalog (21 datasets)
434
+
435
+ EMF & Space Weather (8):
436
+ prairie-emf-live — Real-time EMF from 3 ESP32 EMF nodes + 1 house monitor (Strathmore, AB)
437
+ space-weather-dashboard — GOES flux, Kp/Dst, solar wind, disturbance score
438
+ emf-weather-correlations — Daily EMF vs weather/lunar/seismic analysis
439
+ satellite-rf-exposure — RF power density from satellite passes
440
+ agricultural-emf-index — EMF for precision agriculture
441
+ emf-anomaly-stream — Z-score anomaly detection on EMF data
442
+ solar-flare-alerts — GOES X-ray classification
443
+ geomagnetic-storm-index — Kp tracking with G-scale classification
444
+
445
+ Environment Monitoring (3):
446
+ canadian-wildfire — NASA FIRMS fire hotspots, smoke impact, danger rating (15-min refresh)
447
+ air-quality-health — Respiratory health risk for 12 cities, 6 pollutants (15-min refresh)
448
+ seismic-monitor — USGS earthquakes, 5 Canadian risk zone scores (5-min refresh)
449
+
450
+ Cross-Domain (3):
451
+ alberta-environment — EMF + 56 Alberta water bodies + space weather
452
+ beach-compare — Side-by-side comparison of up to 10 beaches
453
+ water-trends — Provincial statistics, temperature, algae risk
454
+
455
+ Agent Decision Support (1):
456
+ safety-check — Composite go/no-go verdict combining air quality, wildfire proximity, and seismic activity
457
+
458
+ Coastal Environment (5, 355 beaches):
459
+ pnw-water-quality — Water Quality Index (0-100) from satellite data
460
+ coastal-biodiversity — Fauna/flora richness, habitat complexity
461
+ beach-recreation — Recreation score with spatial proximity search
462
+ shoreline-risk — Multi-factor environmental risk with alerts
463
+ bc-coastal-atlas — Geographic discovery API with all indices
464
+
465
+ Use tools like search_beaches, water_quality_rankings, space_weather, emf_latest,
466
+ wildfire_current, air_quality_current, seismic_recent to query data.
467
+ All dataset tools require an API key (set DRIFTWEST_API_KEY env var).
468
+ Free keys: POST https://driftwest.xyz/api/account/register with {"email":"..."}`,
469
+ mimeType: 'text/plain'
470
+ }]
471
+ };
472
+ }
473
+ );
474
+
475
+ // ── Start Server ──
476
+
477
+ async function main() {
478
+ const transport = new StdioServerTransport();
479
+ await server.connect(transport);
480
+ }
481
+
482
+ main().catch(err => {
483
+ console.error('MCP server error:', err);
484
+ process.exit(1);
485
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@driftwest/mcp-server",
3
+ "version": "1.1.1",
4
+ "description": "MCP server for DriftWest environmental data — 28 tools: EMF sensors, space weather, 355 beaches, wildfire monitoring, air quality, and seismic activity. Free tier + $29/mo researcher.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "driftwest-mcp": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "environmental-data",
16
+ "emf",
17
+ "space-weather",
18
+ "beaches",
19
+ "water-quality",
20
+ "wildfire",
21
+ "air-quality",
22
+ "earthquake",
23
+ "seismic",
24
+ "ai-agent",
25
+ "claude",
26
+ "ai-tools"
27
+ ],
28
+ "author": "DriftWest Labs <info@driftwest.xyz>",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/driftwest/mcp-server"
33
+ },
34
+ "homepage": "https://driftwest.xyz",
35
+ "mcpName": "driftwest/mcp-server",
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.0.0"
41
+ }
42
+ }