@ryancardin/noaa-tides-currents-mcp-server 1.0.0 → 2.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 +164 -199
- package/dist/client/cache.d.ts +15 -0
- package/dist/client/cache.js +44 -0
- package/dist/client/http.d.ts +28 -0
- package/dist/client/http.js +178 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +28 -0
- package/dist/format/respond.d.ts +25 -0
- package/dist/format/respond.js +40 -0
- package/dist/format/series.d.ts +16 -0
- package/dist/format/series.js +42 -0
- package/dist/format/units.d.ts +16 -0
- package/dist/format/units.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +78 -8
- package/dist/interfaces/moon.d.ts +4 -4
- package/dist/interfaces/moon.js +58 -17
- package/dist/interfaces/sun.d.ts +4 -4
- package/dist/interfaces/sun.js +94 -25
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.js +89 -0
- package/dist/reference/content.d.ts +10 -0
- package/dist/reference/content.js +202 -0
- package/dist/resources/index.d.ts +7 -0
- package/dist/resources/index.js +73 -0
- package/dist/schemas/common.d.ts +38 -14
- package/dist/schemas/common.js +82 -18
- package/dist/services/data-api.d.ts +81 -0
- package/dist/services/data-api.js +117 -0
- package/dist/services/dpapi.d.ts +55 -0
- package/dist/services/dpapi.js +60 -0
- package/dist/services/metadata-api.d.ts +62 -0
- package/dist/services/metadata-api.js +105 -0
- package/dist/services/moon-phase-service.d.ts +2 -2
- package/dist/services/moon-phase-service.js +23 -25
- package/dist/services/sun-service.d.ts +2 -2
- package/dist/services/sun-service.js +53 -31
- package/dist/tools/astronomy.d.ts +7 -0
- package/dist/tools/astronomy.js +270 -0
- package/dist/tools/currents.d.ts +5 -0
- package/dist/tools/currents.js +168 -0
- package/dist/tools/derived.d.ts +6 -0
- package/dist/tools/derived.js +296 -0
- package/dist/tools/index.d.ts +3 -14
- package/dist/tools/index.js +17 -32
- package/dist/tools/met.d.ts +5 -0
- package/dist/tools/met.js +110 -0
- package/dist/tools/reference.d.ts +6 -0
- package/dist/tools/reference.js +33 -0
- package/dist/tools/station-metadata.d.ts +6 -0
- package/dist/tools/station-metadata.js +208 -0
- package/dist/tools/stations.d.ts +5 -0
- package/dist/tools/stations.js +265 -0
- package/dist/tools/water.d.ts +5 -0
- package/dist/tools/water.js +270 -0
- package/dist/validation/dates.d.ts +50 -0
- package/dist/validation/dates.js +139 -0
- package/package.json +27 -14
- package/.claude/settings.local.json +0 -29
- package/CLAUDE.md +0 -71
- package/Dockerfile +0 -14
- package/smithery.yaml +0 -16
- package/src/index.ts +0 -13
- package/src/interfaces/moon.ts +0 -44
- package/src/interfaces/noaa.ts +0 -130
- package/src/interfaces/parameters.ts +0 -20
- package/src/interfaces/sun.ts +0 -57
- package/src/schemas/common.ts +0 -23
- package/src/schemas/dpapi.ts +0 -99
- package/src/server/config.ts +0 -43
- package/src/server/mcp-server.ts +0 -135
- package/src/services/dpapi-service.ts +0 -187
- package/src/services/moon-phase-service.ts +0 -167
- package/src/services/noaa-parameters-service.ts +0 -139
- package/src/services/noaa-service.ts +0 -171
- package/src/services/sun-service.ts +0 -275
- package/src/tools/derived-product-tools.ts +0 -180
- package/src/tools/index.ts +0 -40
- package/src/tools/moon-tools.ts +0 -79
- package/src/tools/parameter-tools.ts +0 -82
- package/src/tools/station-tools.ts +0 -57
- package/src/tools/sun-tools.ts +0 -120
- package/src/tools/water-tools.ts +0 -166
- package/src/types/moon.ts +0 -27
- package/src/types/sun.ts +0 -51
- package/src/types/suncalc.d.ts +0 -110
- package/test-dpapi.js +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Water level observation and tide prediction tools.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getWaterLevels, getWaterLevelSummaries, getTidePredictions, } from "../services/data-api.js";
|
|
6
|
+
import { dateFields, DatumSchema, READ_ONLY_ANNOTATIONS, ResponseFormatSchema, StationIdSchema, TimeZoneSchema, UnitsSchema, } from "../schemas/common.js";
|
|
7
|
+
import { respond, respondError } from "../format/respond.js";
|
|
8
|
+
import { seriesMarkdown, timeZoneLabel } from "../format/series.js";
|
|
9
|
+
import { FLAG_LEGENDS, unitLabel } from "../format/units.js";
|
|
10
|
+
function metadataName(response) {
|
|
11
|
+
return response.metadata?.name;
|
|
12
|
+
}
|
|
13
|
+
export function registerWaterTools(server) {
|
|
14
|
+
server.registerTool("noaa_get_water_levels", {
|
|
15
|
+
title: "Get Observed Water Levels",
|
|
16
|
+
description: `Get observed water levels from a NOAA tide station as a time series.
|
|
17
|
+
|
|
18
|
+
Choose the interval: "6" = standard 6-minute observations (preliminary or verified, max 31 days per request), "1" = 1-minute preliminary data (max 4 days), "hourly" = verified hourly heights (max 1 year). Heights are relative to the requested datum (MLLW by default — the US nautical chart zero).
|
|
19
|
+
|
|
20
|
+
Returns per record: t (timestamp in requested time zone), v (height), s (sigma), f (quality flags, decoded in output), q (p=preliminary, v=verified). Recent data is preliminary; verification takes days to weeks.
|
|
21
|
+
|
|
22
|
+
Use for: "what is the water level right now" (date=latest), storm surge analysis, comparing observed vs predicted tide. Do NOT use for future tides — use noaa_get_tide_predictions.`,
|
|
23
|
+
inputSchema: {
|
|
24
|
+
station: StationIdSchema,
|
|
25
|
+
interval: z
|
|
26
|
+
.enum(["1", "6", "hourly"])
|
|
27
|
+
.default("6")
|
|
28
|
+
.describe('Observation interval: "6" = 6-minute (standard, 31-day max), "1" = 1-minute preliminary (4-day max), "hourly" = verified hourly heights (1-year max).'),
|
|
29
|
+
datum: DatumSchema.default("MLLW"),
|
|
30
|
+
units: UnitsSchema,
|
|
31
|
+
time_zone: TimeZoneSchema,
|
|
32
|
+
...dateFields,
|
|
33
|
+
response_format: ResponseFormatSchema,
|
|
34
|
+
},
|
|
35
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
36
|
+
}, async (params) => {
|
|
37
|
+
try {
|
|
38
|
+
const { product, response } = await getWaterLevels(params);
|
|
39
|
+
const data = response.data ?? [];
|
|
40
|
+
const unitsLabel = `${unitLabel("water_level", params.units)} above ${params.datum}`;
|
|
41
|
+
const structured = {
|
|
42
|
+
station: params.station,
|
|
43
|
+
station_name: metadataName(response),
|
|
44
|
+
product,
|
|
45
|
+
datum: params.datum,
|
|
46
|
+
units: params.units,
|
|
47
|
+
units_label: unitsLabel,
|
|
48
|
+
time_zone: params.time_zone,
|
|
49
|
+
count: data.length,
|
|
50
|
+
data,
|
|
51
|
+
};
|
|
52
|
+
const anyVerified = data.some((d) => d.q === "v");
|
|
53
|
+
const anyPreliminary = data.some((d) => d.q === "p");
|
|
54
|
+
const legend = params.interval === "hourly"
|
|
55
|
+
? FLAG_LEGENDS.hourly_height
|
|
56
|
+
: anyPreliminary && !anyVerified
|
|
57
|
+
? FLAG_LEGENDS.water_level_preliminary
|
|
58
|
+
: FLAG_LEGENDS.water_level_verified;
|
|
59
|
+
const markdown = seriesMarkdown({
|
|
60
|
+
title: "Observed Water Levels",
|
|
61
|
+
station: params.station,
|
|
62
|
+
stationName: metadataName(response),
|
|
63
|
+
unitsLabel,
|
|
64
|
+
datum: params.datum,
|
|
65
|
+
timeZone: timeZoneLabel(params.time_zone),
|
|
66
|
+
}, [
|
|
67
|
+
"Time",
|
|
68
|
+
`Height (${unitLabel("water_level", params.units)})`,
|
|
69
|
+
"Sigma",
|
|
70
|
+
"Quality",
|
|
71
|
+
"Flags",
|
|
72
|
+
], data.map((d) => [
|
|
73
|
+
d.t,
|
|
74
|
+
d.v,
|
|
75
|
+
d.s,
|
|
76
|
+
d.q === "v" ? "verified" : d.q === "p" ? "preliminary" : d.q,
|
|
77
|
+
d.f,
|
|
78
|
+
]), legend);
|
|
79
|
+
return respond(params.response_format, structured, markdown);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return respondError(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
server.registerTool("noaa_get_water_level_summaries", {
|
|
86
|
+
title: "Get Water Level Summaries (High/Low, Daily, Monthly)",
|
|
87
|
+
description: `Get verified summary water-level products from a NOAA station:
|
|
88
|
+
|
|
89
|
+
- high_low: each day's observed highs/lows with ty = HH (higher high), H, L, LL (lower low). Max 1 year per request.
|
|
90
|
+
- daily_mean: daily mean levels — GREAT LAKES STATIONS ONLY; NOAA requires local standard time, which this tool applies automatically. Max 10 years.
|
|
91
|
+
- daily_max_min: daily maxima/minima from hourly and 6-minute data with completeness percentages. Max 10 years.
|
|
92
|
+
- monthly_mean: monthly tidal datum means (columns MHHW, MHW, MSL, MTL, MLW, MLLW, DTL, GT, MN, DHQ, DLQ, HWI, LWI, highest, lowest). Max 200 years — ideal for long-term climatology.
|
|
93
|
+
|
|
94
|
+
Use for: historical extremes, mixed-tide analysis (HH vs H), long-term averages. For raw time series use noaa_get_water_levels; for future tides use noaa_get_tide_predictions.`,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
station: StationIdSchema,
|
|
97
|
+
product: z
|
|
98
|
+
.enum(["high_low", "daily_mean", "daily_max_min", "monthly_mean"])
|
|
99
|
+
.describe("Summary product: high_low (daily tide extremes, 1yr max), daily_mean (Great Lakes only, 10yr), daily_max_min (10yr), monthly_mean (datum means, 200yr)."),
|
|
100
|
+
datum: DatumSchema.default("MLLW"),
|
|
101
|
+
units: UnitsSchema,
|
|
102
|
+
time_zone: TimeZoneSchema,
|
|
103
|
+
...dateFields,
|
|
104
|
+
response_format: ResponseFormatSchema,
|
|
105
|
+
},
|
|
106
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
107
|
+
}, async (params) => {
|
|
108
|
+
try {
|
|
109
|
+
const { response, timeZoneForced } = await getWaterLevelSummaries(params);
|
|
110
|
+
const data = response.data ?? [];
|
|
111
|
+
const unitsLabel = `${unitLabel("water_level", params.units)} above ${params.datum}`;
|
|
112
|
+
const structured = {
|
|
113
|
+
station: params.station,
|
|
114
|
+
station_name: metadataName(response),
|
|
115
|
+
product: params.product,
|
|
116
|
+
datum: params.datum,
|
|
117
|
+
units: params.units,
|
|
118
|
+
units_label: unitsLabel,
|
|
119
|
+
time_zone: timeZoneForced ? "lst" : params.time_zone,
|
|
120
|
+
time_zone_forced_to_lst: timeZoneForced || undefined,
|
|
121
|
+
count: data.length,
|
|
122
|
+
data,
|
|
123
|
+
};
|
|
124
|
+
let headers;
|
|
125
|
+
let rows;
|
|
126
|
+
let legend;
|
|
127
|
+
if (params.product === "high_low") {
|
|
128
|
+
headers = [
|
|
129
|
+
"Time",
|
|
130
|
+
`Height (${unitLabel("water_level", params.units)})`,
|
|
131
|
+
"Type",
|
|
132
|
+
"Flags",
|
|
133
|
+
];
|
|
134
|
+
const tyLabels = {
|
|
135
|
+
HH: "higher high",
|
|
136
|
+
H: "high",
|
|
137
|
+
L: "low",
|
|
138
|
+
LL: "lower low",
|
|
139
|
+
};
|
|
140
|
+
rows = data.map((d) => {
|
|
141
|
+
const ty = (d.ty ?? "").trim();
|
|
142
|
+
return [d.t, d.v, ty ? `${ty} (${tyLabels[ty] ?? "?"})` : "", d.f];
|
|
143
|
+
});
|
|
144
|
+
legend = FLAG_LEGENDS.high_low;
|
|
145
|
+
}
|
|
146
|
+
else if (params.product === "monthly_mean") {
|
|
147
|
+
headers = [
|
|
148
|
+
"Year",
|
|
149
|
+
"Month",
|
|
150
|
+
"Highest",
|
|
151
|
+
"MHHW",
|
|
152
|
+
"MSL",
|
|
153
|
+
"MLLW",
|
|
154
|
+
"Lowest",
|
|
155
|
+
];
|
|
156
|
+
rows = data.map((d) => {
|
|
157
|
+
const rec = d;
|
|
158
|
+
return [
|
|
159
|
+
rec.year,
|
|
160
|
+
rec.month,
|
|
161
|
+
rec.highest,
|
|
162
|
+
rec.MHHW,
|
|
163
|
+
rec.MSL,
|
|
164
|
+
rec.MLLW,
|
|
165
|
+
rec.lowest,
|
|
166
|
+
];
|
|
167
|
+
});
|
|
168
|
+
legend =
|
|
169
|
+
'Full datum columns (MHW, MTL, MLW, DTL, GT, MN, DHQ, DLQ, HWI, LWI, inferred) are in the JSON payload (response_format "json").';
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
headers = [
|
|
173
|
+
"Date",
|
|
174
|
+
`Value (${unitLabel("water_level", params.units)})`,
|
|
175
|
+
"Flags",
|
|
176
|
+
];
|
|
177
|
+
rows = data.map((d) => [d.t, d.v, d.f]);
|
|
178
|
+
legend = FLAG_LEGENDS.hourly_height;
|
|
179
|
+
}
|
|
180
|
+
const extra = [];
|
|
181
|
+
if (timeZoneForced) {
|
|
182
|
+
extra.push("_Note: daily_mean requires local standard time — time_zone was set to lst._");
|
|
183
|
+
}
|
|
184
|
+
const markdown = seriesMarkdown({
|
|
185
|
+
title: `Water Level Summary — ${params.product}`,
|
|
186
|
+
station: params.station,
|
|
187
|
+
stationName: metadataName(response),
|
|
188
|
+
unitsLabel,
|
|
189
|
+
datum: params.datum,
|
|
190
|
+
timeZone: timeZoneForced
|
|
191
|
+
? timeZoneLabel("lst")
|
|
192
|
+
: timeZoneLabel(params.time_zone),
|
|
193
|
+
extra,
|
|
194
|
+
}, headers, rows, legend);
|
|
195
|
+
return respond(params.response_format, structured, markdown);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
return respondError(error);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
server.registerTool("noaa_get_tide_predictions", {
|
|
202
|
+
title: "Get Tide Predictions",
|
|
203
|
+
description: `Get NOAA harmonic tide predictions (future or past) for a station.
|
|
204
|
+
|
|
205
|
+
interval="hilo" (recommended for "when is high/low tide") returns the daily tide events with type H/L — up to 10 years per request. Other intervals (h, 1, 5, 6, 10, 15, 30, 60 minutes) return a height time series — up to 1 year per request.
|
|
206
|
+
|
|
207
|
+
Heights are relative to the requested datum (MLLW default). Notes:
|
|
208
|
+
- Great Lakes stations have NO tide predictions (lake levels are not tidal).
|
|
209
|
+
- Subordinate stations (type "S") only support interval=hilo; use the station's reference (R) station for interval series.
|
|
210
|
+
- Predictions are astronomical only — they exclude weather effects (storm surge, wind setup). Compare with noaa_get_water_levels for actual conditions.`,
|
|
211
|
+
inputSchema: {
|
|
212
|
+
station: StationIdSchema,
|
|
213
|
+
interval: z
|
|
214
|
+
.enum(["hilo", "h", "1", "5", "6", "10", "15", "30", "60"])
|
|
215
|
+
.default("hilo")
|
|
216
|
+
.describe('"hilo" = high/low tide events only (max 10-year span). "h" = hourly, or 1/5/6/10/15/30/60-minute series (max 1-year span).'),
|
|
217
|
+
datum: DatumSchema.default("MLLW"),
|
|
218
|
+
units: UnitsSchema,
|
|
219
|
+
time_zone: TimeZoneSchema,
|
|
220
|
+
...dateFields,
|
|
221
|
+
response_format: ResponseFormatSchema,
|
|
222
|
+
},
|
|
223
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
224
|
+
}, async (params) => {
|
|
225
|
+
try {
|
|
226
|
+
const response = await getTidePredictions(params);
|
|
227
|
+
const data = response.predictions ?? [];
|
|
228
|
+
const unitsLabel = `${unitLabel("water_level", params.units)} above ${params.datum}`;
|
|
229
|
+
const structured = {
|
|
230
|
+
station: params.station,
|
|
231
|
+
product: "predictions",
|
|
232
|
+
interval: params.interval,
|
|
233
|
+
datum: params.datum,
|
|
234
|
+
units: params.units,
|
|
235
|
+
units_label: unitsLabel,
|
|
236
|
+
time_zone: params.time_zone,
|
|
237
|
+
count: data.length,
|
|
238
|
+
predictions: data,
|
|
239
|
+
};
|
|
240
|
+
const isHilo = params.interval === "hilo";
|
|
241
|
+
const markdown = seriesMarkdown({
|
|
242
|
+
title: isHilo
|
|
243
|
+
? "Tide Predictions — High/Low Events"
|
|
244
|
+
: "Tide Predictions",
|
|
245
|
+
station: params.station,
|
|
246
|
+
unitsLabel,
|
|
247
|
+
datum: params.datum,
|
|
248
|
+
timeZone: timeZoneLabel(params.time_zone),
|
|
249
|
+
extra: [
|
|
250
|
+
"_Astronomical predictions only — actual levels also depend on weather (surge, wind)._",
|
|
251
|
+
],
|
|
252
|
+
}, isHilo
|
|
253
|
+
? [
|
|
254
|
+
"Time",
|
|
255
|
+
`Height (${unitLabel("water_level", params.units)})`,
|
|
256
|
+
"Tide",
|
|
257
|
+
]
|
|
258
|
+
: ["Time", `Height (${unitLabel("water_level", params.units)})`], data.map((d) => {
|
|
259
|
+
const type = d.type ?? d.ty;
|
|
260
|
+
return isHilo
|
|
261
|
+
? [d.t, d.v, type === "H" ? "High" : type === "L" ? "Low" : type]
|
|
262
|
+
: [d.t, d.v];
|
|
263
|
+
}));
|
|
264
|
+
return respond(params.response_format, structured, markdown);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return respondError(error);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-parameter handling for the NOAA Data API.
|
|
3
|
+
*
|
|
4
|
+
* The API accepts exactly one of five combinations:
|
|
5
|
+
* 1. begin_date + end_date
|
|
6
|
+
* 2. begin_date + range (hours forward)
|
|
7
|
+
* 3. end_date + range (hours back)
|
|
8
|
+
* 4. date=today | latest | recent
|
|
9
|
+
* 5. range alone (hours back from now)
|
|
10
|
+
*
|
|
11
|
+
* NOAA date formats: yyyyMMdd, "yyyyMMdd HH:mm", MM/dd/yyyy, "MM/dd/yyyy HH:mm".
|
|
12
|
+
* We additionally accept ISO (yyyy-MM-dd and yyyy-MM-ddTHH:mm) and normalize it,
|
|
13
|
+
* since agents overwhelmingly produce ISO dates.
|
|
14
|
+
*
|
|
15
|
+
* Each product/interval also has a maximum span per request (e.g. 31 days for
|
|
16
|
+
* 6-minute water levels). We enforce those limits *before* calling NOAA so the
|
|
17
|
+
* agent gets an immediate, actionable error instead of an upstream failure.
|
|
18
|
+
*/
|
|
19
|
+
export interface DateParams {
|
|
20
|
+
date?: string;
|
|
21
|
+
begin_date?: string;
|
|
22
|
+
end_date?: string;
|
|
23
|
+
range?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a user-supplied date string to a NOAA-accepted format.
|
|
27
|
+
* Throws with the list of accepted formats when unparseable.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeDate(input: string, paramName: string): string;
|
|
30
|
+
/** Parse a normalized NOAA date string into a Date (UTC interpretation). */
|
|
31
|
+
export declare function parseNoaaDate(normalized: string): Date;
|
|
32
|
+
export interface NormalizedDateParams {
|
|
33
|
+
date?: string;
|
|
34
|
+
begin_date?: string;
|
|
35
|
+
end_date?: string;
|
|
36
|
+
range?: number;
|
|
37
|
+
/** Span of the request in days, when computable. */
|
|
38
|
+
spanDays?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate that exactly one legal date-parameter combination was supplied,
|
|
42
|
+
* normalize formats, and compute the request span in days when possible.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveDateParams(params: DateParams): NormalizedDateParams;
|
|
45
|
+
/**
|
|
46
|
+
* Per-product/interval maximum request spans (days), from NOAA CO-OPS docs.
|
|
47
|
+
* Exceeding these returns an upstream error, so we fail fast with guidance.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MAX_SPAN_DAYS: Record<string, number>;
|
|
50
|
+
export declare function assertSpanWithinLimit(spanDays: number | undefined, limitKey: string, productLabel: string): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-parameter handling for the NOAA Data API.
|
|
3
|
+
*
|
|
4
|
+
* The API accepts exactly one of five combinations:
|
|
5
|
+
* 1. begin_date + end_date
|
|
6
|
+
* 2. begin_date + range (hours forward)
|
|
7
|
+
* 3. end_date + range (hours back)
|
|
8
|
+
* 4. date=today | latest | recent
|
|
9
|
+
* 5. range alone (hours back from now)
|
|
10
|
+
*
|
|
11
|
+
* NOAA date formats: yyyyMMdd, "yyyyMMdd HH:mm", MM/dd/yyyy, "MM/dd/yyyy HH:mm".
|
|
12
|
+
* We additionally accept ISO (yyyy-MM-dd and yyyy-MM-ddTHH:mm) and normalize it,
|
|
13
|
+
* since agents overwhelmingly produce ISO dates.
|
|
14
|
+
*
|
|
15
|
+
* Each product/interval also has a maximum span per request (e.g. 31 days for
|
|
16
|
+
* 6-minute water levels). We enforce those limits *before* calling NOAA so the
|
|
17
|
+
* agent gets an immediate, actionable error instead of an upstream failure.
|
|
18
|
+
*/
|
|
19
|
+
const SPECIAL_DATES = new Set(["today", "latest", "recent"]);
|
|
20
|
+
/** NOAA-native formats we pass through untouched. */
|
|
21
|
+
const NOAA_DATE_RE = /^(\d{8}|\d{2}\/\d{2}\/\d{4})( \d{2}:\d{2})?$/;
|
|
22
|
+
/** ISO formats we normalize to NOAA's "yyyyMMdd HH:mm". */
|
|
23
|
+
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})([T ](\d{2}):(\d{2}))?/;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a user-supplied date string to a NOAA-accepted format.
|
|
26
|
+
* Throws with the list of accepted formats when unparseable.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeDate(input, paramName) {
|
|
29
|
+
const trimmed = input.trim();
|
|
30
|
+
if (NOAA_DATE_RE.test(trimmed))
|
|
31
|
+
return trimmed;
|
|
32
|
+
const iso = trimmed.match(ISO_DATE_RE);
|
|
33
|
+
if (iso) {
|
|
34
|
+
const base = `${iso[1]}${iso[2]}${iso[3]}`;
|
|
35
|
+
return iso[4] ? `${base} ${iso[5]}:${iso[6]}` : base;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Invalid ${paramName} "${input}". Accepted formats: yyyyMMdd, "yyyyMMdd HH:mm", MM/dd/yyyy, "MM/dd/yyyy HH:mm", or ISO yyyy-MM-dd[THH:mm].`);
|
|
38
|
+
}
|
|
39
|
+
/** Parse a normalized NOAA date string into a Date (UTC interpretation). */
|
|
40
|
+
export function parseNoaaDate(normalized) {
|
|
41
|
+
let year, month, day;
|
|
42
|
+
let hour = 0, minute = 0;
|
|
43
|
+
const timeMatch = normalized.match(/ (\d{2}):(\d{2})$/);
|
|
44
|
+
if (timeMatch) {
|
|
45
|
+
hour = Number(timeMatch[1]);
|
|
46
|
+
minute = Number(timeMatch[2]);
|
|
47
|
+
}
|
|
48
|
+
const datePart = normalized.split(" ")[0];
|
|
49
|
+
if (datePart.includes("/")) {
|
|
50
|
+
const [mm, dd, yyyy] = datePart.split("/");
|
|
51
|
+
year = Number(yyyy);
|
|
52
|
+
month = Number(mm);
|
|
53
|
+
day = Number(dd);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
year = Number(datePart.slice(0, 4));
|
|
57
|
+
month = Number(datePart.slice(4, 6));
|
|
58
|
+
day = Number(datePart.slice(6, 8));
|
|
59
|
+
}
|
|
60
|
+
const date = new Date(Date.UTC(year, month - 1, day, hour, minute));
|
|
61
|
+
if (isNaN(date.getTime())) {
|
|
62
|
+
throw new Error(`Invalid date "${normalized}".`);
|
|
63
|
+
}
|
|
64
|
+
return date;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate that exactly one legal date-parameter combination was supplied,
|
|
68
|
+
* normalize formats, and compute the request span in days when possible.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveDateParams(params) {
|
|
71
|
+
const { date, begin_date, end_date, range } = params;
|
|
72
|
+
if (date !== undefined) {
|
|
73
|
+
if (begin_date || end_date || range !== undefined) {
|
|
74
|
+
throw new Error("Use either `date` (today/latest/recent) OR explicit begin_date/end_date/range — not both.");
|
|
75
|
+
}
|
|
76
|
+
const lower = date.toLowerCase();
|
|
77
|
+
if (!SPECIAL_DATES.has(lower)) {
|
|
78
|
+
throw new Error(`Invalid date "${date}". The \`date\` parameter only accepts: today, latest (most recent single reading), recent (last 72 hours). For a specific day use begin_date and end_date.`);
|
|
79
|
+
}
|
|
80
|
+
return { date: lower, spanDays: lower === "recent" ? 3 : 1 };
|
|
81
|
+
}
|
|
82
|
+
const begin = begin_date !== undefined
|
|
83
|
+
? normalizeDate(begin_date, "begin_date")
|
|
84
|
+
: undefined;
|
|
85
|
+
const end = end_date !== undefined ? normalizeDate(end_date, "end_date") : undefined;
|
|
86
|
+
if (begin && end) {
|
|
87
|
+
if (range !== undefined) {
|
|
88
|
+
throw new Error("Provide begin_date+end_date OR a range — not all three.");
|
|
89
|
+
}
|
|
90
|
+
const beginDt = parseNoaaDate(begin);
|
|
91
|
+
const endDt = parseNoaaDate(end);
|
|
92
|
+
if (endDt <= beginDt) {
|
|
93
|
+
throw new Error("end_date must be after begin_date.");
|
|
94
|
+
}
|
|
95
|
+
const spanDays = (endDt.getTime() - beginDt.getTime()) / 86_400_000;
|
|
96
|
+
return { begin_date: begin, end_date: end, spanDays };
|
|
97
|
+
}
|
|
98
|
+
if (begin && range !== undefined) {
|
|
99
|
+
return { begin_date: begin, range, spanDays: range / 24 };
|
|
100
|
+
}
|
|
101
|
+
if (end && range !== undefined) {
|
|
102
|
+
return { end_date: end, range, spanDays: range / 24 };
|
|
103
|
+
}
|
|
104
|
+
if (range !== undefined) {
|
|
105
|
+
return { range, spanDays: range / 24 };
|
|
106
|
+
}
|
|
107
|
+
if (begin || end) {
|
|
108
|
+
throw new Error("Incomplete date parameters: pair begin_date with end_date (or with range in hours), or pair end_date with range.");
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Missing date parameters. Provide one of: begin_date+end_date, begin_date+range, end_date+range, range alone (hours back from now), or date=today|latest|recent.");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Per-product/interval maximum request spans (days), from NOAA CO-OPS docs.
|
|
114
|
+
* Exceeding these returns an upstream error, so we fail fast with guidance.
|
|
115
|
+
*/
|
|
116
|
+
export const MAX_SPAN_DAYS = {
|
|
117
|
+
one_minute_water_level: 4,
|
|
118
|
+
water_level: 31,
|
|
119
|
+
hourly_height: 366,
|
|
120
|
+
high_low: 366,
|
|
121
|
+
daily_mean: 3653,
|
|
122
|
+
daily_max_min: 3653,
|
|
123
|
+
monthly_mean: 73050,
|
|
124
|
+
"predictions:hilo": 3653,
|
|
125
|
+
predictions: 366,
|
|
126
|
+
currents: 7,
|
|
127
|
+
"currents_predictions:max_slack": 366,
|
|
128
|
+
currents_predictions: 31,
|
|
129
|
+
met: 31,
|
|
130
|
+
air_gap: 31,
|
|
131
|
+
};
|
|
132
|
+
export function assertSpanWithinLimit(spanDays, limitKey, productLabel) {
|
|
133
|
+
if (spanDays === undefined)
|
|
134
|
+
return;
|
|
135
|
+
const limit = MAX_SPAN_DAYS[limitKey];
|
|
136
|
+
if (limit !== undefined && spanDays > limit) {
|
|
137
|
+
throw new Error(`Requested span of ~${Math.ceil(spanDays)} days exceeds NOAA's ${limit}-day maximum per request for ${productLabel}. Split the request into chunks of at most ${limit} days.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryancardin/noaa-tides-currents-mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP server for NOAA CO-OPS Tides and Currents: water levels, tide predictions, currents, meteorology, station metadata, datums, high tide flooding, sea level trends, plus sun and moon calculations",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"noaa-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
9
14
|
"type": "module",
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "tsc",
|
|
12
17
|
"start": "node dist/index.js",
|
|
13
18
|
"start:http": "node dist/index.js --http --port 3000",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"dev": "ts-node-esm src/index.ts",
|
|
17
|
-
"dev:http": "ts-node-esm src/index.ts --http --port 3000",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"dev:http": "tsx src/index.ts --http --port 3000",
|
|
18
21
|
"test": "vitest run",
|
|
22
|
+
"test:live": "node scripts/smoke-live.mjs",
|
|
19
23
|
"format": "prettier --write .",
|
|
20
24
|
"inspector": "npx @modelcontextprotocol/inspector ./dist/index.js"
|
|
21
25
|
},
|
|
@@ -23,11 +27,15 @@
|
|
|
23
27
|
"noaa",
|
|
24
28
|
"tides",
|
|
25
29
|
"currents",
|
|
30
|
+
"co-ops",
|
|
31
|
+
"water-levels",
|
|
32
|
+
"tide-predictions",
|
|
26
33
|
"mcp",
|
|
27
|
-
"
|
|
34
|
+
"model-context-protocol",
|
|
28
35
|
"api"
|
|
29
36
|
],
|
|
30
37
|
"author": "Ryan Cardin",
|
|
38
|
+
"mcpName": "io.github.ryancardin15/noaa-tides-and-currents-mcp",
|
|
31
39
|
"repository": {
|
|
32
40
|
"type": "git",
|
|
33
41
|
"url": "https://github.com/RyanCardin15/NOAA-Tides-And-Currents-MCP.git"
|
|
@@ -40,17 +48,22 @@
|
|
|
40
48
|
"access": "public"
|
|
41
49
|
},
|
|
42
50
|
"license": "MIT",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
43
54
|
"dependencies": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
+
"axios": "^1.7.9",
|
|
57
|
+
"express": "^5.0.0",
|
|
46
58
|
"suncalc": "^1.9.0",
|
|
47
|
-
"zod": "^3.
|
|
59
|
+
"zod": "^3.25.0"
|
|
48
60
|
},
|
|
49
61
|
"devDependencies": {
|
|
50
|
-
"@types/
|
|
62
|
+
"@types/express": "^5.0.0",
|
|
63
|
+
"@types/node": "^22.10.0",
|
|
51
64
|
"prettier": "^3.0.3",
|
|
52
|
-
"
|
|
53
|
-
"typescript": "^5.
|
|
54
|
-
"vitest": "^
|
|
65
|
+
"tsx": "^4.19.2",
|
|
66
|
+
"typescript": "^5.7.2",
|
|
67
|
+
"vitest": "^3.2.4"
|
|
55
68
|
}
|
|
56
69
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"WebFetch(domain:tidesandcurrents.noaa.gov)",
|
|
5
|
-
"WebFetch(domain:api.tidesandcurrents.noaa.gov)",
|
|
6
|
-
"WebFetch(domain:www.tidesandcurrents.noaa.gov)",
|
|
7
|
-
"WebFetch(domain:gofastmcp.com)",
|
|
8
|
-
"WebFetch(domain:github.com)",
|
|
9
|
-
"WebFetch(domain:deepwiki.com)",
|
|
10
|
-
"Bash(npm run build:*)",
|
|
11
|
-
"Bash(npx fastmcp inspect:*)",
|
|
12
|
-
"Bash(ls:*)",
|
|
13
|
-
"Bash(node:*)",
|
|
14
|
-
"Bash(npm test)",
|
|
15
|
-
"Bash(rm:*)",
|
|
16
|
-
"Bash(bash -c \"git status\")",
|
|
17
|
-
"Bash(export:*)",
|
|
18
|
-
"Bash(/usr/bin/git status)",
|
|
19
|
-
"Bash(exec /bin/bash -c \"git checkout -b feature/testing-branch\")",
|
|
20
|
-
"Bash(git checkout:*)",
|
|
21
|
-
"Bash(git add:*)",
|
|
22
|
-
"Bash(git commit:*)",
|
|
23
|
-
"Bash(npx @modelcontextprotocol/inspector:*)",
|
|
24
|
-
"Bash(chmod:*)",
|
|
25
|
-
"Bash(npm pack:*)"
|
|
26
|
-
],
|
|
27
|
-
"deny": []
|
|
28
|
-
}
|
|
29
|
-
}
|
package/CLAUDE.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Development Commands
|
|
6
|
-
|
|
7
|
-
### Building and Running
|
|
8
|
-
- `npm run build` - Build TypeScript to JavaScript in `dist/` directory
|
|
9
|
-
- `npm start` - Start the MCP server (requires build first)
|
|
10
|
-
- `npm run dev` - Run in development mode with ts-node-esm
|
|
11
|
-
- `npm run format` - Format code with Prettier
|
|
12
|
-
- `npm test` - Run tests with Vitest
|
|
13
|
-
|
|
14
|
-
### Testing and Development
|
|
15
|
-
- `npx fastmcp dev dist/index.js` - Test server with fastmcp CLI
|
|
16
|
-
- `npx fastmcp inspect dist/index.js` - Inspect server capabilities
|
|
17
|
-
|
|
18
|
-
## Architecture Overview
|
|
19
|
-
|
|
20
|
-
This is a FastMCP server that provides tools for accessing NOAA Tides and Currents data, moon phase information, and sun position calculations.
|
|
21
|
-
|
|
22
|
-
### Core Structure
|
|
23
|
-
- **Entry Point**: `src/index.ts` - Creates server, registers tools, and starts stdio transport
|
|
24
|
-
- **Server Configuration**: `src/server/config.ts` - FastMCP server setup with fixed stdio transport
|
|
25
|
-
- **Tool Registration**: `src/tools/index.ts` - Centralized tool registration hub
|
|
26
|
-
|
|
27
|
-
### Service Layer
|
|
28
|
-
The application follows a service-oriented architecture:
|
|
29
|
-
- `NoaaService` - Handles all NOAA API interactions (data and metadata APIs)
|
|
30
|
-
- `MoonPhaseService` - Calculates moon phases and lunar information
|
|
31
|
-
- `SunService` - Calculates sun position, rise/set times using suncalc library
|
|
32
|
-
- `NoaaParametersService` - Provides parameter definitions for NOAA API
|
|
33
|
-
|
|
34
|
-
### Tool Categories
|
|
35
|
-
Tools are organized by functional area:
|
|
36
|
-
- **Water Tools** (`water-tools.ts`) - Water levels, tide predictions, currents
|
|
37
|
-
- **Station Tools** (`station-tools.ts`) - Station metadata and information
|
|
38
|
-
- **Moon Tools** (`moon-tools.ts`) - Moon phase calculations
|
|
39
|
-
- **Sun Tools** (`sun-tools.ts`) - Sun position and event calculations
|
|
40
|
-
- **Parameter Tools** (`parameter-tools.ts`) - API parameter definitions
|
|
41
|
-
|
|
42
|
-
### Data Validation
|
|
43
|
-
- Uses Zod schemas for parameter validation in `src/schemas/common.ts`
|
|
44
|
-
- Common validation patterns for dates, stations, units, formats, etc.
|
|
45
|
-
- Refined validation for date parameter combinations
|
|
46
|
-
|
|
47
|
-
### API Integration
|
|
48
|
-
- **NOAA Data API**: `https://api.tidesandcurrents.noaa.gov/api/prod/datagetter`
|
|
49
|
-
- **NOAA Metadata API**: `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi`
|
|
50
|
-
- Uses axios for HTTP requests with proper error handling
|
|
51
|
-
|
|
52
|
-
### Key Dependencies
|
|
53
|
-
- `fastmcp` - MCP server framework
|
|
54
|
-
- `axios` - HTTP client for NOAA API calls
|
|
55
|
-
- `suncalc` - Sun position and timing calculations
|
|
56
|
-
- `zod` - Schema validation
|
|
57
|
-
- TypeScript with ES modules and Node16 module resolution
|
|
58
|
-
|
|
59
|
-
## Important Implementation Notes
|
|
60
|
-
|
|
61
|
-
### MCP Server Configuration
|
|
62
|
-
The server uses stdio transport exclusively - do not modify to use other transports as this is designed for MCP client integration.
|
|
63
|
-
|
|
64
|
-
### Error Handling
|
|
65
|
-
NOAA API errors are caught and re-thrown with structured error messages including status codes and response data.
|
|
66
|
-
|
|
67
|
-
### Date Handling
|
|
68
|
-
The codebase supports multiple date formats and has specific validation logic for date parameter combinations (date vs begin_date/end_date).
|
|
69
|
-
|
|
70
|
-
### Tool Organization
|
|
71
|
-
Each tool category has its own registration function that accepts the server instance and relevant service(s). This modular approach makes it easy to add new tools or modify existing ones.
|
package/Dockerfile
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
|
|
2
|
-
FROM node:lts-alpine
|
|
3
|
-
WORKDIR /app
|
|
4
|
-
|
|
5
|
-
# Install dependencies
|
|
6
|
-
COPY package*.json ./
|
|
7
|
-
RUN npm install --ignore-scripts
|
|
8
|
-
|
|
9
|
-
# Copy source code and build the project
|
|
10
|
-
COPY . .
|
|
11
|
-
RUN npm run build
|
|
12
|
-
|
|
13
|
-
# Start the server in stdio mode
|
|
14
|
-
CMD [ "npm", "start" ]
|
package/smithery.yaml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
|
2
|
-
|
|
3
|
-
startCommand:
|
|
4
|
-
type: stdio
|
|
5
|
-
configSchema:
|
|
6
|
-
# Minimal JSON Schema for configuration
|
|
7
|
-
type: object
|
|
8
|
-
properties: {}
|
|
9
|
-
exampleConfig: {}
|
|
10
|
-
commandFunction:
|
|
11
|
-
# Simple command to start the MCP server on stdio without additional configuration
|
|
12
|
-
|-
|
|
13
|
-
() => ({
|
|
14
|
-
command: 'node',
|
|
15
|
-
args: ['dist/index.js']
|
|
16
|
-
})
|