@ryancardin/noaa-tides-currents-mcp-server 2.0.1 → 2.1.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/LICENSE +1 -1
- package/README.md +18 -5
- package/dist/client/deadline.d.ts +6 -0
- package/dist/client/deadline.js +23 -0
- package/dist/client/http.d.ts +2 -0
- package/dist/client/http.js +75 -4
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +11 -0
- package/dist/format/respond.d.ts +37 -1
- package/dist/format/respond.js +45 -10
- package/dist/index.js +0 -0
- package/dist/reference/content.d.ts +1 -1
- package/dist/reference/content.js +24 -0
- package/dist/resources/index.js +8 -0
- package/dist/services/nws-api.d.ts +115 -0
- package/dist/services/nws-api.js +267 -0
- package/dist/tools/astronomy.js +6 -1
- package/dist/tools/currents.js +3 -1
- package/dist/tools/derived.js +6 -1
- package/dist/tools/index.js +12 -0
- package/dist/tools/marine-forecast.d.ts +7 -0
- package/dist/tools/marine-forecast.js +177 -0
- package/dist/tools/met.js +2 -1
- package/dist/tools/reference.js +4 -0
- package/dist/tools/station-metadata.js +4 -1
- package/dist/tools/stations.js +4 -1
- package/dist/tools/water.js +4 -1
- package/package.json +5 -5
- package/dist/interfaces/config.d.ts +0 -6
- package/dist/interfaces/config.js +0 -1
- package/dist/interfaces/noaa.d.ts +0 -334
- package/dist/interfaces/noaa.js +0 -98
- package/dist/interfaces/parameters.d.ts +0 -18
- package/dist/interfaces/parameters.js +0 -5
- package/dist/mcp-server.d.ts +0 -12
- package/dist/mcp-server.js +0 -103
- package/dist/moon-phase-service.d.ts +0 -122
- package/dist/moon-phase-service.js +0 -187
- package/dist/noaa-service.d.ts +0 -60
- package/dist/noaa-service.js +0 -159
- package/dist/schemas/dpapi.d.ts +0 -198
- package/dist/schemas/dpapi.js +0 -89
- package/dist/server/config.d.ts +0 -9
- package/dist/server/config.js +0 -40
- package/dist/server/mcp-server.d.ts +0 -12
- package/dist/server/mcp-server.js +0 -103
- package/dist/services/dpapi-service.d.ts +0 -72
- package/dist/services/dpapi-service.js +0 -164
- package/dist/services/noaa-parameters-service.d.ts +0 -76
- package/dist/services/noaa-parameters-service.js +0 -128
- package/dist/services/noaa-service.d.ts +0 -52
- package/dist/services/noaa-service.js +0 -151
- package/dist/sun-service.d.ts +0 -184
- package/dist/sun-service.js +0 -218
- package/dist/tools/derived-product-tools.d.ts +0 -6
- package/dist/tools/derived-product-tools.js +0 -168
- package/dist/tools/moon-tools.d.ts +0 -6
- package/dist/tools/moon-tools.js +0 -69
- package/dist/tools/parameter-tools.d.ts +0 -6
- package/dist/tools/parameter-tools.js +0 -77
- package/dist/tools/station-tools.d.ts +0 -6
- package/dist/tools/station-tools.js +0 -51
- package/dist/tools/sun-tools.d.ts +0 -6
- package/dist/tools/sun-tools.js +0 -109
- package/dist/tools/water-tools.d.ts +0 -6
- package/dist/tools/water-tools.js +0 -150
- package/dist/types.d.ts +0 -337
- package/dist/types.js +0 -98
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
**A Model Context Protocol server for NOAA CO-OPS Tides and Currents data**
|
|
11
11
|
|
|
12
|
+
Built by [Cardin Labs](https://cardinlabs.com) · Hosted at [Perigee](https://perigee-two.vercel.app)
|
|
13
|
+
|
|
12
14
|
Water levels · tide predictions · currents · marine weather · station metadata ·
|
|
13
15
|
tidal datums · harmonic constituents · sea level trends & projections ·
|
|
14
16
|
high tide flooding · sun & moon calculations
|
|
@@ -56,7 +58,7 @@ No API key is required — NOAA's CO-OPS APIs are open.
|
|
|
56
58
|
|
|
57
59
|
---
|
|
58
60
|
|
|
59
|
-
## Tools (
|
|
61
|
+
## Tools (25)
|
|
60
62
|
|
|
61
63
|
### Observations & Predictions (Data API)
|
|
62
64
|
|
|
@@ -100,18 +102,25 @@ No API key is required — NOAA's CO-OPS APIs are open.
|
|
|
100
102
|
| `astro_get_sun_position` | Azimuth/altitude (+ approximate declination/RA) |
|
|
101
103
|
| `astro_get_next_sun_event` | Next occurrence(s) of any sun event |
|
|
102
104
|
|
|
105
|
+
### Wind & Marine Forecasts (NWS Weather API)
|
|
106
|
+
|
|
107
|
+
| Tool | What it does |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `nws_get_wind_forecast` | Hourly numeric wind forecast (speed/gust/direction, wave height where gridded) for any US lat/lon, up to ~7 days |
|
|
110
|
+
| `nws_get_marine_forecast` | Official Coastal Waters Forecast narrative for the marine zone covering a lat/lon, incl. Small Craft Advisories |
|
|
111
|
+
|
|
103
112
|
### Reference
|
|
104
113
|
|
|
105
114
|
| Tool | What it does |
|
|
106
115
|
|---|---|
|
|
107
|
-
| `noaa_get_reference_guide` | Curated NOAA reference: products, datums, units, time zones, intervals, station types, data limits, quality flags, date formats |
|
|
116
|
+
| `noaa_get_reference_guide` | Curated NOAA reference: products, datums, units, time zones, intervals, station types, data limits, quality flags, date formats, marine forecasts |
|
|
108
117
|
|
|
109
118
|
Every tool supports `response_format: "markdown"` (readable tables with units spelled out — the default) or `"json"` (complete structured payload), and attaches structured content for MCP clients that consume it.
|
|
110
119
|
|
|
111
120
|
## Resources
|
|
112
121
|
|
|
113
122
|
- `noaa://guide/getting-started` — workflow recipes and common pitfalls
|
|
114
|
-
- `noaa://reference/{topic}` — the
|
|
123
|
+
- `noaa://reference/{topic}` — the ten reference topics above as pinnable resources
|
|
115
124
|
|
|
116
125
|
## Prompts
|
|
117
126
|
|
|
@@ -180,7 +189,7 @@ src/
|
|
|
180
189
|
├── format/ # unit labeling, flag legends, markdown/json response shaping
|
|
181
190
|
├── schemas/ # shared Zod field schemas with nuance-carrying descriptions
|
|
182
191
|
├── services/ # Data API, Metadata API, DPAPI, moon & sun services
|
|
183
|
-
├── tools/ #
|
|
192
|
+
├── tools/ # 25 tool registrations grouped by domain
|
|
184
193
|
├── resources/ # noaa:// reference resources
|
|
185
194
|
├── prompts/ # workflow prompt templates
|
|
186
195
|
└── reference/ # curated NOAA reference content
|
|
@@ -194,6 +203,10 @@ Data sources:
|
|
|
194
203
|
|
|
195
204
|
## License
|
|
196
205
|
|
|
197
|
-
MIT ©
|
|
206
|
+
MIT © [Cardin LLC](https://cardinlabs.com) (Cardin Labs)
|
|
198
207
|
|
|
199
208
|
NOAA data is provided by the NOAA Center for Operational Oceanographic Products and Services (CO-OPS). This project is not affiliated with or endorsed by NOAA.
|
|
209
|
+
|
|
210
|
+
## 2.1.0 tool delivery contract
|
|
211
|
+
|
|
212
|
+
Every raw tool advertises an output schema. Successful provider payloads retain their fields and add `_response: { contractVersion: "2026-09-29.1", status: "complete" }`. Reference guides have a separate typed topic/guide result. Both text and structured payloads are bounded: an oversized result returns an explicit `response_too_large` error with narrowing guidance instead of truncated text alongside an unbounded structured response. Error metadata states whether retry may help. Chained provider reads share a 45-second cancellation deadline and each request has an 18-second total retry budget. No fishing, navigation or catch probability is inferred from raw data.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function providerDeadline(): {
|
|
2
|
+
signal: AbortSignal;
|
|
3
|
+
endsAt: number;
|
|
4
|
+
} | undefined;
|
|
5
|
+
/** Propagates cancellation through nested provider reads and retry backoff. */
|
|
6
|
+
export declare function withProviderDeadline<T>(load: () => Promise<T>, milliseconds: number): Promise<T>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const deadlines = new AsyncLocalStorage();
|
|
3
|
+
export function providerDeadline() {
|
|
4
|
+
return deadlines.getStore();
|
|
5
|
+
}
|
|
6
|
+
/** Propagates cancellation through nested provider reads and retry backoff. */
|
|
7
|
+
export async function withProviderDeadline(load, milliseconds) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const parent = providerDeadline();
|
|
10
|
+
const budget = Math.max(1, Math.min(milliseconds, parent ? parent.endsAt - Date.now() : milliseconds));
|
|
11
|
+
const abort = () => controller.abort();
|
|
12
|
+
parent?.signal.addEventListener("abort", abort, { once: true });
|
|
13
|
+
if (parent?.signal.aborted)
|
|
14
|
+
controller.abort();
|
|
15
|
+
const timer = setTimeout(abort, budget);
|
|
16
|
+
try {
|
|
17
|
+
return await deadlines.run({ signal: controller.signal, endsAt: Date.now() + budget }, load);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
parent?.signal.removeEventListener("abort", abort);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/client/http.d.ts
CHANGED
|
@@ -24,5 +24,7 @@ export declare function cleanParams(params: Record<string, string | number | boo
|
|
|
24
24
|
export declare function fetchDataApi<T = Record<string, unknown>>(params: Record<string, string | number | boolean | undefined | null>): Promise<T>;
|
|
25
25
|
/** Metadata API GET: path like "/stations.json" or "/stations/8454000/datums.json". */
|
|
26
26
|
export declare function fetchMetadataApi<T = Record<string, unknown>>(path: string, params?: Record<string, string | number | boolean | undefined | null>): Promise<T>;
|
|
27
|
+
/** NWS Weather API GET: path like "/points/28.77,-95.62" or "/products/{id}". */
|
|
28
|
+
export declare function fetchNwsApi<T = Record<string, unknown>>(path: string, params?: Record<string, string | number | boolean | undefined | null>): Promise<T>;
|
|
27
29
|
/** Derived Product API GET: path like "/htf/htf_annual.json". */
|
|
28
30
|
export declare function fetchDpapi<T = Record<string, unknown>>(path: string, params?: Record<string, string | number | boolean | undefined | null>): Promise<T>;
|
package/dist/client/http.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Metadata API returns bare 404s with no body — they are handled
|
|
10
10
|
* differently on purpose)
|
|
11
11
|
*/
|
|
12
|
+
import { providerDeadline } from "./deadline.js";
|
|
12
13
|
import axios from "axios";
|
|
13
|
-
import { APPLICATION_NAME, DATA_API_BASE_URL, DPAPI_BASE_URL, MAX_RETRIES, METADATA_API_BASE_URL, REQUEST_TIMEOUT_MS, } from "../constants.js";
|
|
14
|
+
import { APPLICATION_NAME, DATA_API_BASE_URL, DPAPI_BASE_URL, MAX_RETRIES, METADATA_API_BASE_URL, NWS_API_BASE_URL, NWS_USER_AGENT, REQUEST_TIMEOUT_MS, } from "../constants.js";
|
|
14
15
|
/** Error thrown for any NOAA API failure, with a message safe to show the agent. */
|
|
15
16
|
export class NoaaApiError extends Error {
|
|
16
17
|
status;
|
|
@@ -27,6 +28,15 @@ const http = axios.create({
|
|
|
27
28
|
"User-Agent": `${APPLICATION_NAME}/2.0`,
|
|
28
29
|
},
|
|
29
30
|
});
|
|
31
|
+
// Separate instance for the NWS Weather API: NWS mandates a descriptive
|
|
32
|
+
// User-Agent with contact info, and serves GeoJSON rather than plain JSON.
|
|
33
|
+
const nwsHttp = axios.create({
|
|
34
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
35
|
+
headers: {
|
|
36
|
+
Accept: "application/geo+json, application/ld+json, application/json",
|
|
37
|
+
"User-Agent": NWS_USER_AGENT,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
30
40
|
function sleep(ms) {
|
|
31
41
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
42
|
}
|
|
@@ -48,16 +58,29 @@ export function cleanParams(params) {
|
|
|
48
58
|
}
|
|
49
59
|
return out;
|
|
50
60
|
}
|
|
51
|
-
async function getWithRetry(url, params) {
|
|
61
|
+
async function getWithRetry(url, params, instance = http) {
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
const budgetMs = 18_000;
|
|
52
64
|
let lastError;
|
|
53
65
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
54
66
|
try {
|
|
55
|
-
const
|
|
67
|
+
const deadline = providerDeadline();
|
|
68
|
+
const remainingMs = Math.min(budgetMs - (Date.now() - startedAt), deadline ? deadline.endsAt - Date.now() : Infinity);
|
|
69
|
+
if (remainingMs <= 0 || deadline?.signal.aborted)
|
|
70
|
+
throw new NoaaApiError("Provider request deadline reached.", 504);
|
|
71
|
+
const response = await instance.get(url, {
|
|
72
|
+
params,
|
|
73
|
+
timeout: Math.min(REQUEST_TIMEOUT_MS, remainingMs),
|
|
74
|
+
signal: deadline?.signal,
|
|
75
|
+
});
|
|
56
76
|
return response.data;
|
|
57
77
|
}
|
|
58
78
|
catch (error) {
|
|
59
79
|
lastError = error;
|
|
60
|
-
if (attempt < MAX_RETRIES &&
|
|
80
|
+
if (attempt < MAX_RETRIES &&
|
|
81
|
+
!providerDeadline()?.signal.aborted &&
|
|
82
|
+
isRetryable(error) &&
|
|
83
|
+
500 * Math.pow(3, attempt) < budgetMs - (Date.now() - startedAt)) {
|
|
61
84
|
await sleep(500 * Math.pow(3, attempt)); // 500ms, 1.5s
|
|
62
85
|
continue;
|
|
63
86
|
}
|
|
@@ -160,6 +183,54 @@ export async function fetchMetadataApi(path, params = {}) {
|
|
|
160
183
|
throw toNoaaError(error, "Metadata API");
|
|
161
184
|
}
|
|
162
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Shape of NWS API error bodies: RFC 7807 problem+json —
|
|
188
|
+
* {"type": "...", "title": "...", "status": 404, "detail": "..."}.
|
|
189
|
+
*/
|
|
190
|
+
function extractNwsProblem(data) {
|
|
191
|
+
if (data && typeof data === "object") {
|
|
192
|
+
const body = data;
|
|
193
|
+
const parts = [body.title, body.detail].filter((s) => typeof s === "string" && s.length > 0);
|
|
194
|
+
if (parts.length > 0)
|
|
195
|
+
return parts.join(": ");
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
function toNwsError(error) {
|
|
200
|
+
if (axios.isAxiosError(error)) {
|
|
201
|
+
const axErr = error;
|
|
202
|
+
if (axErr.response) {
|
|
203
|
+
const status = axErr.response.status;
|
|
204
|
+
const problem = extractNwsProblem(axErr.response.data);
|
|
205
|
+
if (problem) {
|
|
206
|
+
return new NoaaApiError(`NWS API error: ${problem}`, status);
|
|
207
|
+
}
|
|
208
|
+
if (status === 404) {
|
|
209
|
+
return new NoaaApiError("NWS API error: not found (404). NWS forecasts cover the US and its territories only — the point may be outside coverage, or too far offshore for a gridpoint.", status);
|
|
210
|
+
}
|
|
211
|
+
if (status === 429) {
|
|
212
|
+
return new NoaaApiError("NWS API error: rate limited (429). Wait a few seconds and retry.", status);
|
|
213
|
+
}
|
|
214
|
+
return new NoaaApiError(`NWS API error: HTTP ${status}.`, status);
|
|
215
|
+
}
|
|
216
|
+
if (axErr.code === "ECONNABORTED") {
|
|
217
|
+
return new NoaaApiError(`NWS API error: request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. Retry.`);
|
|
218
|
+
}
|
|
219
|
+
return new NoaaApiError(`NWS API error: network failure (${axErr.code ?? "unknown"}).`);
|
|
220
|
+
}
|
|
221
|
+
return new NoaaApiError(`NWS API error: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
|
+
}
|
|
223
|
+
/** NWS Weather API GET: path like "/points/28.77,-95.62" or "/products/{id}". */
|
|
224
|
+
export async function fetchNwsApi(path, params = {}) {
|
|
225
|
+
try {
|
|
226
|
+
return await getWithRetry(`${NWS_API_BASE_URL}${path}`, cleanParams(params), nwsHttp);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error instanceof NoaaApiError)
|
|
230
|
+
throw error;
|
|
231
|
+
throw toNwsError(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
163
234
|
/** Derived Product API GET: path like "/htf/htf_annual.json". */
|
|
164
235
|
export async function fetchDpapi(path, params = {}) {
|
|
165
236
|
try {
|
package/dist/constants.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export declare const DATA_API_BASE_URL = "https://api.tidesandcurrents.noaa.gov/
|
|
|
7
7
|
export declare const METADATA_API_BASE_URL = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi";
|
|
8
8
|
/** NOAA CO-OPS Derived Product API (sea level trends, high tide flooding...). */
|
|
9
9
|
export declare const DPAPI_BASE_URL = "https://api.tidesandcurrents.noaa.gov/dpapi/prod";
|
|
10
|
+
/** NWS Weather API (point/gridpoint forecasts, marine zone text products). */
|
|
11
|
+
export declare const NWS_API_BASE_URL = "https://api.weather.gov";
|
|
12
|
+
/**
|
|
13
|
+
* NWS requires a descriptive User-Agent identifying the application and a
|
|
14
|
+
* contact point (their abuse-mitigation mechanism; the API has no keys).
|
|
15
|
+
*/
|
|
16
|
+
export declare const NWS_USER_AGENT = "noaa-tides-currents-mcp-server (perigeetides.com, ryandcardin@gmail.com)";
|
|
10
17
|
/**
|
|
11
18
|
* Sent as the `application` parameter on every Data API call so NOAA can
|
|
12
19
|
* attribute traffic in their logs (not an API key; the API is open).
|
|
@@ -25,4 +32,8 @@ export declare const CACHE_TTL: {
|
|
|
25
32
|
readonly stationList: number;
|
|
26
33
|
/** Individual station metadata (datums, sensors, harcon...). */
|
|
27
34
|
readonly stationResource: number;
|
|
35
|
+
/** Lat/lon → NWS gridpoint resolution (grid assignments drift rarely, but do drift). */
|
|
36
|
+
readonly nwsPoint: number;
|
|
37
|
+
/** NWS forecasts and marine text products update roughly hourly. */
|
|
38
|
+
readonly nwsForecast: number;
|
|
28
39
|
};
|
package/dist/constants.js
CHANGED
|
@@ -7,6 +7,13 @@ export const DATA_API_BASE_URL = "https://api.tidesandcurrents.noaa.gov/api/prod
|
|
|
7
7
|
export const METADATA_API_BASE_URL = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi";
|
|
8
8
|
/** NOAA CO-OPS Derived Product API (sea level trends, high tide flooding...). */
|
|
9
9
|
export const DPAPI_BASE_URL = "https://api.tidesandcurrents.noaa.gov/dpapi/prod";
|
|
10
|
+
/** NWS Weather API (point/gridpoint forecasts, marine zone text products). */
|
|
11
|
+
export const NWS_API_BASE_URL = "https://api.weather.gov";
|
|
12
|
+
/**
|
|
13
|
+
* NWS requires a descriptive User-Agent identifying the application and a
|
|
14
|
+
* contact point (their abuse-mitigation mechanism; the API has no keys).
|
|
15
|
+
*/
|
|
16
|
+
export const NWS_USER_AGENT = "noaa-tides-currents-mcp-server (perigeetides.com, ryandcardin@gmail.com)";
|
|
10
17
|
/**
|
|
11
18
|
* Sent as the `application` parameter on every Data API call so NOAA can
|
|
12
19
|
* attribute traffic in their logs (not an API key; the API is open).
|
|
@@ -25,4 +32,8 @@ export const CACHE_TTL = {
|
|
|
25
32
|
stationList: 6 * 60 * 60 * 1000,
|
|
26
33
|
/** Individual station metadata (datums, sensors, harcon...). */
|
|
27
34
|
stationResource: 60 * 60 * 1000,
|
|
35
|
+
/** Lat/lon → NWS gridpoint resolution (grid assignments drift rarely, but do drift). */
|
|
36
|
+
nwsPoint: 6 * 60 * 60 * 1000,
|
|
37
|
+
/** NWS forecasts and marine text products update roughly hourly. */
|
|
38
|
+
nwsForecast: 30 * 60 * 1000,
|
|
28
39
|
};
|
package/dist/format/respond.d.ts
CHANGED
|
@@ -5,8 +5,44 @@
|
|
|
5
5
|
* tables with units spelled out) or "json" (complete structured payload).
|
|
6
6
|
* Structured content is attached in both modes so MCP clients that support
|
|
7
7
|
* it can consume typed output. Responses longer than CHARACTER_LIMIT are
|
|
8
|
-
*
|
|
8
|
+
* rejected with narrowing guidance, including their structured payload.
|
|
9
9
|
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
/** Stable delivery contract; provider/domain fields remain additive. */
|
|
12
|
+
export declare const RawToolOutputSchema: z.ZodObject<{
|
|
13
|
+
_response: z.ZodObject<{
|
|
14
|
+
contractVersion: z.ZodLiteral<"2026-09-29.1">;
|
|
15
|
+
status: z.ZodLiteral<"complete">;
|
|
16
|
+
}, "strict", z.ZodTypeAny, {
|
|
17
|
+
status: "complete";
|
|
18
|
+
contractVersion: "2026-09-29.1";
|
|
19
|
+
}, {
|
|
20
|
+
status: "complete";
|
|
21
|
+
contractVersion: "2026-09-29.1";
|
|
22
|
+
}>;
|
|
23
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
24
|
+
_response: z.ZodObject<{
|
|
25
|
+
contractVersion: z.ZodLiteral<"2026-09-29.1">;
|
|
26
|
+
status: z.ZodLiteral<"complete">;
|
|
27
|
+
}, "strict", z.ZodTypeAny, {
|
|
28
|
+
status: "complete";
|
|
29
|
+
contractVersion: "2026-09-29.1";
|
|
30
|
+
}, {
|
|
31
|
+
status: "complete";
|
|
32
|
+
contractVersion: "2026-09-29.1";
|
|
33
|
+
}>;
|
|
34
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
35
|
+
_response: z.ZodObject<{
|
|
36
|
+
contractVersion: z.ZodLiteral<"2026-09-29.1">;
|
|
37
|
+
status: z.ZodLiteral<"complete">;
|
|
38
|
+
}, "strict", z.ZodTypeAny, {
|
|
39
|
+
status: "complete";
|
|
40
|
+
contractVersion: "2026-09-29.1";
|
|
41
|
+
}, {
|
|
42
|
+
status: "complete";
|
|
43
|
+
contractVersion: "2026-09-29.1";
|
|
44
|
+
}>;
|
|
45
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
10
46
|
export type ResponseFormat = "markdown" | "json";
|
|
11
47
|
export interface ToolResult {
|
|
12
48
|
[key: string]: unknown;
|
package/dist/format/respond.js
CHANGED
|
@@ -5,27 +5,62 @@
|
|
|
5
5
|
* tables with units spelled out) or "json" (complete structured payload).
|
|
6
6
|
* Structured content is attached in both modes so MCP clients that support
|
|
7
7
|
* it can consume typed output. Responses longer than CHARACTER_LIMIT are
|
|
8
|
-
*
|
|
8
|
+
* rejected with narrowing guidance, including their structured payload.
|
|
9
9
|
*/
|
|
10
|
+
import { z } from "zod";
|
|
10
11
|
import { CHARACTER_LIMIT } from "../constants.js";
|
|
12
|
+
/** Stable delivery contract; provider/domain fields remain additive. */
|
|
13
|
+
export const RawToolOutputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
_response: z
|
|
16
|
+
.object({
|
|
17
|
+
contractVersion: z.literal("2026-09-29.1"),
|
|
18
|
+
status: z.literal("complete"),
|
|
19
|
+
})
|
|
20
|
+
.strict(),
|
|
21
|
+
})
|
|
22
|
+
.passthrough();
|
|
11
23
|
/** Build a successful tool response in the requested format. */
|
|
12
24
|
export function respond(format, structured, markdown) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
text.slice(0, CHARACTER_LIMIT) +
|
|
17
|
-
"\n\n…[truncated: response exceeded the size limit. Narrow the date range, lower the limit parameter, or request fewer fields.]";
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
content: [{ type: "text", text }],
|
|
21
|
-
structuredContent: structured,
|
|
25
|
+
const payload = {
|
|
26
|
+
...structured,
|
|
27
|
+
_response: { contractVersion: "2026-09-29.1", status: "complete" },
|
|
22
28
|
};
|
|
29
|
+
const json = JSON.stringify(payload);
|
|
30
|
+
const text = format === "json" ? json : markdown;
|
|
31
|
+
if (json.length > CHARACTER_LIMIT || text.length > CHARACTER_LIMIT) {
|
|
32
|
+
return {
|
|
33
|
+
isError: true,
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: "The complete response exceeds the delivery limit. Narrow the date range, lower the limit, or request fewer fields; no partial result is presented as complete.",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
_meta: {
|
|
41
|
+
error: {
|
|
42
|
+
code: "response_too_large",
|
|
43
|
+
retryable: false,
|
|
44
|
+
limitCharacters: CHARACTER_LIMIT,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { content: [{ type: "text", text }], structuredContent: payload };
|
|
23
50
|
}
|
|
24
51
|
/** Build an error tool response (kept inside the result per MCP guidance). */
|
|
25
52
|
export function respondError(error) {
|
|
26
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
27
54
|
return {
|
|
28
55
|
isError: true,
|
|
56
|
+
_meta: {
|
|
57
|
+
error: {
|
|
58
|
+
code: "provider_error",
|
|
59
|
+
retryable: error?.status === undefined ||
|
|
60
|
+
error.status === 429 ||
|
|
61
|
+
(error.status ?? 0) >= 500,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
29
64
|
content: [{ type: "text", text: `Error: ${message}` }],
|
|
30
65
|
};
|
|
31
66
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* documentation (api.tidesandcurrents.noaa.gov). Served both by the
|
|
4
4
|
* noaa_get_reference_guide tool and as noaa://reference/{topic} resources.
|
|
5
5
|
*/
|
|
6
|
-
export declare const REFERENCE_TOPICS: readonly ["products", "datums", "units", "time_zones", "intervals", "station_types", "data_limits", "quality_flags", "date_formats"];
|
|
6
|
+
export declare const REFERENCE_TOPICS: readonly ["products", "datums", "units", "time_zones", "intervals", "station_types", "data_limits", "quality_flags", "date_formats", "marine_forecast"];
|
|
7
7
|
export type ReferenceTopic = (typeof REFERENCE_TOPICS)[number];
|
|
8
8
|
export declare const REFERENCE_CONTENT: Record<ReferenceTopic, string>;
|
|
9
9
|
/** One-line summaries used in resource listings and the guide index. */
|
|
@@ -13,6 +13,7 @@ export const REFERENCE_TOPICS = [
|
|
|
13
13
|
"data_limits",
|
|
14
14
|
"quality_flags",
|
|
15
15
|
"date_formats",
|
|
16
|
+
"marine_forecast",
|
|
16
17
|
];
|
|
17
18
|
export const REFERENCE_CONTENT = {
|
|
18
19
|
products: `# NOAA CO-OPS Data Products
|
|
@@ -187,6 +188,28 @@ Exactly ONE of these combinations per request:
|
|
|
187
188
|
5. range alone — N hours back from now
|
|
188
189
|
|
|
189
190
|
Timestamps in responses are in the requested time_zone (gmt | lst | lst_ldt).`,
|
|
191
|
+
marine_forecast: `# Wind & Marine Forecasts (NWS)
|
|
192
|
+
|
|
193
|
+
Forecasts come from the NWS Weather API (api.weather.gov) — a different NOAA
|
|
194
|
+
service from CO-OPS. CO-OPS observes and predicts tides; NWS forecasts weather.
|
|
195
|
+
|
|
196
|
+
| Need | Tool |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Hourly wind forecast (numeric speed/gust/direction) at a lat/lon | nws_get_wind_forecast |
|
|
199
|
+
| Official marine narrative (Coastal Waters Forecast, advisories) | nws_get_marine_forecast |
|
|
200
|
+
| OBSERVED wind right now at a NOAA station | noaa_get_meteorological_data (product "wind") |
|
|
201
|
+
|
|
202
|
+
How it works:
|
|
203
|
+
- A lat/lon resolves to a forecast-office gridpoint (points → gridpoints);
|
|
204
|
+
values are numeric NDFD series with up to a ~7-day (156 h) horizon.
|
|
205
|
+
- Wave height appears on marine/nearshore gridpoints; swell period/direction
|
|
206
|
+
are generally only populated for open-ocean grids, not bays.
|
|
207
|
+
- Marine text forecasts are per-ZONE (e.g. GMZ350 "Freeport to Matagorda Ship
|
|
208
|
+
Channel out 20 NM"); one office bulletin covers many zones and the tool
|
|
209
|
+
extracts the matching segment. Winds in the narrative are in KNOTS.
|
|
210
|
+
- Coverage: US and territories only. No API key; forecasts update ~hourly.
|
|
211
|
+
- Forecast wind direction is where the wind blows FROM, degrees true — same
|
|
212
|
+
convention as CO-OPS wind observations.`,
|
|
190
213
|
};
|
|
191
214
|
/** One-line summaries used in resource listings and the guide index. */
|
|
192
215
|
export const REFERENCE_SUMMARIES = {
|
|
@@ -199,4 +222,5 @@ export const REFERENCE_SUMMARIES = {
|
|
|
199
222
|
data_limits: "Maximum date-range span per product",
|
|
200
223
|
quality_flags: "Data quality fields (v/s/f/q), flag letters, HH/H/L/LL",
|
|
201
224
|
date_formats: "Accepted date formats and parameter combinations",
|
|
225
|
+
marine_forecast: "NWS wind & marine forecast tools vs CO-OPS observations",
|
|
202
226
|
};
|
package/dist/resources/index.js
CHANGED
|
@@ -19,6 +19,14 @@ Typical workflows:
|
|
|
19
19
|
- noaa_get_meteorological_data (product "wind"/"air_temperature", date "latest")
|
|
20
20
|
- noaa_get_currents (date "latest") at a current station (different ID scheme!)
|
|
21
21
|
|
|
22
|
+
## "What will the wind do tomorrow near <place>?"
|
|
23
|
+
1. Geocode the place to lat/lon (outside this server).
|
|
24
|
+
2. nws_get_wind_forecast (hours 48) → hourly numeric speed/gust/direction.
|
|
25
|
+
3. nws_get_marine_forecast for the official Coastal Waters Forecast narrative
|
|
26
|
+
(small-craft advisories, sea state) when the point is in US coastal waters.
|
|
27
|
+
Forecast ≠ observation: measured wind right now comes from
|
|
28
|
+
noaa_get_meteorological_data (product "wind") at a station with a met sensor.
|
|
29
|
+
|
|
22
30
|
## "How often does <station> flood, and what's projected?"
|
|
23
31
|
- noaa_get_high_tide_flooding (report "annual", range 15)
|
|
24
32
|
- noaa_get_sea_level_trends, noaa_get_sea_level_rise_projections
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NWS Weather API (api.weather.gov) — wind and marine FORECASTS.
|
|
3
|
+
*
|
|
4
|
+
* This is a different NOAA service from CO-OPS with different shapes:
|
|
5
|
+
* - /points/{lat},{lon} resolves a coordinate to a forecast office gridpoint
|
|
6
|
+
* (gridX/gridY assignments drift over re-gridding, so cache with a TTL and
|
|
7
|
+
* never hardcode them).
|
|
8
|
+
* - /gridpoints/{office}/{x},{y} returns NUMERIC time series (the friendlier
|
|
9
|
+
* /forecast/hourly endpoint returns display strings like "10 to 15 mph"
|
|
10
|
+
* and is deliberately not used). Each series value carries a validTime of
|
|
11
|
+
* the form "2026-07-05T18:00:00+00:00/PT3H" — an ISO instant plus an ISO
|
|
12
|
+
* duration the value holds for — which we expand into hourly samples.
|
|
13
|
+
* - Marine zone TEXT forecasts are NOT served by /zones/forecast/{id}/forecast
|
|
14
|
+
* (that 404s with "Marine Forecast Not Supported"); the working path is the
|
|
15
|
+
* Products API: latest CWF product for the issuing office, then extracting
|
|
16
|
+
* the segment for the zone from the multi-zone bulletin.
|
|
17
|
+
*/
|
|
18
|
+
export interface NwsPoint {
|
|
19
|
+
gridId: string;
|
|
20
|
+
gridX: number;
|
|
21
|
+
gridY: number;
|
|
22
|
+
/** Issuing forecast office, e.g. "HGX". */
|
|
23
|
+
cwa: string;
|
|
24
|
+
/** IANA time zone for the point, e.g. "America/Chicago". */
|
|
25
|
+
timeZone: string;
|
|
26
|
+
/** "land" or "marine" — how NWS classifies the coordinate. */
|
|
27
|
+
pointType?: string;
|
|
28
|
+
/** Nearest named place, e.g. "Sargent, TX". */
|
|
29
|
+
place?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function resolvePoint(latitude: number, longitude: number): Promise<NwsPoint>;
|
|
32
|
+
export interface GridSeries {
|
|
33
|
+
uom?: string;
|
|
34
|
+
values?: Array<{
|
|
35
|
+
validTime: string;
|
|
36
|
+
value: number | null;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
/** Parse an ISO 8601 duration like "PT3H", "P1D", "PT1H30M" to milliseconds. */
|
|
40
|
+
export declare function parseIsoDurationMs(duration: string): number;
|
|
41
|
+
/**
|
|
42
|
+
* Expand a gridpoint series (start-instant + hold-duration values) into a map
|
|
43
|
+
* of epoch-ms-per-hour → value.
|
|
44
|
+
*/
|
|
45
|
+
export declare function expandGridSeries(series: GridSeries | undefined): Map<number, number>;
|
|
46
|
+
/** Convert a gridpoint speed value to knots based on its declared unit. */
|
|
47
|
+
export declare function speedToKnots(value: number, uom: string | undefined): number;
|
|
48
|
+
export declare function knotsToMs(knots: number): number;
|
|
49
|
+
/** Convert a gridpoint length value to meters based on its declared unit. */
|
|
50
|
+
export declare function lengthToMeters(value: number, uom: string | undefined): number;
|
|
51
|
+
export declare function metersToFeet(meters: number): number;
|
|
52
|
+
export declare function toCompass(degrees: number): string;
|
|
53
|
+
export interface HourlyWindSample {
|
|
54
|
+
/** UTC instant, ISO 8601. */
|
|
55
|
+
time: string;
|
|
56
|
+
/** Wind speed in knots (converted from the gridpoint's declared unit). */
|
|
57
|
+
speed_knots: number | null;
|
|
58
|
+
/** Wind gust in knots. */
|
|
59
|
+
gust_knots: number | null;
|
|
60
|
+
/** Direction the wind blows FROM, degrees true. */
|
|
61
|
+
direction_deg: number | null;
|
|
62
|
+
/** 16-point compass rendering of direction_deg. */
|
|
63
|
+
compass: string | null;
|
|
64
|
+
/** Significant wave height in meters (marine gridpoints only, often absent). */
|
|
65
|
+
wave_height_m: number | null;
|
|
66
|
+
}
|
|
67
|
+
export interface WindForecast {
|
|
68
|
+
point: NwsPoint;
|
|
69
|
+
updated?: string;
|
|
70
|
+
samples: HourlyWindSample[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Hourly numeric wind forecast (plus wave height when the grid carries it)
|
|
74
|
+
* for a coordinate, starting at the current hour.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getWindForecast(latitude: number, longitude: number, hours: number): Promise<WindForecast>;
|
|
77
|
+
export interface MarineZone {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
cwa: string;
|
|
81
|
+
}
|
|
82
|
+
/** Resolve the NWS coastal marine zone covering a coordinate (e.g. GMZ350). */
|
|
83
|
+
export declare function findCoastalZone(latitude: number, longitude: number): Promise<MarineZone>;
|
|
84
|
+
/**
|
|
85
|
+
* True when a CWF bulletin segment's UGC header covers the given zone.
|
|
86
|
+
* Headers look like "GMZ330-335-350-061015-" (list) or "GMZ350>355-061015-"
|
|
87
|
+
* (range), always ending in a 6-digit expiry.
|
|
88
|
+
*/
|
|
89
|
+
export declare function segmentCoversZone(segment: string, zoneId: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Extract the segment of a multi-zone CWF bulletin covering one zone.
|
|
92
|
+
* Bulletins are segmented by "$$" terminators; each segment opens with a UGC
|
|
93
|
+
* zone-list header. Returns undefined when no segment matches.
|
|
94
|
+
*/
|
|
95
|
+
export declare function extractZoneSegment(productText: string, zoneId: string): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Extract the office-wide synopsis block, when the bulletin carries one.
|
|
98
|
+
* Offices vary the header casing (".SYNOPSIS...", ".Synopsis For ...").
|
|
99
|
+
*/
|
|
100
|
+
export declare function extractSynopsis(productText: string): string | undefined;
|
|
101
|
+
export interface MarineTextForecast {
|
|
102
|
+
zone: MarineZone;
|
|
103
|
+
productId?: string;
|
|
104
|
+
issuanceTime?: string;
|
|
105
|
+
/** The bulletin segment for the zone (day-part narrative, winds in knots). */
|
|
106
|
+
segment?: string;
|
|
107
|
+
/** The office-wide marine synopsis, when present. */
|
|
108
|
+
synopsis?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Latest Coastal Waters Forecast text for the marine zone covering a point.
|
|
112
|
+
* CWF products are keyed by issuing office (not zone) and cover every zone
|
|
113
|
+
* that office serves in one bulletin, so the zone's segment is extracted here.
|
|
114
|
+
*/
|
|
115
|
+
export declare function getMarineTextForecast(latitude: number, longitude: number): Promise<MarineTextForecast>;
|