@dangahagan/weather-mcp 1.27.0 → 1.28.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 (39) hide show
  1. package/README.md +6 -6
  2. package/dist/config/displayThresholds.d.ts +31 -0
  3. package/dist/config/displayThresholds.d.ts.map +1 -1
  4. package/dist/config/displayThresholds.js +31 -0
  5. package/dist/config/displayThresholds.js.map +1 -1
  6. package/dist/handlers/criticalAlertBanner.d.ts +84 -0
  7. package/dist/handlers/criticalAlertBanner.d.ts.map +1 -0
  8. package/dist/handlers/criticalAlertBanner.js +172 -0
  9. package/dist/handlers/criticalAlertBanner.js.map +1 -0
  10. package/dist/handlers/currentConditionsHandler.d.ts +1 -1
  11. package/dist/handlers/currentConditionsHandler.d.ts.map +1 -1
  12. package/dist/handlers/currentConditionsHandler.js +64 -37
  13. package/dist/handlers/currentConditionsHandler.js.map +1 -1
  14. package/dist/handlers/forecastHandler.d.ts +1 -1
  15. package/dist/handlers/forecastHandler.d.ts.map +1 -1
  16. package/dist/handlers/forecastHandler.js +68 -43
  17. package/dist/handlers/forecastHandler.js.map +1 -1
  18. package/dist/handlers/weatherSummaryHandler.d.ts +1 -1
  19. package/dist/handlers/weatherSummaryHandler.d.ts.map +1 -1
  20. package/dist/handlers/weatherSummaryHandler.js +30 -2
  21. package/dist/handlers/weatherSummaryHandler.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +11 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/services/jma.d.ts.map +1 -1
  26. package/dist/services/jma.js +22 -0
  27. package/dist/services/jma.js.map +1 -1
  28. package/dist/utils/criticalAlert.d.ts +86 -0
  29. package/dist/utils/criticalAlert.d.ts.map +1 -0
  30. package/dist/utils/criticalAlert.js +252 -0
  31. package/dist/utils/criticalAlert.js.map +1 -0
  32. package/dist/utils/jmaAreaResolver.d.ts.map +1 -1
  33. package/dist/utils/jmaAreaResolver.js +7 -5
  34. package/dist/utils/jmaAreaResolver.js.map +1 -1
  35. package/dist/utils/locationResolver.d.ts +32 -0
  36. package/dist/utils/locationResolver.d.ts.map +1 -1
  37. package/dist/utils/locationResolver.js +33 -0
  38. package/dist/utils/locationResolver.js.map +1 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://badge.fury.io/js/@dangahagan%2Fweather-mcp.svg)](https://www.npmjs.com/package/@dangahagan/weather-mcp)
4
4
  [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.dgahagan/weather-mcp)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Tests](https://img.shields.io/badge/tests-3%2C130%20passing-brightgreen)](./docs/testing/TEST_SUITE_README.md)
6
+ [![Tests](https://img.shields.io/badge/tests-3%2C250%20passing-brightgreen)](./docs/testing/TEST_SUITE_README.md)
7
7
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](https://nodejs.org)
8
8
 
9
9
  **Give your AI assistant real weather data — 17 tools, zero API keys, zero signup, zero cost.**
@@ -58,7 +58,7 @@ Choose this one if you want:
58
58
 
59
59
  - **Genuinely free** — every data source is a free public API. No trial that expires, no credit card, no rate-limited "free tier" bait.
60
60
  - **No API keys** — install to first forecast in under a minute. Nothing to configure, nothing to leak into a repo. ([Three optional keys](#optional-api-keys) add extras if you want them; the default configuration needs none.)
61
- - **Fully open source** — MIT licensed, readable TypeScript, 3,130 tests. Audit it, fork it, fix it.
61
+ - **Fully open source** — MIT licensed, readable TypeScript, 3,250 tests. Audit it, fork it, fix it.
62
62
  - **Privacy-respecting** — your queries go directly from your machine to public weather APIs. No middleman server, no telemetry.
63
63
  - **Breadth** — 17 tools covering weather, safety hazards (lightning, floods, wildfires), marine conditions, air quality, and historical data back to 1940. Most weather MCPs stop at forecasts.
64
64
 
@@ -70,11 +70,11 @@ All 17 tools, documented in detail in **[docs/TOOLS.md](./docs/TOOLS.md)**:
70
70
 
71
71
  | Tool | What it does | Coverage |
72
72
  |------|-------------|----------|
73
- | `get_forecast` | Daily/hourly forecasts up to 16 days by coordinates, saved location, or city name (NOAA publishes 7 days daily and ~6.5 days hourly in the US, and says so when you ask for more; `source="openmeteo"` reaches the full range); sunrise/sunset, UV, precipitation probability, optional climate-normals comparison, optional moon phase & twilight almanac, optional five-model agreement comparison | 🌍 Global |
74
- | `get_current_conditions` | Current weather: temperature, wind, humidity, pressure; NOAA station observations in the US (plus heat index/wind chill, snow depth, optional NOAA fire-weather indices), Open-Meteo model data elsewhere (with an optional computed Fosberg fire-weather index), or real airport station observations worldwide with `source="metar"`. Always states the observation's age, and automatically falls back to a fresher nearby station when the nearest one has gone dark. In extreme conditions automatically adds frostbite time-to-onset (computed wind chill) or heat-stress context (estimated WBGT) | 🌍 Global |
73
+ | `get_forecast` | Daily/hourly forecasts up to 16 days by coordinates, saved location, or city name (NOAA publishes 7 days daily and ~6.5 days hourly in the US, and says so when you ask for more; `source="openmeteo"` reaches the full range); sunrise/sunset, UV, precipitation probability, optional climate-normals comparison, optional moon phase & twilight almanac, optional five-model agreement comparison When the National Weather Service has a life-threatening alert active for a US point, its warning is surfaced at the top of the response before the report — absence of the banner is not an all-clear, so call `get_alerts` for the authoritative answer | 🌍 Global |
74
+ | `get_current_conditions` | Current weather: temperature, wind, humidity, pressure; NOAA station observations in the US (plus heat index/wind chill, snow depth, optional NOAA fire-weather indices), Open-Meteo model data elsewhere (with an optional computed Fosberg fire-weather index), or real airport station observations worldwide with `source="metar"`. Always states the observation's age, and automatically falls back to a fresher nearby station when the nearest one has gone dark. In extreme conditions automatically adds frostbite time-to-onset (computed wind chill) or heat-stress context (estimated WBGT) When the National Weather Service has a life-threatening alert active for a US point, its warning is surfaced at the top of the response before the report — absence of the banner is not an all-clear, so call `get_alerts` for the authoritative answer | 🌍 Global |
75
75
  | `get_alerts` | Active watches, warnings, and advisories sorted by severity; official national warnings for the US (NOAA), Canada (ECCC), 38 European countries (MeteoAlarm), India (NDMA SACHET), the Philippines (PAGASA), Indonesia (BMKG), and Japan (JMA) | 🇺🇸 🇨🇦 🇪🇺 🇮🇳 🇵🇭 🇮🇩 🇯🇵 |
76
76
  | `get_historical_weather` | Hourly/daily observations from 1940 to present | 🌍 Global |
77
- | `get_weather_summary` | One-call overview combining current conditions, forecast, and alerts (optionally air quality and lightning) | 🌍 Global |
77
+ | `get_weather_summary` | One-call overview combining current conditions, forecast, and alerts (optionally air quality and lightning); a life-threatening US alert is surfaced once above the whole summary, not once per section | 🌍 Global |
78
78
  | `search_location` | Geocode place names to coordinates ("Paris" → 48.85, 2.35) | 🌍 Global |
79
79
  | `get_air_quality` | AQI (US/European scales), pollutants, UV index, health guidance; current pollen levels for European locations (or worldwide with an optional key); optional day-grouped forecast up to 7 days with per-day peak AQI and UV | 🌍 Global |
80
80
  | `get_marine_conditions` | Wave height, swell, ocean currents, sea state on WMO Code Table 3700 (Douglas sea scale) with a severity marker and legend — includes Great Lakes and major US bays; forecast up to 16 days | 🌍 Global |
@@ -380,7 +380,7 @@ Being honest about what free public data can and can't do:
380
380
  ```bash
381
381
  npm run build # Compile TypeScript
382
382
  npm run dev # Run in development mode
383
- npm test # Run all 3,130 tests
383
+ npm test # Run all 3,250 tests
384
384
  npm run test:coverage # Coverage report
385
385
  npm run audit # Dependency vulnerability scan
386
386
  ```
@@ -107,6 +107,37 @@ export declare const DisplayThresholds: {
107
107
  /** Compute/render WBGT only when air temp (°F) is at or above showHeatIndex and rounded WBGT (°F) is at or above this */
108
108
  readonly showWbgtF: 80;
109
109
  };
110
+ /**
111
+ * Life-threatening alert banner gate
112
+ */
113
+ readonly criticalAlert: {
114
+ /**
115
+ * Display gates only. The predicate that reads them lives in
116
+ * `src/utils/criticalAlert.ts`, which owns the *shape* of the rule — the
117
+ * three-axis leg is an `and`, the response leg stands alone, and the two
118
+ * are `or`ed. Widening a list here loosens the gate; it cannot change that
119
+ * shape.
120
+ *
121
+ * These are membership lists rather than a rank because CAP's severity,
122
+ * urgency and certainty are unordered enums as far as this repo is
123
+ * concerned: `CAP_SEVERITY_ORDER` in `alertsHandler.ts` is a sort rank for
124
+ * display, and reusing it here would tie a safety gate to a presentation
125
+ * decision.
126
+ *
127
+ * Calibrated against the live national feed — see the Verification section
128
+ * of the design plan for the histogram the current values were read from.
129
+ * Tuning is a config change here, deliberately, so re-calibration never
130
+ * needs a code review of the predicate.
131
+ */
132
+ /** CAP `severity` values that can reach the gate's three-axis leg */
133
+ readonly severities: readonly string[];
134
+ /** CAP `urgency` values that can reach the gate's three-axis leg */
135
+ readonly urgencies: readonly string[];
136
+ /** CAP `certainty` values that can reach the gate's three-axis leg */
137
+ readonly certainties: readonly string[];
138
+ /** CAP `response` values that fire the gate on their own, whatever the severity */
139
+ readonly responses: readonly string[];
140
+ };
110
141
  };
111
142
  /**
112
143
  * API-related constants
@@ -1 +1 @@
1
- {"version":3,"file":"displayThresholds.d.ts","sourceRoot":"","sources":["../../src/config/displayThresholds.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,iBAAiB;IAC5B;;OAEG;aACH,WAAW;QACT,2DAA2D;iBAC3D,aAAa,EAAE,EAAE;QACjB,2DAA2D;iBAC3D,aAAa,EAAE,EAAE;QACjB;;;;;;WAMG;iBACH,YAAY;qBACV,CAAC,EAAE,CAAC;qBACJ,CAAC,EAAE,CAAC;;;IAIR;;OAEG;aACH,IAAI;QACF,4FAA4F;iBAC5F,qBAAqB,EAAE,GAAG;;IAG5B;;OAEG;aACH,UAAU;QACR,gBAAgB;iBAChB,QAAQ,EAAE,IAAI;QACd,UAAU;iBACV,GAAG,EAAE,CAAG;QACR,mBAAmB;iBACnB,QAAQ,EAAE,CAAG;QACb,uBAAuB;iBACvB,KAAK,EAAE,EAAI;;IAGb;;OAEG;aACH,iBAAiB;QACf;;;;WAIG;iBACH,mBAAmB,EAAE,GAAG;QACxB;;;;WAIG;iBACH,sBAAsB,EAAE,GAAG;QAC3B;;;WAGG;iBACH,kBAAkB,EAAE,CAAC;;IAGvB;;OAEG;aACH,aAAa;QACX,4CAA4C;iBAC5C,KAAK,EAAE,GAAG;QACV,+CAA+C;iBAC/C,QAAQ,EAAE,GAAG;QACb,4CAA4C;iBAC5C,KAAK,EAAE,GAAG;QACV;;;;;;;;;WASG;iBACH,UAAU;qBACR,IAAI,EAAE,KAAK;qBACX,EAAE,EAAE,IAAI;;;IAIZ;;OAEG;aACH,aAAa;QACX;;;;;;WAMG;QACH,mFAAmF;iBACnF,yBAAyB,EAAE,CAAC,EAAE;QAC9B,yHAAyH;iBACzH,SAAS,EAAE,EAAE;;CAEP,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;IACvB;;;OAGG;aACH,2BAA2B,EAAE,CAAC;IAE9B;;OAEG;aACH,uBAAuB,EAAE,EAAE;CACnB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,eAAe;IAC1B;;OAEG;aACH,mBAAmB,EAAE,CAAC;IAEtB;;OAEG;aACH,eAAe,EAAE,EAAE;IAEnB;;OAEG;aACH,sBAAsB,EAAE,GAAG;IAE3B;;;;;OAKG;aACH,kBAAkB,EAAE,GAAG;CACf,CAAC"}
1
+ {"version":3,"file":"displayThresholds.d.ts","sourceRoot":"","sources":["../../src/config/displayThresholds.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,iBAAiB;IAC5B;;OAEG;aACH,WAAW;QACT,2DAA2D;iBAC3D,aAAa,EAAE,EAAE;QACjB,2DAA2D;iBAC3D,aAAa,EAAE,EAAE;QACjB;;;;;;WAMG;iBACH,YAAY;qBACV,CAAC,EAAE,CAAC;qBACJ,CAAC,EAAE,CAAC;;;IAIR;;OAEG;aACH,IAAI;QACF,4FAA4F;iBAC5F,qBAAqB,EAAE,GAAG;;IAG5B;;OAEG;aACH,UAAU;QACR,gBAAgB;iBAChB,QAAQ,EAAE,IAAI;QACd,UAAU;iBACV,GAAG,EAAE,CAAG;QACR,mBAAmB;iBACnB,QAAQ,EAAE,CAAG;QACb,uBAAuB;iBACvB,KAAK,EAAE,EAAI;;IAGb;;OAEG;aACH,iBAAiB;QACf;;;;WAIG;iBACH,mBAAmB,EAAE,GAAG;QACxB;;;;WAIG;iBACH,sBAAsB,EAAE,GAAG;QAC3B;;;WAGG;iBACH,kBAAkB,EAAE,CAAC;;IAGvB;;OAEG;aACH,aAAa;QACX,4CAA4C;iBAC5C,KAAK,EAAE,GAAG;QACV,+CAA+C;iBAC/C,QAAQ,EAAE,GAAG;QACb,4CAA4C;iBAC5C,KAAK,EAAE,GAAG;QACV;;;;;;;;;WASG;iBACH,UAAU;qBACR,IAAI,EAAE,KAAK;qBACX,EAAE,EAAE,IAAI;;;IAIZ;;OAEG;aACH,aAAa;QACX;;;;;;WAMG;QACH,mFAAmF;iBACnF,yBAAyB,EAAE,CAAC,EAAE;QAC9B,yHAAyH;iBACzH,SAAS,EAAE,EAAE;;IAGf;;OAEG;aACH,aAAa;QACX;;;;;;;;;;;;;;;;;WAiBG;QACH,qEAAqE;iBACrE,UAAU,EAAiB,SAAS,MAAM,EAAE;QAC5C,oEAAoE;iBACpE,SAAS,EAAmB,SAAS,MAAM,EAAE;QAC7C,sEAAsE;iBACtE,WAAW,EAA4B,SAAS,MAAM,EAAE;QACxD,mFAAmF;iBACnF,SAAS,EAAkB,SAAS,MAAM,EAAE;;CAEtC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;IACvB;;;OAGG;aACH,2BAA2B,EAAE,CAAC;IAE9B;;OAEG;aACH,uBAAuB,EAAE,EAAE;CACnB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,eAAe;IAC1B;;OAEG;aACH,mBAAmB,EAAE,CAAC;IAEtB;;OAEG;aACH,eAAe,EAAE,EAAE;IAEnB;;OAEG;aACH,sBAAsB,EAAE,GAAG;IAE3B;;;;;OAKG;aACH,kBAAkB,EAAE,GAAG;CACf,CAAC"}
@@ -107,6 +107,37 @@ export const DisplayThresholds = {
107
107
  /** Compute/render WBGT only when air temp (°F) is at or above showHeatIndex and rounded WBGT (°F) is at or above this */
108
108
  showWbgtF: 80,
109
109
  },
110
+ /**
111
+ * Life-threatening alert banner gate
112
+ */
113
+ criticalAlert: {
114
+ /**
115
+ * Display gates only. The predicate that reads them lives in
116
+ * `src/utils/criticalAlert.ts`, which owns the *shape* of the rule — the
117
+ * three-axis leg is an `and`, the response leg stands alone, and the two
118
+ * are `or`ed. Widening a list here loosens the gate; it cannot change that
119
+ * shape.
120
+ *
121
+ * These are membership lists rather than a rank because CAP's severity,
122
+ * urgency and certainty are unordered enums as far as this repo is
123
+ * concerned: `CAP_SEVERITY_ORDER` in `alertsHandler.ts` is a sort rank for
124
+ * display, and reusing it here would tie a safety gate to a presentation
125
+ * decision.
126
+ *
127
+ * Calibrated against the live national feed — see the Verification section
128
+ * of the design plan for the histogram the current values were read from.
129
+ * Tuning is a config change here, deliberately, so re-calibration never
130
+ * needs a code review of the predicate.
131
+ */
132
+ /** CAP `severity` values that can reach the gate's three-axis leg */
133
+ severities: ['Extreme'],
134
+ /** CAP `urgency` values that can reach the gate's three-axis leg */
135
+ urgencies: ['Immediate'],
136
+ /** CAP `certainty` values that can reach the gate's three-axis leg */
137
+ certainties: ['Observed', 'Likely'],
138
+ /** CAP `response` values that fire the gate on their own, whatever the severity */
139
+ responses: ['Evacuate'],
140
+ },
110
141
  };
111
142
  /**
112
143
  * API-related constants
@@ -1 +1 @@
1
- {"version":3,"file":"displayThresholds.js","sourceRoot":"","sources":["../../src/config/displayThresholds.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B;;OAEG;IACH,WAAW,EAAE;QACX,2DAA2D;QAC3D,aAAa,EAAE,EAAE;QACjB,2DAA2D;QAC3D,aAAa,EAAE,EAAE;QACjB;;;;;;WAMG;QACH,YAAY,EAAE;YACZ,CAAC,EAAE,CAAC;YACJ,CAAC,EAAE,CAAC;SACL;KACF;IAED;;OAEG;IACH,IAAI,EAAE;QACJ,4FAA4F;QAC5F,qBAAqB,EAAE,GAAG;KAC3B;IAED;;OAEG;IACH,UAAU,EAAE;QACV,gBAAgB;QAChB,QAAQ,EAAE,IAAI;QACd,UAAU;QACV,GAAG,EAAE,GAAG;QACR,mBAAmB;QACnB,QAAQ,EAAE,GAAG;QACb,uBAAuB;QACvB,KAAK,EAAE,IAAI;KACZ;IAED;;OAEG;IACH,iBAAiB,EAAE;QACjB;;;;WAIG;QACH,mBAAmB,EAAE,GAAG;QACxB;;;;WAIG;QACH,sBAAsB,EAAE,GAAG;QAC3B;;;WAGG;QACH,kBAAkB,EAAE,CAAC;KACtB;IAED;;OAEG;IACH,aAAa,EAAE;QACb,4CAA4C;QAC5C,KAAK,EAAE,GAAG;QACV,+CAA+C;QAC/C,QAAQ,EAAE,GAAG;QACb,4CAA4C;QAC5C,KAAK,EAAE,GAAG;QACV;;;;;;;;;WASG;QACH,UAAU,EAAE;YACV,IAAI,EAAE,KAAK;YACX,EAAE,EAAE,IAAI;SACT;KACF;IAED;;OAEG;IACH,aAAa,EAAE;QACb;;;;;;WAMG;QACH,mFAAmF;QACnF,yBAAyB,EAAE,CAAC,EAAE;QAC9B,yHAAyH;QACzH,SAAS,EAAE,EAAE;KACd;CACO,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B;;;OAGG;IACH,2BAA2B,EAAE,CAAC;IAE9B;;OAEG;IACH,uBAAuB,EAAE,EAAE;CACnB,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B;;OAEG;IACH,mBAAmB,EAAE,CAAC;IAEtB;;OAEG;IACH,eAAe,EAAE,EAAE;IAEnB;;OAEG;IACH,sBAAsB,EAAE,GAAG,EAAE,0BAA0B;IAEvD;;;;;OAKG;IACH,kBAAkB,EAAE,GAAG;CACf,CAAC"}
1
+ {"version":3,"file":"displayThresholds.js","sourceRoot":"","sources":["../../src/config/displayThresholds.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B;;OAEG;IACH,WAAW,EAAE;QACX,2DAA2D;QAC3D,aAAa,EAAE,EAAE;QACjB,2DAA2D;QAC3D,aAAa,EAAE,EAAE;QACjB;;;;;;WAMG;QACH,YAAY,EAAE;YACZ,CAAC,EAAE,CAAC;YACJ,CAAC,EAAE,CAAC;SACL;KACF;IAED;;OAEG;IACH,IAAI,EAAE;QACJ,4FAA4F;QAC5F,qBAAqB,EAAE,GAAG;KAC3B;IAED;;OAEG;IACH,UAAU,EAAE;QACV,gBAAgB;QAChB,QAAQ,EAAE,IAAI;QACd,UAAU;QACV,GAAG,EAAE,GAAG;QACR,mBAAmB;QACnB,QAAQ,EAAE,GAAG;QACb,uBAAuB;QACvB,KAAK,EAAE,IAAI;KACZ;IAED;;OAEG;IACH,iBAAiB,EAAE;QACjB;;;;WAIG;QACH,mBAAmB,EAAE,GAAG;QACxB;;;;WAIG;QACH,sBAAsB,EAAE,GAAG;QAC3B;;;WAGG;QACH,kBAAkB,EAAE,CAAC;KACtB;IAED;;OAEG;IACH,aAAa,EAAE;QACb,4CAA4C;QAC5C,KAAK,EAAE,GAAG;QACV,+CAA+C;QAC/C,QAAQ,EAAE,GAAG;QACb,4CAA4C;QAC5C,KAAK,EAAE,GAAG;QACV;;;;;;;;;WASG;QACH,UAAU,EAAE;YACV,IAAI,EAAE,KAAK;YACX,EAAE,EAAE,IAAI;SACT;KACF;IAED;;OAEG;IACH,aAAa,EAAE;QACb;;;;;;WAMG;QACH,mFAAmF;QACnF,yBAAyB,EAAE,CAAC,EAAE;QAC9B,yHAAyH;QACzH,SAAS,EAAE,EAAE;KACd;IAED;;OAEG;IACH,aAAa,EAAE;QACb;;;;;;;;;;;;;;;;;WAiBG;QACH,qEAAqE;QACrE,UAAU,EAAE,CAAC,SAAS,CAAsB;QAC5C,oEAAoE;QACpE,SAAS,EAAE,CAAC,WAAW,CAAsB;QAC7C,sEAAsE;QACtE,WAAW,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAsB;QACxD,mFAAmF;QACnF,SAAS,EAAE,CAAC,UAAU,CAAsB;KAC7C;CACO,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B;;;OAGG;IACH,2BAA2B,EAAE,CAAC;IAE9B;;OAEG;IACH,uBAAuB,EAAE,EAAE;CACnB,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B;;OAEG;IACH,mBAAmB,EAAE,CAAC;IAEtB;;OAEG;IACH,eAAe,EAAE,EAAE;IAEnB;;OAEG;IACH,sBAAsB,EAAE,GAAG,EAAE,0BAA0B;IAEvD;;;;;OAKG;IACH,kBAAkB,EAAE,GAAG;CACf,CAAC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The one place that fetches for the life-threatening alert banner.
3
+ *
4
+ * `get_forecast`, `get_current_conditions` and `get_weather_summary` all call
5
+ * `resolveCriticalAlertBanner` rather than each doing their own fetch-gate-format
6
+ * sequence, so the **failure posture exists once**. Three copies of a safety
7
+ * failure posture is how one copy drifts, and the copy that drifts is the one
8
+ * that renders a fabricated all-clear.
9
+ *
10
+ * The posture itself is D1 of the design plan: **silent omit**, one
11
+ * `logger.warn`, and no retries. That is only safe because the banner is a
12
+ * positive assertion — its absence claims nothing. `get_alerts` still serves
13
+ * the same data under full contract rules, where a failure propagates. Nothing
14
+ * in a tool description, schema or doc may ever say these three tools "check
15
+ * for alerts", or absence silently becomes an implied all-clear.
16
+ *
17
+ * The gate, the selection and the copy all live in `src/utils/criticalAlert.ts`,
18
+ * which is pure and zero-I/O. This module is the glue between that and the
19
+ * service, and holds no display logic of its own.
20
+ */
21
+ import { NOAAService } from '../services/noaa.js';
22
+ import type { ResolvedLocation } from '../utils/locationResolver.js';
23
+ /**
24
+ * The banner for this point, or `''` — which is every path but one.
25
+ *
26
+ * In order:
27
+ *
28
+ * 1. **US pre-filter.** `country_code` when the resolution path knew one,
29
+ * `isInUS` otherwise. NOAA is the only upstream in v1 (D2), so a non-US
30
+ * point returns without making any request at all.
31
+ * 2. **One fetch, no retries.** `getAlerts` caches on its own point-keyed
32
+ * alerts entry with the five-minute alerts TTL (`src/services/noaa.ts`), so a
33
+ * banner fetch and a real `get_alerts` call within five minutes share one
34
+ * request. This module deliberately builds no key and declares no TTL of its
35
+ * own — that is what keeps risk floor F5 untripped, and the acceptance check
36
+ * for it is a grep of this file for either construct.
37
+ * 3. **Any failure is silent.** One `logger.warn` and `''`. Never a
38
+ * "could not check alerts" note: repeated benign warnings during an upstream
39
+ * outage train readers to ignore the banner slot, which is worse than
40
+ * nothing at all.
41
+ * 4. **All-or-nothing.** With no critical alert this emits nothing — no header,
42
+ * no rule, no "no critical alerts" line.
43
+ *
44
+ * **G2 is not tripped by the pre-filter.** `isInUS` is a bounding box, but
45
+ * nothing about the box is rendered: a false negative omits a
46
+ * positive-assertion-only element and claims nothing, and a false positive (a
47
+ * point just over the Canadian border) costs one request that returns no alerts
48
+ * and therefore no banner.
49
+ *
50
+ * @param noaaService The NOAA client; its `getAlerts` is the only method called
51
+ * @param resolved The already-resolved location, carrying `country_code` when known
52
+ * @param timezone IANA zone the banner's expiry is rendered in
53
+ * @returns The formatted banner, or `''`
54
+ */
55
+ export declare function resolveCriticalAlertBanner(noaaService: NOAAService, resolved: ResolvedLocation, timezone?: string): Promise<string>;
56
+ /**
57
+ * Carry an already-resolved banner on an error about to be thrown.
58
+ *
59
+ * `get_forecast` and `get_current_conditions` resolve the banner concurrently
60
+ * with the weather body. When the body fails, the warning is the half the
61
+ * caller most needs, so it travels with the error to the one place that renders
62
+ * errors (`src/index.ts`) instead of being dropped. This keeps error formatting
63
+ * and error logging at that single site rather than duplicating either here.
64
+ *
65
+ * An empty banner attaches nothing at all, so a failure with no critical alert
66
+ * — every non-US point, and every US point with nothing life-threatening
67
+ * active — is byte-identical to what it was before.
68
+ *
69
+ * @param error The error being thrown; returned unchanged for `throw` chaining
70
+ * @param banner The resolved banner, or `''`
71
+ * @returns The same error
72
+ */
73
+ export declare function carryCriticalAlertBannerOnError(error: unknown, banner: string): unknown;
74
+ /**
75
+ * The banner carried by an error, or `''`.
76
+ *
77
+ * Read once, by the dispatch's error path. Returning `''` for every error that
78
+ * carries nothing is what keeps the existing error output unchanged.
79
+ *
80
+ * @param error The caught error
81
+ * @returns The carried banner, or `''`
82
+ */
83
+ export declare function criticalAlertBannerFromError(error: unknown): string;
84
+ //# sourceMappingURL=criticalAlertBanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"criticalAlertBanner.d.ts","sourceRoot":"","sources":["../../src/handlers/criticalAlertBanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AA4BrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAqDjB;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKvF;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQnE"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The one place that fetches for the life-threatening alert banner.
3
+ *
4
+ * `get_forecast`, `get_current_conditions` and `get_weather_summary` all call
5
+ * `resolveCriticalAlertBanner` rather than each doing their own fetch-gate-format
6
+ * sequence, so the **failure posture exists once**. Three copies of a safety
7
+ * failure posture is how one copy drifts, and the copy that drifts is the one
8
+ * that renders a fabricated all-clear.
9
+ *
10
+ * The posture itself is D1 of the design plan: **silent omit**, one
11
+ * `logger.warn`, and no retries. That is only safe because the banner is a
12
+ * positive assertion — its absence claims nothing. `get_alerts` still serves
13
+ * the same data under full contract rules, where a failure propagates. Nothing
14
+ * in a tool description, schema or doc may ever say these three tools "check
15
+ * for alerts", or absence silently becomes an implied all-clear.
16
+ *
17
+ * The gate, the selection and the copy all live in `src/utils/criticalAlert.ts`,
18
+ * which is pure and zero-I/O. This module is the glue between that and the
19
+ * service, and holds no display logic of its own.
20
+ */
21
+ import { isInUS } from '../utils/geography.js';
22
+ import { logger } from '../utils/logger.js';
23
+ import { formatCriticalAlertBanner, selectCriticalAlert, } from '../utils/criticalAlert.js';
24
+ /**
25
+ * The HTTP status an error carries, or `undefined`.
26
+ *
27
+ * Reads the two shapes this path can see — an `ApiError` subclass's
28
+ * `statusCode`, and an axios error's `response.status` — and nothing else. A
29
+ * bare number is the whole of what is logged: no message, no config, no URL.
30
+ */
31
+ function httpStatusOf(error) {
32
+ if (typeof error !== 'object' || error === null) {
33
+ return undefined;
34
+ }
35
+ const record = error;
36
+ if (typeof record.statusCode === 'number') {
37
+ return record.statusCode;
38
+ }
39
+ if (typeof record.response?.status === 'number') {
40
+ return record.response.status;
41
+ }
42
+ return undefined;
43
+ }
44
+ /**
45
+ * The banner for this point, or `''` — which is every path but one.
46
+ *
47
+ * In order:
48
+ *
49
+ * 1. **US pre-filter.** `country_code` when the resolution path knew one,
50
+ * `isInUS` otherwise. NOAA is the only upstream in v1 (D2), so a non-US
51
+ * point returns without making any request at all.
52
+ * 2. **One fetch, no retries.** `getAlerts` caches on its own point-keyed
53
+ * alerts entry with the five-minute alerts TTL (`src/services/noaa.ts`), so a
54
+ * banner fetch and a real `get_alerts` call within five minutes share one
55
+ * request. This module deliberately builds no key and declares no TTL of its
56
+ * own — that is what keeps risk floor F5 untripped, and the acceptance check
57
+ * for it is a grep of this file for either construct.
58
+ * 3. **Any failure is silent.** One `logger.warn` and `''`. Never a
59
+ * "could not check alerts" note: repeated benign warnings during an upstream
60
+ * outage train readers to ignore the banner slot, which is worse than
61
+ * nothing at all.
62
+ * 4. **All-or-nothing.** With no critical alert this emits nothing — no header,
63
+ * no rule, no "no critical alerts" line.
64
+ *
65
+ * **G2 is not tripped by the pre-filter.** `isInUS` is a bounding box, but
66
+ * nothing about the box is rendered: a false negative omits a
67
+ * positive-assertion-only element and claims nothing, and a false positive (a
68
+ * point just over the Canadian border) costs one request that returns no alerts
69
+ * and therefore no banner.
70
+ *
71
+ * @param noaaService The NOAA client; its `getAlerts` is the only method called
72
+ * @param resolved The already-resolved location, carrying `country_code` when known
73
+ * @param timezone IANA zone the banner's expiry is rendered in
74
+ * @returns The formatted banner, or `''`
75
+ */
76
+ export async function resolveCriticalAlertBanner(noaaService, resolved, timezone) {
77
+ const { latitude, longitude } = resolved;
78
+ // country_code first, isInUS second. A saved or geocoded location already
79
+ // knows its country, so the common case costs no geography at all. Casing
80
+ // follows the upstream source (saved locations store "US", geocoders vary),
81
+ // hence the case-insensitive compare.
82
+ const countryCode = resolved.country_code;
83
+ const isUS = typeof countryCode === 'string' && countryCode.length > 0
84
+ ? countryCode.toLowerCase() === 'us'
85
+ : isInUS(latitude, longitude);
86
+ if (!isUS) {
87
+ return '';
88
+ }
89
+ try {
90
+ const alerts = await noaaService.getAlerts(latitude, longitude, true);
91
+ const features = Array.isArray(alerts?.features) ? alerts.features : [];
92
+ if (features.length === 0) {
93
+ return '';
94
+ }
95
+ // `properties` is required on the documented shape but a third-party
96
+ // response may omit it, so the map tolerates a feature without one rather
97
+ // than throwing inside a garnish path.
98
+ const candidates = features.map((feature) => (feature?.properties ?? {}));
99
+ return formatCriticalAlertBanner(selectCriticalAlert(candidates), features.length, timezone);
100
+ }
101
+ catch (error) {
102
+ // Exactly one warn, carrying no URL and no raw axios error. The banner is
103
+ // garnish (D1): this must add no latency and must never rethrow, because
104
+ // the forecast the caller actually asked for still has to render.
105
+ //
106
+ // The impl plan's contract is "no URL and no raw axios error", and
107
+ // `error.message` was carrying exactly that — "Request failed with status
108
+ // code 503" is the axios message, and an axios message is one upstream
109
+ // change away from carrying the request URL with it. What is logged is
110
+ // therefore the error's *type* and its status, the shape this project
111
+ // already uses for key-bearing services, which is what a reader of these
112
+ // logs actually needs to tell a 404 from an outage.
113
+ logger.warn('Critical-alert banner lookup failed; omitting the banner', {
114
+ service: 'NOAA',
115
+ securityEvent: true,
116
+ errorType: error instanceof Error ? error.constructor.name : 'Unknown',
117
+ status: httpStatusOf(error),
118
+ });
119
+ return '';
120
+ }
121
+ }
122
+ /**
123
+ * The banner a failed weather request is carrying, if any.
124
+ *
125
+ * A `Symbol.for` key rather than a named property: it cannot collide with any
126
+ * field on an `ApiError` subclass, it does not appear in `JSON.stringify`, and
127
+ * it survives being rethrown unchanged through the handlers' existing
128
+ * fallback `catch` blocks.
129
+ */
130
+ const CRITICAL_ALERT_BANNER = Symbol.for('weather-mcp.critical-alert-banner');
131
+ /**
132
+ * Carry an already-resolved banner on an error about to be thrown.
133
+ *
134
+ * `get_forecast` and `get_current_conditions` resolve the banner concurrently
135
+ * with the weather body. When the body fails, the warning is the half the
136
+ * caller most needs, so it travels with the error to the one place that renders
137
+ * errors (`src/index.ts`) instead of being dropped. This keeps error formatting
138
+ * and error logging at that single site rather than duplicating either here.
139
+ *
140
+ * An empty banner attaches nothing at all, so a failure with no critical alert
141
+ * — every non-US point, and every US point with nothing life-threatening
142
+ * active — is byte-identical to what it was before.
143
+ *
144
+ * @param error The error being thrown; returned unchanged for `throw` chaining
145
+ * @param banner The resolved banner, or `''`
146
+ * @returns The same error
147
+ */
148
+ export function carryCriticalAlertBannerOnError(error, banner) {
149
+ if (banner && typeof error === 'object' && error !== null) {
150
+ error[CRITICAL_ALERT_BANNER] = banner;
151
+ }
152
+ return error;
153
+ }
154
+ /**
155
+ * The banner carried by an error, or `''`.
156
+ *
157
+ * Read once, by the dispatch's error path. Returning `''` for every error that
158
+ * carries nothing is what keeps the existing error output unchanged.
159
+ *
160
+ * @param error The caught error
161
+ * @returns The carried banner, or `''`
162
+ */
163
+ export function criticalAlertBannerFromError(error) {
164
+ if (typeof error === 'object' && error !== null) {
165
+ const banner = error[CRITICAL_ALERT_BANNER];
166
+ if (typeof banner === 'string') {
167
+ return banner;
168
+ }
169
+ }
170
+ return '';
171
+ }
172
+ //# sourceMappingURL=criticalAlertBanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"criticalAlertBanner.js","sourceRoot":"","sources":["../../src/handlers/criticalAlertBanner.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@ import { AcisService } from '../services/acis.js';
8
8
  import { AviationWeatherService } from '../services/aviationWeather.js';
9
9
  import { LocationStore } from '../services/locationStore.js';
10
10
  import { GeocodingService } from '../services/geocoding.js';
11
- export declare function handleGetCurrentConditions(args: unknown, noaaService: NOAAService, openMeteoService: OpenMeteoService, nceiService: NCEIService, locationStore: LocationStore, geocodingService: GeocodingService, acisService?: AcisService, aviationWeatherService?: AviationWeatherService): Promise<{
11
+ export declare function handleGetCurrentConditions(args: unknown, noaaService: NOAAService, openMeteoService: OpenMeteoService, nceiService: NCEIService, locationStore: LocationStore, geocodingService: GeocodingService, acisService?: AcisService, aviationWeatherService?: AviationWeatherService, criticalAlertBanner?: boolean): Promise<{
12
12
  content: Array<{
13
13
  type: string;
14
14
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"currentConditionsHandler.d.ts","sourceRoot":"","sources":["../../src/handlers/currentConditionsHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AA6G5D,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,CAAC,EAAE,WAAW,EACzB,sBAAsB,CAAC,EAAE,sBAAsB,GAC9C,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAkH7D"}
1
+ {"version":3,"file":"currentConditionsHandler.d.ts","sourceRoot":"","sources":["../../src/handlers/currentConditionsHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AA8G5D,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,CAAC,EAAE,WAAW,EACzB,sBAAsB,CAAC,EAAE,sBAAsB,EAC/C,mBAAmB,CAAC,EAAE,OAAO,GAC5B,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAiJ7D"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Handler for get_current_conditions tool
3
3
  */
4
- import { resolveLocationAsync, prependLocationLine } from '../utils/locationResolver.js';
4
+ import { resolveLocationAsync, prependLocationLine, prependCriticalAlertBanner } from '../utils/locationResolver.js';
5
+ import { resolveCriticalAlertBanner, carryCriticalAlertBannerOnError } from './criticalAlertBanner.js';
5
6
  import { validateOptionalBoolean } from '../utils/validation.js';
6
7
  import { convertToFahrenheit } from '../utils/temperatureConversion.js';
7
8
  import { resolveUnitPreferences } from '../utils/unitPreferences.js';
@@ -36,7 +37,7 @@ function insertNoteAfterHeading(text, note) {
36
37
  // The remainder starts with the original newline(s), so no trailing \n here.
37
38
  return `${text.slice(0, newline)}\n\n${note}${text.slice(newline)}`;
38
39
  }
39
- export async function handleGetCurrentConditions(args, noaaService, openMeteoService, nceiService, locationStore, geocodingService, acisService, aviationWeatherService) {
40
+ export async function handleGetCurrentConditions(args, noaaService, openMeteoService, nceiService, locationStore, geocodingService, acisService, aviationWeatherService, criticalAlertBanner) {
40
41
  // Resolve location from coordinates, a saved location name, or a geocoded city name
41
42
  const resolved = await resolveLocationAsync(args, locationStore, geocodingService);
42
43
  const { latitude, longitude } = resolved;
@@ -49,53 +50,79 @@ export async function handleGetCurrentConditions(args, noaaService, openMeteoSer
49
50
  const useNOAA = requestedSource === 'auto'
50
51
  ? isInUS(latitude, longitude)
51
52
  : requestedSource === 'noaa';
53
+ // BLOCKER-1 (diff review): a life-threatening warning must not be suppressed
54
+ // by a weather failure. Started here rather than awaited — every validation
55
+ // above has already run, and the alert fetch now overlaps the observation
56
+ // fetch instead of serialising ahead of it. `resolveCriticalAlertBanner`
57
+ // never rejects (it is silent-omit by construction), so this promise is safe
58
+ // to leave in flight across the observation's own error paths.
59
+ const bannerPromise = criticalAlertBanner
60
+ ? resolveCriticalAlertBanner(noaaService, resolved, guessTimezoneFromCoords(latitude, longitude))
61
+ : undefined;
52
62
  let output;
53
- if (requestedSource === 'metar') {
54
- // METAR is never auto-routed (D1): `auto` keeps its exact US-NOAA /
55
- // elsewhere-Open-Meteo semantics, and this arm is reached only on an
56
- // explicit request. A model estimate at the caller's coordinates and a
57
- // measurement at an airport 40 km away answer different questions.
58
- if (!aviationWeatherService) {
59
- throw new ServiceUnavailableError('AviationWeather', 'METAR station observations are not available in this server configuration.');
60
- }
61
- output = await formatMetarCurrentConditions(aviationWeatherService, openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
62
- }
63
- else if (useNOAA) {
64
- try {
65
- output = await formatNOAACurrentConditions(noaaService, openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
63
+ try {
64
+ if (requestedSource === 'metar') {
65
+ // METAR is never auto-routed (D1): `auto` keeps its exact US-NOAA /
66
+ // elsewhere-Open-Meteo semantics, and this arm is reached only on an
67
+ // explicit request. A model estimate at the caller's coordinates and a
68
+ // measurement at an airport 40 km away answer different questions.
69
+ if (!aviationWeatherService) {
70
+ throw new ServiceUnavailableError('AviationWeather', 'METAR station observations are not available in this server configuration.');
71
+ }
72
+ output = await formatMetarCurrentConditions(aviationWeatherService, openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
66
73
  }
67
- catch (error) {
68
- // The US bounding boxes overrun the border (Toronto, Vancouver, Windsor
69
- // all sit inside them), so auto-routed points NOAA rejects fall back to
70
- // Open-Meteo instead of erroring. NOAA maps the coverage 404 ("Unable to
71
- // provide data for requested point") to DataNotFoundError and other 4xx
72
- // to InvalidLocationError — both are non-retryable "NOAA can't serve
73
- // this request" failures, so both fall back. Transient failures
74
- // (RateLimitError, ServiceUnavailableError, network) still propagate,
75
- // and explicit source="noaa" keeps its error contract.
76
- if (requestedSource !== 'auto' ||
77
- !(error instanceof DataNotFoundError || error instanceof InvalidLocationError)) {
78
- throw error;
74
+ else if (useNOAA) {
75
+ try {
76
+ output = await formatNOAACurrentConditions(noaaService, openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
77
+ }
78
+ catch (error) {
79
+ // The US bounding boxes overrun the border (Toronto, Vancouver, Windsor
80
+ // all sit inside them), so auto-routed points NOAA rejects fall back to
81
+ // Open-Meteo instead of erroring. NOAA maps the coverage 404 ("Unable to
82
+ // provide data for requested point") to DataNotFoundError and other 4xx
83
+ // to InvalidLocationError — both are non-retryable "NOAA can't serve
84
+ // this request" failures, so both fall back. Transient failures
85
+ // (RateLimitError, ServiceUnavailableError, network) still propagate,
86
+ // and explicit source="noaa" keeps its error contract.
87
+ if (requestedSource !== 'auto' ||
88
+ !(error instanceof DataNotFoundError || error instanceof InvalidLocationError)) {
89
+ throw error;
90
+ }
91
+ logger.warn('NOAA rejected auto-routed location; falling back to Open-Meteo', {
92
+ latitude,
93
+ longitude,
94
+ fallback: true
95
+ });
96
+ output = insertNoteAfterHeading(await formatOpenMeteoCurrentConditions(openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService), NOAA_FALLBACK_NOTE);
79
97
  }
80
- logger.warn('NOAA rejected auto-routed location; falling back to Open-Meteo', {
81
- latitude,
82
- longitude,
83
- fallback: true
84
- });
85
- output = insertNoteAfterHeading(await formatOpenMeteoCurrentConditions(openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService), NOAA_FALLBACK_NOTE);
98
+ }
99
+ else {
100
+ output = await formatOpenMeteoCurrentConditions(openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
86
101
  }
87
102
  }
88
- else {
89
- output = await formatOpenMeteoCurrentConditions(openMeteoService, nceiService, latitude, longitude, includeFireWeather, includeNormals, prefs, acisService);
103
+ catch (error) {
104
+ // The observation failed, but the banner may still have resolved. Carry it
105
+ // on the error so the dispatch's single error-rendering site can show the
106
+ // warning above the failure message; with no banner this rethrows the
107
+ // original error untouched and the existing error contract is unchanged.
108
+ throw carryCriticalAlertBannerOnError(error, bannerPromise ? await bannerPromise : '');
90
109
  }
91
- return prependLocationLine({
110
+ // The flag check gated the fetch at the start (before any await), so an
111
+ // absent flag adds no latency to the no-op path. The banner is prepended
112
+ // last — outermost, above the `**Location:**` line this wraps — because a
113
+ // life-threatening warning outranks knowing which place it is about. This
114
+ // single return is shared by every source arm (NOAA, Open-Meteo, and the
115
+ // METAR arm which falls through to it), so wrapping it here covers all three
116
+ // by construction.
117
+ const banner = bannerPromise ? await bannerPromise : '';
118
+ return prependCriticalAlertBanner(prependLocationLine({
92
119
  content: [
93
120
  {
94
121
  type: 'text',
95
122
  text: output
96
123
  }
97
124
  ]
98
- }, resolved);
125
+ }, resolved), banner);
99
126
  }
100
127
  /**
101
128
  * Render a fixed-°F value in the caller's preferred temperature unit.