@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.
Files changed (88) hide show
  1. package/README.md +164 -199
  2. package/dist/client/cache.d.ts +15 -0
  3. package/dist/client/cache.js +44 -0
  4. package/dist/client/http.d.ts +28 -0
  5. package/dist/client/http.js +178 -0
  6. package/dist/constants.d.ts +28 -0
  7. package/dist/constants.js +28 -0
  8. package/dist/format/respond.d.ts +25 -0
  9. package/dist/format/respond.js +40 -0
  10. package/dist/format/series.d.ts +16 -0
  11. package/dist/format/series.js +42 -0
  12. package/dist/format/units.d.ts +16 -0
  13. package/dist/format/units.js +37 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.js +78 -8
  16. package/dist/interfaces/moon.d.ts +4 -4
  17. package/dist/interfaces/moon.js +58 -17
  18. package/dist/interfaces/sun.d.ts +4 -4
  19. package/dist/interfaces/sun.js +94 -25
  20. package/dist/prompts/index.d.ts +6 -0
  21. package/dist/prompts/index.js +89 -0
  22. package/dist/reference/content.d.ts +10 -0
  23. package/dist/reference/content.js +202 -0
  24. package/dist/resources/index.d.ts +7 -0
  25. package/dist/resources/index.js +73 -0
  26. package/dist/schemas/common.d.ts +38 -14
  27. package/dist/schemas/common.js +82 -18
  28. package/dist/services/data-api.d.ts +81 -0
  29. package/dist/services/data-api.js +117 -0
  30. package/dist/services/dpapi.d.ts +55 -0
  31. package/dist/services/dpapi.js +60 -0
  32. package/dist/services/metadata-api.d.ts +62 -0
  33. package/dist/services/metadata-api.js +105 -0
  34. package/dist/services/moon-phase-service.d.ts +2 -2
  35. package/dist/services/moon-phase-service.js +23 -25
  36. package/dist/services/sun-service.d.ts +2 -2
  37. package/dist/services/sun-service.js +53 -31
  38. package/dist/tools/astronomy.d.ts +7 -0
  39. package/dist/tools/astronomy.js +270 -0
  40. package/dist/tools/currents.d.ts +5 -0
  41. package/dist/tools/currents.js +168 -0
  42. package/dist/tools/derived.d.ts +6 -0
  43. package/dist/tools/derived.js +296 -0
  44. package/dist/tools/index.d.ts +3 -14
  45. package/dist/tools/index.js +17 -32
  46. package/dist/tools/met.d.ts +5 -0
  47. package/dist/tools/met.js +110 -0
  48. package/dist/tools/reference.d.ts +6 -0
  49. package/dist/tools/reference.js +33 -0
  50. package/dist/tools/station-metadata.d.ts +6 -0
  51. package/dist/tools/station-metadata.js +208 -0
  52. package/dist/tools/stations.d.ts +5 -0
  53. package/dist/tools/stations.js +265 -0
  54. package/dist/tools/water.d.ts +5 -0
  55. package/dist/tools/water.js +270 -0
  56. package/dist/validation/dates.d.ts +50 -0
  57. package/dist/validation/dates.js +139 -0
  58. package/package.json +27 -14
  59. package/.claude/settings.local.json +0 -29
  60. package/CLAUDE.md +0 -71
  61. package/Dockerfile +0 -14
  62. package/smithery.yaml +0 -16
  63. package/src/index.ts +0 -13
  64. package/src/interfaces/moon.ts +0 -44
  65. package/src/interfaces/noaa.ts +0 -130
  66. package/src/interfaces/parameters.ts +0 -20
  67. package/src/interfaces/sun.ts +0 -57
  68. package/src/schemas/common.ts +0 -23
  69. package/src/schemas/dpapi.ts +0 -99
  70. package/src/server/config.ts +0 -43
  71. package/src/server/mcp-server.ts +0 -135
  72. package/src/services/dpapi-service.ts +0 -187
  73. package/src/services/moon-phase-service.ts +0 -167
  74. package/src/services/noaa-parameters-service.ts +0 -139
  75. package/src/services/noaa-service.ts +0 -171
  76. package/src/services/sun-service.ts +0 -275
  77. package/src/tools/derived-product-tools.ts +0 -180
  78. package/src/tools/index.ts +0 -40
  79. package/src/tools/moon-tools.ts +0 -79
  80. package/src/tools/parameter-tools.ts +0 -82
  81. package/src/tools/station-tools.ts +0 -57
  82. package/src/tools/sun-tools.ts +0 -120
  83. package/src/tools/water-tools.ts +0 -166
  84. package/src/types/moon.ts +0 -27
  85. package/src/types/sun.ts +0 -51
  86. package/src/types/suncalc.d.ts +0 -110
  87. package/test-dpapi.js +0 -0
  88. 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": "1.0.0",
4
- "description": "MCP Server that interfaces with NOAA Tides and Currents API using FastMCP",
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
- "start:http:3001": "node dist/index.js --http --port 3001",
15
- "start:http:8080": "node dist/index.js --http --port 8080",
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
- "fastmcp",
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
- "axios": "^1.6.2",
45
- "fastmcp": "^1.16.3",
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.22.4"
59
+ "zod": "^3.25.0"
48
60
  },
49
61
  "devDependencies": {
50
- "@types/node": "^20.10.0",
62
+ "@types/express": "^5.0.0",
63
+ "@types/node": "^22.10.0",
51
64
  "prettier": "^3.0.3",
52
- "ts-node": "^10.9.1",
53
- "typescript": "^5.3.2",
54
- "vitest": "^0.34.6"
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
- })