mcp-server-environment-agency 1.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/src/index.ts ADDED
@@ -0,0 +1,1029 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ErrorCode,
8
+ ListToolsRequestSchema,
9
+ McpError,
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+
12
+ // Types for Environment Agency API responses
13
+ interface FloodWarning {
14
+ "@id": string;
15
+ description: string;
16
+ eaAreaName: string;
17
+ eaRegionName: string;
18
+ floodArea: FloodArea;
19
+ floodAreaID: string;
20
+ isTidal: boolean;
21
+ message?: string;
22
+ severity: string;
23
+ severityLevel: number;
24
+ timeMessageChanged: string;
25
+ timeRaised: string;
26
+ timeSeverityChanged: string;
27
+ }
28
+
29
+ interface FloodArea {
30
+ "@id": string;
31
+ county: string;
32
+ notation: string;
33
+ polygon: string;
34
+ riverOrSea?: string;
35
+ description?: string;
36
+ eaAreaName?: string;
37
+ eaRegionName?: string;
38
+ lat?: number;
39
+ long?: number;
40
+ quickDialNumber?: string;
41
+ }
42
+
43
+ interface Station {
44
+ "@id": string;
45
+ RLOIid?: string;
46
+ catchmentName?: string;
47
+ dateOpened?: string;
48
+ datumOffset?: number;
49
+ label: string;
50
+ measures: Measure[];
51
+ notation: string;
52
+ riverName?: string;
53
+ stationReference: string;
54
+ town?: string;
55
+ wiskiID?: string;
56
+ lat?: number;
57
+ long?: number;
58
+ easting?: number;
59
+ northing?: number;
60
+ status?: string;
61
+ statusReason?: string;
62
+ statusDate?: string;
63
+ type?: string[];
64
+ stageScale?: Scale;
65
+ downstageScale?: Scale;
66
+ }
67
+
68
+ interface Measure {
69
+ "@id": string;
70
+ datumType?: string;
71
+ label: string;
72
+ latestReading?: Reading;
73
+ notation: string;
74
+ parameter: string;
75
+ parameterName: string;
76
+ period?: number;
77
+ qualifier?: string;
78
+ station: string;
79
+ stationReference: string;
80
+ unit?: string;
81
+ unitName?: string;
82
+ valueType?: string;
83
+ }
84
+
85
+ interface Reading {
86
+ "@id"?: string;
87
+ date: string;
88
+ dateTime: string;
89
+ measure: string | Measure;
90
+ value?: number;
91
+ }
92
+
93
+ interface Scale {
94
+ highestRecent?: Reading;
95
+ maxOnRecord?: Reading;
96
+ minOnRecord?: Reading;
97
+ scaleMax?: number;
98
+ typicalRangeHigh?: number;
99
+ typicalRangeLow?: number;
100
+ }
101
+
102
+ interface APIResponse<T> {
103
+ "@context": string;
104
+ meta: {
105
+ publisher: string;
106
+ licence: string;
107
+ documentation: string;
108
+ version: string;
109
+ comment?: string;
110
+ limit?: number;
111
+ offset?: number;
112
+ hasFormat?: string[];
113
+ };
114
+ items: T;
115
+ }
116
+
117
+ // Base API class
118
+ class EnvironmentAgencyAPI {
119
+ private baseUrl = "https://environment.data.gov.uk/flood-monitoring";
120
+
121
+ private async makeRequest<T>(endpoint: string): Promise<APIResponse<T>> {
122
+ const url = `${this.baseUrl}${endpoint}`;
123
+ const response = await fetch(url, {
124
+ headers: {
125
+ 'Accept': 'application/json',
126
+ 'User-Agent': 'MCP-Environment-Agency-Server/1.0.0',
127
+ },
128
+ });
129
+
130
+ if (!response.ok) {
131
+ if (response.status === 404) {
132
+ throw new McpError(ErrorCode.InvalidRequest, "Resource not found");
133
+ }
134
+ throw new McpError(ErrorCode.InternalError, `API request failed: ${response.statusText}`);
135
+ }
136
+
137
+ return response.json() as Promise<APIResponse<T>>;
138
+ }
139
+
140
+ // Flood warnings
141
+ async getFloodWarnings(params: {
142
+ minSeverity?: number;
143
+ county?: string;
144
+ lat?: number;
145
+ long?: number;
146
+ dist?: number;
147
+ } = {}): Promise<APIResponse<FloodWarning[]>> {
148
+ const searchParams = new URLSearchParams();
149
+
150
+ if (params.minSeverity) searchParams.set('min-severity', params.minSeverity.toString());
151
+ if (params.county) searchParams.set('county', params.county);
152
+ if (params.lat && params.long) {
153
+ searchParams.set('lat', params.lat.toString());
154
+ searchParams.set('long', params.long.toString());
155
+ if (params.dist) searchParams.set('dist', params.dist.toString());
156
+ }
157
+
158
+ const queryString = searchParams.toString();
159
+ return this.makeRequest(`/id/floods${queryString ? '?' + queryString : ''}`);
160
+ }
161
+
162
+ async getFloodWarning(id: string): Promise<APIResponse<FloodWarning>> {
163
+ return this.makeRequest(`/id/floods/${id}`);
164
+ }
165
+
166
+ // Flood areas
167
+ async getFloodAreas(params: {
168
+ lat?: number;
169
+ long?: number;
170
+ dist?: number;
171
+ limit?: number;
172
+ offset?: number;
173
+ } = {}): Promise<APIResponse<FloodArea[]>> {
174
+ const searchParams = new URLSearchParams();
175
+
176
+ if (params.lat && params.long) {
177
+ searchParams.set('lat', params.lat.toString());
178
+ searchParams.set('long', params.long.toString());
179
+ if (params.dist) searchParams.set('dist', params.dist.toString());
180
+ }
181
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
182
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
183
+
184
+ const queryString = searchParams.toString();
185
+ return this.makeRequest(`/id/floodAreas${queryString ? '?' + queryString : ''}`);
186
+ }
187
+
188
+ async getFloodArea(areaCode: string): Promise<APIResponse<FloodArea>> {
189
+ return this.makeRequest(`/id/floodAreas/${areaCode}`);
190
+ }
191
+
192
+ // Stations
193
+ async getStations(params: {
194
+ parameterName?: string;
195
+ parameter?: string;
196
+ qualifier?: string;
197
+ label?: string;
198
+ town?: string;
199
+ catchmentName?: string;
200
+ riverName?: string;
201
+ stationReference?: string;
202
+ RLOIid?: string;
203
+ search?: string;
204
+ lat?: number;
205
+ long?: number;
206
+ dist?: number;
207
+ type?: string;
208
+ status?: string;
209
+ view?: 'full';
210
+ limit?: number;
211
+ offset?: number;
212
+ } = {}): Promise<APIResponse<Station[]>> {
213
+ const searchParams = new URLSearchParams();
214
+
215
+ if (params.parameterName) searchParams.set('parameterName', params.parameterName);
216
+ if (params.parameter) searchParams.set('parameter', params.parameter);
217
+ if (params.qualifier) searchParams.set('qualifier', params.qualifier);
218
+ if (params.label) searchParams.set('label', params.label);
219
+ if (params.town) searchParams.set('town', params.town);
220
+ if (params.catchmentName) searchParams.set('catchmentName', params.catchmentName);
221
+ if (params.riverName) searchParams.set('riverName', params.riverName);
222
+ if (params.stationReference) searchParams.set('stationReference', params.stationReference);
223
+ if (params.RLOIid) searchParams.set('RLOIid', params.RLOIid);
224
+ if (params.search) searchParams.set('search', params.search);
225
+ if (params.lat && params.long) {
226
+ searchParams.set('lat', params.lat.toString());
227
+ searchParams.set('long', params.long.toString());
228
+ if (params.dist) searchParams.set('dist', params.dist.toString());
229
+ }
230
+ if (params.type) searchParams.set('type', params.type);
231
+ if (params.status) searchParams.set('status', params.status);
232
+ if (params.view) searchParams.set('_view', params.view);
233
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
234
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
235
+
236
+ const queryString = searchParams.toString();
237
+ return this.makeRequest(`/id/stations${queryString ? '?' + queryString : ''}`);
238
+ }
239
+
240
+ async getStation(id: string): Promise<APIResponse<Station>> {
241
+ return this.makeRequest(`/id/stations/${id}`);
242
+ }
243
+
244
+ // Measures
245
+ async getMeasures(params: {
246
+ parameterName?: string;
247
+ parameter?: string;
248
+ qualifier?: string;
249
+ stationReference?: string;
250
+ station?: string;
251
+ limit?: number;
252
+ offset?: number;
253
+ } = {}): Promise<APIResponse<Measure[]>> {
254
+ const searchParams = new URLSearchParams();
255
+
256
+ if (params.parameterName) searchParams.set('parameterName', params.parameterName);
257
+ if (params.parameter) searchParams.set('parameter', params.parameter);
258
+ if (params.qualifier) searchParams.set('qualifier', params.qualifier);
259
+ if (params.stationReference) searchParams.set('stationReference', params.stationReference);
260
+ if (params.station) searchParams.set('station', params.station);
261
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
262
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
263
+
264
+ const queryString = searchParams.toString();
265
+ return this.makeRequest(`/id/measures${queryString ? '?' + queryString : ''}`);
266
+ }
267
+
268
+ async getStationMeasures(stationId: string): Promise<APIResponse<Measure[]>> {
269
+ return this.makeRequest(`/id/stations/${stationId}/measures`);
270
+ }
271
+
272
+ async getMeasure(id: string): Promise<APIResponse<Measure>> {
273
+ return this.makeRequest(`/id/measures/${id}`);
274
+ }
275
+
276
+ // Readings
277
+ async getReadings(params: {
278
+ latest?: boolean;
279
+ today?: boolean;
280
+ date?: string;
281
+ startdate?: string;
282
+ enddate?: string;
283
+ since?: string;
284
+ parameter?: string;
285
+ qualifier?: string;
286
+ stationReference?: string;
287
+ station?: string;
288
+ view?: 'full';
289
+ sorted?: boolean;
290
+ limit?: number;
291
+ offset?: number;
292
+ } = {}): Promise<APIResponse<Reading[]>> {
293
+ const searchParams = new URLSearchParams();
294
+
295
+ if (params.latest) searchParams.set('latest', '');
296
+ if (params.today) searchParams.set('today', '');
297
+ if (params.date) searchParams.set('date', params.date);
298
+ if (params.startdate) searchParams.set('startdate', params.startdate);
299
+ if (params.enddate) searchParams.set('enddate', params.enddate);
300
+ if (params.since) searchParams.set('since', params.since);
301
+ if (params.parameter) searchParams.set('parameter', params.parameter);
302
+ if (params.qualifier) searchParams.set('qualifier', params.qualifier);
303
+ if (params.stationReference) searchParams.set('stationReference', params.stationReference);
304
+ if (params.station) searchParams.set('station', params.station);
305
+ if (params.view) searchParams.set('_view', params.view);
306
+ if (params.sorted) searchParams.set('_sorted', '');
307
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
308
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
309
+
310
+ const queryString = searchParams.toString();
311
+ return this.makeRequest(`/data/readings${queryString ? '?' + queryString : ''}`);
312
+ }
313
+
314
+ async getMeasureReadings(measureId: string, params: {
315
+ latest?: boolean;
316
+ today?: boolean;
317
+ date?: string;
318
+ startdate?: string;
319
+ enddate?: string;
320
+ since?: string;
321
+ view?: 'full';
322
+ sorted?: boolean;
323
+ limit?: number;
324
+ offset?: number;
325
+ } = {}): Promise<APIResponse<Reading[]>> {
326
+ const searchParams = new URLSearchParams();
327
+
328
+ if (params.latest) searchParams.set('latest', '');
329
+ if (params.today) searchParams.set('today', '');
330
+ if (params.date) searchParams.set('date', params.date);
331
+ if (params.startdate) searchParams.set('startdate', params.startdate);
332
+ if (params.enddate) searchParams.set('enddate', params.enddate);
333
+ if (params.since) searchParams.set('since', params.since);
334
+ if (params.view) searchParams.set('_view', params.view);
335
+ if (params.sorted) searchParams.set('_sorted', '');
336
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
337
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
338
+
339
+ const queryString = searchParams.toString();
340
+ return this.makeRequest(`/id/measures/${measureId}/readings${queryString ? '?' + queryString : ''}`);
341
+ }
342
+
343
+ async getStationReadings(stationId: string, params: {
344
+ latest?: boolean;
345
+ today?: boolean;
346
+ date?: string;
347
+ startdate?: string;
348
+ enddate?: string;
349
+ since?: string;
350
+ view?: 'full';
351
+ sorted?: boolean;
352
+ limit?: number;
353
+ offset?: number;
354
+ } = {}): Promise<APIResponse<Reading[]>> {
355
+ const searchParams = new URLSearchParams();
356
+
357
+ if (params.latest) searchParams.set('latest', '');
358
+ if (params.today) searchParams.set('today', '');
359
+ if (params.date) searchParams.set('date', params.date);
360
+ if (params.startdate) searchParams.set('startdate', params.startdate);
361
+ if (params.enddate) searchParams.set('enddate', params.enddate);
362
+ if (params.since) searchParams.set('since', params.since);
363
+ if (params.view) searchParams.set('_view', params.view);
364
+ if (params.sorted) searchParams.set('_sorted', '');
365
+ if (params.limit) searchParams.set('_limit', params.limit.toString());
366
+ if (params.offset) searchParams.set('_offset', params.offset.toString());
367
+
368
+ const queryString = searchParams.toString();
369
+ return this.makeRequest(`/id/stations/${stationId}/readings${queryString ? '?' + queryString : ''}`);
370
+ }
371
+ }
372
+
373
+ // Initialize server
374
+ const server = new Server({
375
+ name: "environment-agency-flood-server",
376
+ version: "1.0.0",
377
+ capabilities: {
378
+ tools: {},
379
+ },
380
+ });
381
+
382
+ const api = new EnvironmentAgencyAPI();
383
+
384
+ // Tool definitions
385
+ const TOOLS = [
386
+ {
387
+ name: "get_flood_warnings",
388
+ description: "Get current flood warnings and alerts. Updated every 15 minutes.",
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ min_severity: {
393
+ type: "number",
394
+ description: "Minimum severity level (1=Severe Flood Warning, 2=Flood Warning, 3=Flood Alert, 4=No longer in force)",
395
+ enum: [1, 2, 3, 4],
396
+ },
397
+ county: {
398
+ type: "string",
399
+ description: "Filter by county name (e.g., 'Somerset', 'Yorkshire')",
400
+ },
401
+ lat: {
402
+ type: "number",
403
+ description: "Latitude for geographic filter (WGS84)",
404
+ },
405
+ long: {
406
+ type: "number",
407
+ description: "Longitude for geographic filter (WGS84)",
408
+ },
409
+ dist: {
410
+ type: "number",
411
+ description: "Distance in km for geographic filter (used with lat/long)",
412
+ },
413
+ },
414
+ },
415
+ },
416
+ {
417
+ name: "get_flood_warning",
418
+ description: "Get details of a specific flood warning by ID",
419
+ inputSchema: {
420
+ type: "object",
421
+ properties: {
422
+ id: {
423
+ type: "string",
424
+ description: "Flood warning ID",
425
+ },
426
+ },
427
+ required: ["id"],
428
+ },
429
+ },
430
+ {
431
+ name: "get_flood_areas",
432
+ description: "Get flood areas (regions where warnings/alerts may apply)",
433
+ inputSchema: {
434
+ type: "object",
435
+ properties: {
436
+ lat: {
437
+ type: "number",
438
+ description: "Latitude for geographic filter (WGS84)",
439
+ },
440
+ long: {
441
+ type: "number",
442
+ description: "Longitude for geographic filter (WGS84)",
443
+ },
444
+ dist: {
445
+ type: "number",
446
+ description: "Distance in km for geographic filter",
447
+ },
448
+ limit: {
449
+ type: "number",
450
+ description: "Maximum number of results (default 500)",
451
+ default: 500,
452
+ },
453
+ offset: {
454
+ type: "number",
455
+ description: "Offset for pagination",
456
+ default: 0,
457
+ },
458
+ },
459
+ },
460
+ },
461
+ {
462
+ name: "get_flood_area",
463
+ description: "Get details of a specific flood area by area code",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ area_code: {
468
+ type: "string",
469
+ description: "Flood area code (e.g., '122WAC953')",
470
+ },
471
+ },
472
+ required: ["area_code"],
473
+ },
474
+ },
475
+ {
476
+ name: "get_monitoring_stations",
477
+ description: "Get monitoring stations that measure water levels, flows, etc.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ parameter_name: {
482
+ type: "string",
483
+ description: "Parameter name (e.g., 'Water Level', 'Flow', 'Temperature')",
484
+ },
485
+ parameter: {
486
+ type: "string",
487
+ description: "Short parameter name (e.g., 'level', 'flow', 'temperature')",
488
+ },
489
+ qualifier: {
490
+ type: "string",
491
+ description: "Qualifier (e.g., 'Stage', 'Downstream Stage', 'Groundwater', 'Tidal Level')",
492
+ },
493
+ town: {
494
+ type: "string",
495
+ description: "Filter by town name",
496
+ },
497
+ catchment_name: {
498
+ type: "string",
499
+ description: "Filter by catchment name",
500
+ },
501
+ river_name: {
502
+ type: "string",
503
+ description: "Filter by river name",
504
+ },
505
+ search: {
506
+ type: "string",
507
+ description: "Search text in station labels",
508
+ },
509
+ lat: {
510
+ type: "number",
511
+ description: "Latitude for geographic filter (WGS84)",
512
+ },
513
+ long: {
514
+ type: "number",
515
+ description: "Longitude for geographic filter (WGS84)",
516
+ },
517
+ dist: {
518
+ type: "number",
519
+ description: "Distance in km for geographic filter",
520
+ },
521
+ type: {
522
+ type: "string",
523
+ description: "Station type",
524
+ enum: ["SingleLevel", "MultiTraceLevel", "Coastal", "Groundwater", "Meteorological"],
525
+ },
526
+ status: {
527
+ type: "string",
528
+ description: "Station status",
529
+ enum: ["Active", "Closed", "Suspended"],
530
+ },
531
+ view: {
532
+ type: "string",
533
+ description: "Set to 'full' for detailed information including scale data",
534
+ enum: ["full"],
535
+ },
536
+ limit: {
537
+ type: "number",
538
+ description: "Maximum number of results",
539
+ },
540
+ offset: {
541
+ type: "number",
542
+ description: "Offset for pagination",
543
+ },
544
+ },
545
+ },
546
+ },
547
+ {
548
+ name: "get_monitoring_station",
549
+ description: "Get detailed information about a specific monitoring station",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ station_id: {
554
+ type: "string",
555
+ description: "Station ID (e.g., '1491TH')",
556
+ },
557
+ },
558
+ required: ["station_id"],
559
+ },
560
+ },
561
+ {
562
+ name: "get_measures",
563
+ description: "Get measurement types available across all stations",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {
567
+ parameter_name: {
568
+ type: "string",
569
+ description: "Parameter name (e.g., 'Water Level', 'Flow')",
570
+ },
571
+ parameter: {
572
+ type: "string",
573
+ description: "Short parameter name (e.g., 'level', 'flow')",
574
+ },
575
+ qualifier: {
576
+ type: "string",
577
+ description: "Qualifier (e.g., 'Stage', 'Downstream Stage')",
578
+ },
579
+ station_reference: {
580
+ type: "string",
581
+ description: "Station reference ID",
582
+ },
583
+ station: {
584
+ type: "string",
585
+ description: "Station URI",
586
+ },
587
+ limit: {
588
+ type: "number",
589
+ description: "Maximum number of results",
590
+ },
591
+ offset: {
592
+ type: "number",
593
+ description: "Offset for pagination",
594
+ },
595
+ },
596
+ },
597
+ },
598
+ {
599
+ name: "get_station_measures",
600
+ description: "Get all measurement types available from a specific station",
601
+ inputSchema: {
602
+ type: "object",
603
+ properties: {
604
+ station_id: {
605
+ type: "string",
606
+ description: "Station ID",
607
+ },
608
+ },
609
+ required: ["station_id"],
610
+ },
611
+ },
612
+ {
613
+ name: "get_readings",
614
+ description: "Get measurement readings from all stations. Updated every 15 minutes.",
615
+ inputSchema: {
616
+ type: "object",
617
+ properties: {
618
+ latest: {
619
+ type: "boolean",
620
+ description: "Get only the most recent reading for each measure",
621
+ },
622
+ today: {
623
+ type: "boolean",
624
+ description: "Get all readings from today",
625
+ },
626
+ date: {
627
+ type: "string",
628
+ description: "Get readings from specific date (YYYY-MM-DD)",
629
+ },
630
+ startdate: {
631
+ type: "string",
632
+ description: "Start date for date range (YYYY-MM-DD)",
633
+ },
634
+ enddate: {
635
+ type: "string",
636
+ description: "End date for date range (YYYY-MM-DD)",
637
+ },
638
+ parameter: {
639
+ type: "string",
640
+ description: "Filter by parameter (e.g., 'level', 'flow')",
641
+ },
642
+ qualifier: {
643
+ type: "string",
644
+ description: "Filter by qualifier (e.g., 'Groundwater', 'Tidal Level')",
645
+ },
646
+ station_reference: {
647
+ type: "string",
648
+ description: "Filter by station reference",
649
+ },
650
+ station: {
651
+ type: "string",
652
+ description: "Filter by station URI",
653
+ },
654
+ view: {
655
+ type: "string",
656
+ description: "Set to 'full' for detailed measure information",
657
+ enum: ["full"],
658
+ },
659
+ sorted: {
660
+ type: "boolean",
661
+ description: "Sort by date (descending)",
662
+ },
663
+ limit: {
664
+ type: "number",
665
+ description: "Maximum number of results (default 500, max 10000)",
666
+ },
667
+ offset: {
668
+ type: "number",
669
+ description: "Offset for pagination",
670
+ },
671
+ },
672
+ },
673
+ },
674
+ {
675
+ name: "get_measure_readings",
676
+ description: "Get readings for a specific measurement type",
677
+ inputSchema: {
678
+ type: "object",
679
+ properties: {
680
+ measure_id: {
681
+ type: "string",
682
+ description: "Measure ID (e.g., '1491TH-level-stage-i-15_min-mASD')",
683
+ },
684
+ latest: {
685
+ type: "boolean",
686
+ description: "Get only the most recent reading",
687
+ },
688
+ today: {
689
+ type: "boolean",
690
+ description: "Get all readings from today",
691
+ },
692
+ date: {
693
+ type: "string",
694
+ description: "Get readings from specific date (YYYY-MM-DD)",
695
+ },
696
+ startdate: {
697
+ type: "string",
698
+ description: "Start date for date range (YYYY-MM-DD)",
699
+ },
700
+ enddate: {
701
+ type: "string",
702
+ description: "End date for date range (YYYY-MM-DD)",
703
+ },
704
+ since: {
705
+ type: "string",
706
+ description: "Get readings since specific datetime (ISO format)",
707
+ },
708
+ view: {
709
+ type: "string",
710
+ description: "Set to 'full' for detailed information",
711
+ enum: ["full"],
712
+ },
713
+ sorted: {
714
+ type: "boolean",
715
+ description: "Sort by date (descending)",
716
+ },
717
+ limit: {
718
+ type: "number",
719
+ description: "Maximum number of results",
720
+ },
721
+ offset: {
722
+ type: "number",
723
+ description: "Offset for pagination",
724
+ },
725
+ },
726
+ required: ["measure_id"],
727
+ },
728
+ },
729
+ {
730
+ name: "get_station_readings",
731
+ description: "Get all readings from a specific monitoring station",
732
+ inputSchema: {
733
+ type: "object",
734
+ properties: {
735
+ station_id: {
736
+ type: "string",
737
+ description: "Station ID",
738
+ },
739
+ latest: {
740
+ type: "boolean",
741
+ description: "Get only the most recent readings",
742
+ },
743
+ today: {
744
+ type: "boolean",
745
+ description: "Get all readings from today",
746
+ },
747
+ date: {
748
+ type: "string",
749
+ description: "Get readings from specific date (YYYY-MM-DD)",
750
+ },
751
+ startdate: {
752
+ type: "string",
753
+ description: "Start date for date range (YYYY-MM-DD)",
754
+ },
755
+ enddate: {
756
+ type: "string",
757
+ description: "End date for date range (YYYY-MM-DD)",
758
+ },
759
+ since: {
760
+ type: "string",
761
+ description: "Get readings since specific datetime (ISO format)",
762
+ },
763
+ view: {
764
+ type: "string",
765
+ description: "Set to 'full' for detailed information",
766
+ enum: ["full"],
767
+ },
768
+ sorted: {
769
+ type: "boolean",
770
+ description: "Sort by date (descending)",
771
+ },
772
+ limit: {
773
+ type: "number",
774
+ description: "Maximum number of results",
775
+ },
776
+ offset: {
777
+ type: "number",
778
+ description: "Offset for pagination",
779
+ },
780
+ },
781
+ required: ["station_id"],
782
+ },
783
+ },
784
+ ];
785
+
786
+ // List tools handler
787
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
788
+ return {
789
+ tools: TOOLS,
790
+ };
791
+ });
792
+
793
+ // Call tool handler
794
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
795
+ const { name, arguments: args } = request.params;
796
+
797
+ // Type guard to ensure args is an object
798
+ if (!args || typeof args !== 'object') {
799
+ throw new McpError(ErrorCode.InvalidRequest, "Invalid arguments provided");
800
+ }
801
+
802
+ try {
803
+ switch (name) {
804
+ case "get_flood_warnings": {
805
+ const result = await api.getFloodWarnings({
806
+ minSeverity: args.min_severity as number | undefined,
807
+ county: args.county as string | undefined,
808
+ lat: args.lat as number | undefined,
809
+ long: args.long as number | undefined,
810
+ dist: args.dist as number | undefined,
811
+ });
812
+ return {
813
+ content: [
814
+ {
815
+ type: "text",
816
+ text: JSON.stringify(result, null, 2),
817
+ },
818
+ ],
819
+ };
820
+ }
821
+
822
+ case "get_flood_warning": {
823
+ const result = await api.getFloodWarning(args.id as string);
824
+ return {
825
+ content: [
826
+ {
827
+ type: "text",
828
+ text: JSON.stringify(result, null, 2),
829
+ },
830
+ ],
831
+ };
832
+ }
833
+
834
+ case "get_flood_areas": {
835
+ const result = await api.getFloodAreas({
836
+ lat: args.lat as number | undefined,
837
+ long: args.long as number | undefined,
838
+ dist: args.dist as number | undefined,
839
+ limit: args.limit as number | undefined,
840
+ offset: args.offset as number | undefined,
841
+ });
842
+ return {
843
+ content: [
844
+ {
845
+ type: "text",
846
+ text: JSON.stringify(result, null, 2),
847
+ },
848
+ ],
849
+ };
850
+ }
851
+
852
+ case "get_flood_area": {
853
+ const result = await api.getFloodArea(args.area_code as string);
854
+ return {
855
+ content: [
856
+ {
857
+ type: "text",
858
+ text: JSON.stringify(result, null, 2),
859
+ },
860
+ ],
861
+ };
862
+ }
863
+
864
+ case "get_monitoring_stations": {
865
+ const result = await api.getStations({
866
+ parameterName: args.parameter_name as string | undefined,
867
+ parameter: args.parameter as string | undefined,
868
+ qualifier: args.qualifier as string | undefined,
869
+ town: args.town as string | undefined,
870
+ catchmentName: args.catchment_name as string | undefined,
871
+ riverName: args.river_name as string | undefined,
872
+ search: args.search as string | undefined,
873
+ lat: args.lat as number | undefined,
874
+ long: args.long as number | undefined,
875
+ dist: args.dist as number | undefined,
876
+ type: args.type as string | undefined,
877
+ status: args.status as string | undefined,
878
+ view: args.view as 'full' | undefined,
879
+ limit: args.limit as number | undefined,
880
+ offset: args.offset as number | undefined,
881
+ });
882
+ return {
883
+ content: [
884
+ {
885
+ type: "text",
886
+ text: JSON.stringify(result, null, 2),
887
+ },
888
+ ],
889
+ };
890
+ }
891
+
892
+ case "get_monitoring_station": {
893
+ const result = await api.getStation(args.station_id as string);
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text: JSON.stringify(result, null, 2),
899
+ },
900
+ ],
901
+ };
902
+ }
903
+
904
+ case "get_measures": {
905
+ const result = await api.getMeasures({
906
+ parameterName: args.parameter_name as string | undefined,
907
+ parameter: args.parameter as string | undefined,
908
+ qualifier: args.qualifier as string | undefined,
909
+ stationReference: args.station_reference as string | undefined,
910
+ station: args.station as string | undefined,
911
+ limit: args.limit as number | undefined,
912
+ offset: args.offset as number | undefined,
913
+ });
914
+ return {
915
+ content: [
916
+ {
917
+ type: "text",
918
+ text: JSON.stringify(result, null, 2),
919
+ },
920
+ ],
921
+ };
922
+ }
923
+
924
+ case "get_station_measures": {
925
+ const result = await api.getStationMeasures(args.station_id as string);
926
+ return {
927
+ content: [
928
+ {
929
+ type: "text",
930
+ text: JSON.stringify(result, null, 2),
931
+ },
932
+ ],
933
+ };
934
+ }
935
+
936
+ case "get_readings": {
937
+ const result = await api.getReadings({
938
+ latest: args.latest as boolean | undefined,
939
+ today: args.today as boolean | undefined,
940
+ date: args.date as string | undefined,
941
+ startdate: args.startdate as string | undefined,
942
+ enddate: args.enddate as string | undefined,
943
+ parameter: args.parameter as string | undefined,
944
+ qualifier: args.qualifier as string | undefined,
945
+ stationReference: args.station_reference as string | undefined,
946
+ station: args.station as string | undefined,
947
+ view: args.view as 'full' | undefined,
948
+ sorted: args.sorted as boolean | undefined,
949
+ limit: args.limit as number | undefined,
950
+ offset: args.offset as number | undefined,
951
+ });
952
+ return {
953
+ content: [
954
+ {
955
+ type: "text",
956
+ text: JSON.stringify(result, null, 2),
957
+ },
958
+ ],
959
+ };
960
+ }
961
+
962
+ case "get_measure_readings": {
963
+ const result = await api.getMeasureReadings(args.measure_id as string, {
964
+ latest: args.latest as boolean | undefined,
965
+ today: args.today as boolean | undefined,
966
+ date: args.date as string | undefined,
967
+ startdate: args.startdate as string | undefined,
968
+ enddate: args.enddate as string | undefined,
969
+ since: args.since as string | undefined,
970
+ view: args.view as 'full' | undefined,
971
+ sorted: args.sorted as boolean | undefined,
972
+ limit: args.limit as number | undefined,
973
+ offset: args.offset as number | undefined,
974
+ });
975
+ return {
976
+ content: [
977
+ {
978
+ type: "text",
979
+ text: JSON.stringify(result, null, 2),
980
+ },
981
+ ],
982
+ };
983
+ }
984
+
985
+ case "get_station_readings": {
986
+ const result = await api.getStationReadings(args.station_id as string, {
987
+ latest: args.latest as boolean | undefined,
988
+ today: args.today as boolean | undefined,
989
+ date: args.date as string | undefined,
990
+ startdate: args.startdate as string | undefined,
991
+ enddate: args.enddate as string | undefined,
992
+ since: args.since as string | undefined,
993
+ view: args.view as 'full' | undefined,
994
+ sorted: args.sorted as boolean | undefined,
995
+ limit: args.limit as number | undefined,
996
+ offset: args.offset as number | undefined,
997
+ });
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: JSON.stringify(result, null, 2),
1003
+ },
1004
+ ],
1005
+ };
1006
+ }
1007
+
1008
+ default:
1009
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1010
+ }
1011
+ } catch (error) {
1012
+ if (error instanceof McpError) {
1013
+ throw error;
1014
+ }
1015
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error}`);
1016
+ }
1017
+ });
1018
+
1019
+ // Start server
1020
+ async function main() {
1021
+ const transport = new StdioServerTransport();
1022
+ await server.connect(transport);
1023
+ console.error("Environment Agency Flood Monitoring MCP Server running on stdio");
1024
+ }
1025
+
1026
+ main().catch((error) => {
1027
+ console.error("Server error:", error);
1028
+ process.exit(1);
1029
+ });