@dangahagan/weather-mcp 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +372 -53
  2. package/dist/config/cache.d.ts +2 -0
  3. package/dist/config/cache.d.ts.map +1 -1
  4. package/dist/config/cache.js +48 -2
  5. package/dist/config/cache.js.map +1 -1
  6. package/dist/config/displayThresholds.d.ts +83 -0
  7. package/dist/config/displayThresholds.d.ts.map +1 -0
  8. package/dist/config/displayThresholds.js +83 -0
  9. package/dist/config/displayThresholds.js.map +1 -0
  10. package/dist/errors/ApiError.d.ts +62 -0
  11. package/dist/errors/ApiError.d.ts.map +1 -0
  12. package/dist/errors/ApiError.js +147 -0
  13. package/dist/errors/ApiError.js.map +1 -0
  14. package/dist/handlers/airQualityHandler.d.ts +11 -0
  15. package/dist/handlers/airQualityHandler.d.ts.map +1 -0
  16. package/dist/handlers/airQualityHandler.js +154 -0
  17. package/dist/handlers/airQualityHandler.js.map +1 -0
  18. package/dist/handlers/alertsHandler.d.ts +11 -0
  19. package/dist/handlers/alertsHandler.d.ts.map +1 -0
  20. package/dist/handlers/alertsHandler.js +82 -0
  21. package/dist/handlers/alertsHandler.js.map +1 -0
  22. package/dist/handlers/currentConditionsHandler.d.ts +11 -0
  23. package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
  24. package/dist/handlers/currentConditionsHandler.js +231 -0
  25. package/dist/handlers/currentConditionsHandler.js.map +1 -0
  26. package/dist/handlers/forecastHandler.d.ts +13 -0
  27. package/dist/handlers/forecastHandler.d.ts.map +1 -0
  28. package/dist/handlers/forecastHandler.js +337 -0
  29. package/dist/handlers/forecastHandler.js.map +1 -0
  30. package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
  31. package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
  32. package/dist/handlers/historicalWeatherHandler.js +188 -0
  33. package/dist/handlers/historicalWeatherHandler.js.map +1 -0
  34. package/dist/handlers/locationHandler.d.ts +11 -0
  35. package/dist/handlers/locationHandler.d.ts.map +1 -0
  36. package/dist/handlers/locationHandler.js +113 -0
  37. package/dist/handlers/locationHandler.js.map +1 -0
  38. package/dist/handlers/marineConditionsHandler.d.ts +11 -0
  39. package/dist/handlers/marineConditionsHandler.d.ts.map +1 -0
  40. package/dist/handlers/marineConditionsHandler.js +154 -0
  41. package/dist/handlers/marineConditionsHandler.js.map +1 -0
  42. package/dist/handlers/statusHandler.d.ts +12 -0
  43. package/dist/handlers/statusHandler.d.ts.map +1 -0
  44. package/dist/handlers/statusHandler.js +106 -0
  45. package/dist/handlers/statusHandler.js.map +1 -0
  46. package/dist/index.js +219 -377
  47. package/dist/index.js.map +1 -1
  48. package/dist/services/noaa.d.ts +24 -1
  49. package/dist/services/noaa.d.ts.map +1 -1
  50. package/dist/services/noaa.js +116 -43
  51. package/dist/services/noaa.js.map +1 -1
  52. package/dist/services/openmeteo.d.ts +110 -3
  53. package/dist/services/openmeteo.d.ts.map +1 -1
  54. package/dist/services/openmeteo.js +568 -112
  55. package/dist/services/openmeteo.js.map +1 -1
  56. package/dist/types/noaa.d.ts +158 -12
  57. package/dist/types/noaa.d.ts.map +1 -1
  58. package/dist/types/openmeteo.d.ts +448 -0
  59. package/dist/types/openmeteo.d.ts.map +1 -1
  60. package/dist/utils/airQuality.d.ts +54 -0
  61. package/dist/utils/airQuality.d.ts.map +1 -0
  62. package/dist/utils/airQuality.js +251 -0
  63. package/dist/utils/airQuality.js.map +1 -0
  64. package/dist/utils/cache.d.ts +11 -4
  65. package/dist/utils/cache.d.ts.map +1 -1
  66. package/dist/utils/cache.js +33 -4
  67. package/dist/utils/cache.js.map +1 -1
  68. package/dist/utils/fireWeather.d.ts +62 -0
  69. package/dist/utils/fireWeather.d.ts.map +1 -0
  70. package/dist/utils/fireWeather.js +168 -0
  71. package/dist/utils/fireWeather.js.map +1 -0
  72. package/dist/utils/logger.d.ts +59 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +128 -0
  75. package/dist/utils/logger.js.map +1 -0
  76. package/dist/utils/marine.d.ts +38 -0
  77. package/dist/utils/marine.d.ts.map +1 -0
  78. package/dist/utils/marine.js +162 -0
  79. package/dist/utils/marine.js.map +1 -0
  80. package/dist/utils/temperatureConversion.d.ts +12 -0
  81. package/dist/utils/temperatureConversion.d.ts.map +1 -0
  82. package/dist/utils/temperatureConversion.js +17 -0
  83. package/dist/utils/temperatureConversion.js.map +1 -0
  84. package/dist/utils/validation.d.ts +89 -0
  85. package/dist/utils/validation.d.ts.map +1 -0
  86. package/dist/utils/validation.js +177 -0
  87. package/dist/utils/validation.js.map +1 -0
  88. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -11,16 +11,33 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
11
11
  import { NOAAService } from './services/noaa.js';
12
12
  import { OpenMeteoService } from './services/openmeteo.js';
13
13
  import { CacheConfig } from './config/cache.js';
14
+ import { logger } from './utils/logger.js';
15
+ import { formatErrorForUser } from './errors/ApiError.js';
16
+ import { handleGetForecast } from './handlers/forecastHandler.js';
17
+ import { handleGetCurrentConditions } from './handlers/currentConditionsHandler.js';
18
+ import { handleGetAlerts } from './handlers/alertsHandler.js';
19
+ import { handleGetHistoricalWeather } from './handlers/historicalWeatherHandler.js';
20
+ import { handleCheckServiceStatus } from './handlers/statusHandler.js';
21
+ import { handleSearchLocation } from './handlers/locationHandler.js';
22
+ import { handleGetAirQuality } from './handlers/airQualityHandler.js';
23
+ import { handleGetMarineConditions } from './handlers/marineConditionsHandler.js';
14
24
  /**
15
25
  * Server information
16
26
  */
27
+ import { readFileSync } from 'fs';
28
+ import { fileURLToPath } from 'url';
29
+ import { dirname, join } from 'path';
30
+ // Read version from package.json to ensure single source of truth
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
17
34
  const SERVER_NAME = 'weather-mcp';
18
- const SERVER_VERSION = '0.1.0';
35
+ const SERVER_VERSION = packageJson.version;
19
36
  /**
20
37
  * Initialize the NOAA service
21
38
  */
22
39
  const noaaService = new NOAAService({
23
- userAgent: '(weather-mcp, github.com/weather-mcp)'
40
+ userAgent: `weather-mcp/${SERVER_VERSION} (https://github.com/dgahagan/weather-mcp)`
24
41
  });
25
42
  /**
26
43
  * Initialize the Open-Meteo service for historical data
@@ -46,7 +63,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
46
63
  tools: [
47
64
  {
48
65
  name: 'get_forecast',
49
- description: 'Get future weather forecast for a location (US only). Use this for upcoming weather predictions (e.g., "tomorrow", "this week", "next 7 days"). Returns forecast data including temperature, precipitation, wind, and conditions. For current weather, use get_current_conditions. For past weather, use get_historical_weather. If this tool returns an error, check the error message for status page links and consider using check_service_status to verify API availability.',
66
+ description: 'Get future weather forecast for a location (global coverage). Use this for upcoming weather predictions (e.g., "tomorrow", "this week", "next 7 days", "hourly forecast"). Returns forecast data including temperature, precipitation, wind, conditions, and sunrise/sunset times. Supports both daily and hourly granularity. Automatically selects best data source: NOAA for US locations (more detailed), Open-Meteo for international locations. For current weather, use get_current_conditions. For past weather, use get_historical_weather. If this tool returns an error, check the error message for status page links and consider using check_service_status to verify API availability.',
50
67
  inputSchema: {
51
68
  type: 'object',
52
69
  properties: {
@@ -64,10 +81,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
64
81
  },
65
82
  days: {
66
83
  type: 'number',
67
- description: 'Number of days to include in forecast (1-7, default: 7)',
84
+ description: 'Number of days to include in forecast (1-16 for global, 1-7 for US NOAA, default: 7)',
68
85
  minimum: 1,
69
- maximum: 7,
86
+ maximum: 16,
70
87
  default: 7
88
+ },
89
+ granularity: {
90
+ type: 'string',
91
+ description: 'Forecast granularity: "daily" for day/night periods or "hourly" for hour-by-hour detail (default: "daily")',
92
+ enum: ['daily', 'hourly'],
93
+ default: 'daily'
94
+ },
95
+ include_precipitation_probability: {
96
+ type: 'boolean',
97
+ description: 'Include precipitation probability in the forecast output (default: true)',
98
+ default: true
99
+ },
100
+ include_severe_weather: {
101
+ type: 'boolean',
102
+ description: 'Include severe weather probabilities such as thunderstorm chance, wind gust probabilities, and tropical storm/hurricane risks (default: false, US/NOAA only)',
103
+ default: false
104
+ },
105
+ source: {
106
+ type: 'string',
107
+ description: 'Data source: "auto" (default, selects NOAA for US or Open-Meteo for international), "noaa" (US only), or "openmeteo" (global)',
108
+ enum: ['auto', 'noaa', 'openmeteo'],
109
+ default: 'auto'
71
110
  }
72
111
  },
73
112
  required: ['latitude', 'longitude']
@@ -75,7 +114,34 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
75
114
  },
76
115
  {
77
116
  name: 'get_current_conditions',
78
- description: 'Get the most recent weather observation for a location (US only). Use this for current weather or when asking about "today\'s weather", "right now", or recent conditions without a specific historical date range. Returns the latest observation from the nearest weather station. For specific past dates or date ranges, use get_historical_weather instead. If this tool returns an error, check the error message for status page links and consider using check_service_status to verify API availability.',
117
+ description: 'Get the most recent weather observation for a location (US only). Use this for current weather or when asking about "today\'s weather", "right now", or recent conditions without a specific historical date range. Returns the latest observation from the nearest weather station. Optionally includes fire weather indices (Haines Index, Grassland Fire Danger, Red Flag Threat) when requested. For specific past dates or date ranges, use get_historical_weather instead. If this tool returns an error, check the error message for status page links and consider using check_service_status to verify API availability.',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ latitude: {
122
+ type: 'number',
123
+ description: 'Latitude of the location (-90 to 90)',
124
+ minimum: -90,
125
+ maximum: 90
126
+ },
127
+ longitude: {
128
+ type: 'number',
129
+ description: 'Longitude of the location (-180 to 180)',
130
+ minimum: -180,
131
+ maximum: 180
132
+ },
133
+ include_fire_weather: {
134
+ type: 'boolean',
135
+ description: 'Include fire weather indices (Haines Index, Grassland Fire Danger, Red Flag Threat) in the response (default: false, US only)',
136
+ default: false
137
+ }
138
+ },
139
+ required: ['latitude', 'longitude']
140
+ }
141
+ },
142
+ {
143
+ name: 'get_alerts',
144
+ description: 'Get active weather alerts, watches, warnings, and advisories for a location (US only). Use this for safety-critical weather information when asked about "any alerts?", "weather warnings?", "is it safe?", "dangerous weather?", or "weather watches?". Returns severity, urgency, certainty, effective/expiration times, and affected areas. For forecast data, use get_forecast instead. If this tool returns an error, check the error message for status page links and consider using check_service_status to verify API availability.',
79
145
  inputSchema: {
80
146
  type: 'object',
81
147
  properties: {
@@ -90,6 +156,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
90
156
  description: 'Longitude of the location (-180 to 180)',
91
157
  minimum: -180,
92
158
  maximum: 180
159
+ },
160
+ active_only: {
161
+ type: 'boolean',
162
+ description: 'Whether to show only active alerts (default: true)',
163
+ default: true
93
164
  }
94
165
  },
95
166
  required: ['latitude', 'longitude']
@@ -140,6 +211,81 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
140
211
  properties: {},
141
212
  required: []
142
213
  }
214
+ },
215
+ {
216
+ name: 'search_location',
217
+ description: 'Search for locations by name to get coordinates for weather queries. Use this when the user provides a location name instead of coordinates (e.g., "Paris", "New York", "Tokyo", "San Francisco, CA"). Returns location matches with coordinates, timezone, elevation, and other metadata. Enables natural language location queries like "What\'s the weather in Paris?" by converting location names to coordinates.',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ query: {
222
+ type: 'string',
223
+ description: 'Location name to search for (e.g., "Paris", "New York, NY", "Tokyo")'
224
+ },
225
+ limit: {
226
+ type: 'number',
227
+ description: 'Maximum number of results to return (1-100, default: 5)',
228
+ minimum: 1,
229
+ maximum: 100,
230
+ default: 5
231
+ }
232
+ },
233
+ required: ['query']
234
+ }
235
+ },
236
+ {
237
+ name: 'get_air_quality',
238
+ description: 'Get air quality data including AQI (Air Quality Index), pollutant concentrations, and UV index for a location (global coverage). Use this when asked about "air quality", "pollution", "AQI", "UV index", "safe to exercise outside", or health-related environmental conditions. Returns current conditions and optional hourly forecast. Shows appropriate AQI scale (US AQI for US locations, European EAQI elsewhere) with health recommendations. Pollutants include PM2.5, PM10, ozone, NO2, SO2, and CO.',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ latitude: {
243
+ type: 'number',
244
+ description: 'Latitude of the location (-90 to 90)',
245
+ minimum: -90,
246
+ maximum: 90
247
+ },
248
+ longitude: {
249
+ type: 'number',
250
+ description: 'Longitude of the location (-180 to 180)',
251
+ minimum: -180,
252
+ maximum: 180
253
+ },
254
+ forecast: {
255
+ type: 'boolean',
256
+ description: 'Include hourly air quality forecast for next 5 days (default: false, shows current only)',
257
+ default: false
258
+ }
259
+ },
260
+ required: ['latitude', 'longitude']
261
+ }
262
+ },
263
+ {
264
+ name: 'get_marine_conditions',
265
+ description: 'Get marine conditions including wave height, swell, ocean currents, and sea state for a location (global coverage). Use this when asked about "ocean conditions", "wave height", "surf conditions", "safe to boat", "marine forecast", "swell", or "sea state". Returns current conditions and optional daily/hourly forecast. Includes significant wave height, wind waves, swell, wave period, and ocean currents. Shows safety assessment for maritime activities. NOTE: Data has limited accuracy in coastal areas and is NOT suitable for coastal navigation - always consult official marine forecasts.',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ latitude: {
270
+ type: 'number',
271
+ description: 'Latitude of the location (-90 to 90)',
272
+ minimum: -90,
273
+ maximum: 90
274
+ },
275
+ longitude: {
276
+ type: 'number',
277
+ description: 'Longitude of the location (-180 to 180)',
278
+ minimum: -180,
279
+ maximum: 180
280
+ },
281
+ forecast: {
282
+ type: 'boolean',
283
+ description: 'Include marine forecast for next 5 days (default: false, shows current only)',
284
+ default: false
285
+ }
286
+ },
287
+ required: ['latitude', 'longitude']
288
+ }
143
289
  }
144
290
  ]
145
291
  };
@@ -151,382 +297,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
151
297
  const { name, arguments: args } = request.params;
152
298
  try {
153
299
  switch (name) {
154
- case 'get_forecast': {
155
- const { latitude, longitude, days = 7 } = args;
156
- // Get forecast data
157
- const forecast = await noaaService.getForecastByCoordinates(latitude, longitude);
158
- const periods = forecast.properties.periods.slice(0, days * 2); // Each day typically has 2 periods (day/night)
159
- // Format the forecast for display
160
- let output = `# Weather Forecast\n\n`;
161
- output += `**Location:** ${forecast.properties.elevation.value}m elevation\n`;
162
- output += `**Updated:** ${new Date(forecast.properties.updated).toLocaleString()}\n\n`;
163
- for (const period of periods) {
164
- output += `## ${period.name}\n`;
165
- output += `**Temperature:** ${period.temperature}°${period.temperatureUnit}\n`;
166
- output += `**Wind:** ${period.windSpeed} ${period.windDirection}\n`;
167
- output += `**Forecast:** ${period.shortForecast}\n\n`;
168
- if (period.detailedForecast) {
169
- output += `${period.detailedForecast}\n\n`;
170
- }
171
- }
172
- return {
173
- content: [
174
- {
175
- type: 'text',
176
- text: output
177
- }
178
- ]
179
- };
180
- }
181
- case 'get_current_conditions': {
182
- const { latitude, longitude } = args;
183
- // Get current observation
184
- const observation = await noaaService.getCurrentConditions(latitude, longitude);
185
- const props = observation.properties;
186
- // Format current conditions
187
- let output = `# Current Weather Conditions\n\n`;
188
- output += `**Station:** ${props.station}\n`;
189
- output += `**Time:** ${new Date(props.timestamp).toLocaleString()}\n\n`;
190
- if (props.textDescription) {
191
- output += `**Conditions:** ${props.textDescription}\n`;
192
- }
193
- if (props.temperature.value !== null) {
194
- const tempF = props.temperature.unitCode.includes('degC')
195
- ? (props.temperature.value * 9 / 5) + 32
196
- : props.temperature.value;
197
- output += `**Temperature:** ${Math.round(tempF)}°F\n`;
198
- }
199
- if (props.dewpoint.value !== null) {
200
- const dewF = props.dewpoint.unitCode.includes('degC')
201
- ? (props.dewpoint.value * 9 / 5) + 32
202
- : props.dewpoint.value;
203
- output += `**Dewpoint:** ${Math.round(dewF)}°F\n`;
204
- }
205
- if (props.relativeHumidity.value !== null) {
206
- output += `**Humidity:** ${Math.round(props.relativeHumidity.value)}%\n`;
207
- }
208
- if (props.windSpeed.value !== null) {
209
- const windMph = props.windSpeed.unitCode.includes('km_h')
210
- ? props.windSpeed.value * 0.621371
211
- : props.windSpeed.value * 2.23694; // m/s to mph
212
- const windDir = props.windDirection.value;
213
- output += `**Wind:** ${Math.round(windMph)} mph`;
214
- if (windDir !== null) {
215
- output += ` from ${Math.round(windDir)}°`;
216
- }
217
- output += `\n`;
218
- }
219
- if (props.barometricPressure.value !== null) {
220
- const pressureInHg = props.barometricPressure.value * 0.0002953;
221
- output += `**Pressure:** ${pressureInHg.toFixed(2)} inHg\n`;
222
- }
223
- if (props.visibility.value !== null) {
224
- const visibilityMiles = props.visibility.value * 0.000621371;
225
- output += `**Visibility:** ${visibilityMiles.toFixed(1)} miles\n`;
226
- }
227
- return {
228
- content: [
229
- {
230
- type: 'text',
231
- text: output
232
- }
233
- ]
234
- };
235
- }
236
- case 'get_historical_weather': {
237
- const { latitude, longitude, start_date, end_date, limit = 168 } = args;
238
- // Parse dates
239
- const startTime = new Date(start_date);
240
- const endTime = new Date(end_date);
241
- // Validate date parsing
242
- if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
243
- throw new Error('Invalid date format. Please use ISO format (YYYY-MM-DD or full ISO 8601 datetime).');
244
- }
245
- // Validate date range
246
- if (startTime > endTime) {
247
- throw new Error(`Invalid date range: start date (${start_date}) must be before end date (${end_date}).`);
248
- }
249
- // Validate dates are not in the future
250
- const now = new Date();
251
- if (startTime > now) {
252
- throw new Error(`Start date (${start_date}) cannot be in the future. Current date is ${now.toISOString().split('T')[0]}.`);
253
- }
254
- if (endTime > now) {
255
- throw new Error(`End date (${end_date}) cannot be in the future. Current date is ${now.toISOString().split('T')[0]}.`);
256
- }
257
- // Determine which API to use based on date range
258
- // If start date is more than 7 days old, use CDO API for archival data
259
- const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
260
- const useArchivalData = startTime < sevenDaysAgo;
261
- if (useArchivalData) {
262
- // Use Open-Meteo API for historical/archival data
263
- try {
264
- // Determine whether to use hourly or daily data based on date range
265
- const daysDiff = Math.ceil((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60 * 24));
266
- const useHourly = daysDiff <= 31; // Use hourly for up to 31 days
267
- const weatherData = await openMeteoService.getHistoricalWeather(latitude, longitude, start_date.split('T')[0], // Ensure YYYY-MM-DD format
268
- end_date.split('T')[0], useHourly);
269
- // Format the response based on data granularity
270
- if (useHourly && weatherData.hourly) {
271
- // Format hourly observations
272
- let output = `# Historical Weather Observations (Hourly)\n\n`;
273
- output += `**Period:** ${startTime.toLocaleDateString()} to ${endTime.toLocaleDateString()}\n`;
274
- output += `**Location:** ${weatherData.latitude.toFixed(4)}°N, ${Math.abs(weatherData.longitude).toFixed(4)}°${weatherData.longitude >= 0 ? 'E' : 'W'} (${weatherData.elevation}m elevation)\n`;
275
- output += `**Number of observations:** ${weatherData.hourly.time.length}\n`;
276
- output += `**Data source:** Open-Meteo Historical Weather API (Reanalysis)\n\n`;
277
- const maxObservations = Math.min(limit, weatherData.hourly.time.length);
278
- for (let i = 0; i < maxObservations; i++) {
279
- const time = new Date(weatherData.hourly.time[i]);
280
- output += `## ${time.toLocaleString()}\n`;
281
- if (weatherData.hourly.temperature_2m?.[i] !== null && weatherData.hourly.temperature_2m?.[i] !== undefined) {
282
- output += `- **Temperature:** ${Math.round(weatherData.hourly.temperature_2m[i])}°F\n`;
283
- }
284
- if (weatherData.hourly.apparent_temperature?.[i] !== null && weatherData.hourly.apparent_temperature?.[i] !== undefined) {
285
- output += `- **Feels Like:** ${Math.round(weatherData.hourly.apparent_temperature[i])}°F\n`;
286
- }
287
- if (weatherData.hourly.weather_code?.[i] !== null && weatherData.hourly.weather_code?.[i] !== undefined) {
288
- output += `- **Conditions:** ${openMeteoService.getWeatherDescription(weatherData.hourly.weather_code[i])}\n`;
289
- }
290
- if (weatherData.hourly.precipitation?.[i] !== null && weatherData.hourly.precipitation?.[i] !== undefined && weatherData.hourly.precipitation[i] > 0) {
291
- output += `- **Precipitation:** ${weatherData.hourly.precipitation[i].toFixed(2)} in\n`;
292
- }
293
- if (weatherData.hourly.snowfall?.[i] !== null && weatherData.hourly.snowfall?.[i] !== undefined && weatherData.hourly.snowfall[i] > 0) {
294
- output += `- **Snowfall:** ${weatherData.hourly.snowfall[i].toFixed(1)} in\n`;
295
- }
296
- if (weatherData.hourly.wind_speed_10m?.[i] !== null && weatherData.hourly.wind_speed_10m?.[i] !== undefined) {
297
- output += `- **Wind:** ${Math.round(weatherData.hourly.wind_speed_10m[i])} mph`;
298
- if (weatherData.hourly.wind_direction_10m?.[i] !== null && weatherData.hourly.wind_direction_10m?.[i] !== undefined) {
299
- output += ` from ${Math.round(weatherData.hourly.wind_direction_10m[i])}°`;
300
- }
301
- output += `\n`;
302
- }
303
- if (weatherData.hourly.relative_humidity_2m?.[i] !== null && weatherData.hourly.relative_humidity_2m?.[i] !== undefined) {
304
- output += `- **Humidity:** ${Math.round(weatherData.hourly.relative_humidity_2m[i])}%\n`;
305
- }
306
- if (weatherData.hourly.pressure_msl?.[i] !== null && weatherData.hourly.pressure_msl?.[i] !== undefined) {
307
- const pressureInHg = weatherData.hourly.pressure_msl[i] * 0.02953;
308
- output += `- **Pressure:** ${pressureInHg.toFixed(2)} inHg\n`;
309
- }
310
- if (weatherData.hourly.cloud_cover?.[i] !== null && weatherData.hourly.cloud_cover?.[i] !== undefined) {
311
- output += `- **Cloud Cover:** ${weatherData.hourly.cloud_cover[i]}%\n`;
312
- }
313
- output += `\n`;
314
- }
315
- return {
316
- content: [
317
- {
318
- type: 'text',
319
- text: output
320
- }
321
- ]
322
- };
323
- }
324
- else if (weatherData.daily) {
325
- // Format daily summaries
326
- let output = `# Historical Weather Data (Daily Summaries)\n\n`;
327
- output += `**Period:** ${startTime.toLocaleDateString()} to ${endTime.toLocaleDateString()}\n`;
328
- output += `**Location:** ${weatherData.latitude.toFixed(4)}°N, ${Math.abs(weatherData.longitude).toFixed(4)}°${weatherData.longitude >= 0 ? 'E' : 'W'} (${weatherData.elevation}m elevation)\n`;
329
- output += `**Number of days:** ${weatherData.daily.time.length}\n`;
330
- output += `**Data source:** Open-Meteo Historical Weather API (Reanalysis)\n\n`;
331
- for (let i = 0; i < weatherData.daily.time.length; i++) {
332
- const date = new Date(weatherData.daily.time[i]);
333
- output += `## ${date.toLocaleDateString()}\n`;
334
- if (weatherData.daily.temperature_2m_max?.[i] !== null && weatherData.daily.temperature_2m_max?.[i] !== undefined) {
335
- output += `- **High Temperature:** ${Math.round(weatherData.daily.temperature_2m_max[i])}°F\n`;
336
- }
337
- if (weatherData.daily.temperature_2m_min?.[i] !== null && weatherData.daily.temperature_2m_min?.[i] !== undefined) {
338
- output += `- **Low Temperature:** ${Math.round(weatherData.daily.temperature_2m_min[i])}°F\n`;
339
- }
340
- if (weatherData.daily.temperature_2m_mean?.[i] !== null && weatherData.daily.temperature_2m_mean?.[i] !== undefined) {
341
- output += `- **Average Temperature:** ${Math.round(weatherData.daily.temperature_2m_mean[i])}°F\n`;
342
- }
343
- if (weatherData.daily.weather_code?.[i] !== null && weatherData.daily.weather_code?.[i] !== undefined) {
344
- output += `- **Conditions:** ${openMeteoService.getWeatherDescription(weatherData.daily.weather_code[i])}\n`;
345
- }
346
- if (weatherData.daily.precipitation_sum?.[i] !== null && weatherData.daily.precipitation_sum?.[i] !== undefined) {
347
- output += `- **Precipitation:** ${weatherData.daily.precipitation_sum[i].toFixed(2)} in\n`;
348
- }
349
- if (weatherData.daily.snowfall_sum?.[i] !== null && weatherData.daily.snowfall_sum?.[i] !== undefined && weatherData.daily.snowfall_sum[i] > 0) {
350
- output += `- **Snowfall:** ${weatherData.daily.snowfall_sum[i].toFixed(1)} in\n`;
351
- }
352
- if (weatherData.daily.wind_speed_10m_max?.[i] !== null && weatherData.daily.wind_speed_10m_max?.[i] !== undefined) {
353
- output += `- **Max Wind Speed:** ${Math.round(weatherData.daily.wind_speed_10m_max[i])} mph\n`;
354
- }
355
- output += `\n`;
356
- }
357
- return {
358
- content: [
359
- {
360
- type: 'text',
361
- text: output
362
- }
363
- ]
364
- };
365
- }
366
- else {
367
- throw new Error('No weather data available in response');
368
- }
369
- }
370
- catch (error) {
371
- // If Open-Meteo API fails, provide helpful error message
372
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
373
- throw new Error(`Unable to retrieve historical data: ${errorMessage}`);
374
- }
375
- }
376
- else {
377
- // Use real-time NOAA API for recent data (last 7 days)
378
- const observations = await noaaService.getHistoricalObservations(latitude, longitude, startTime, endTime, limit);
379
- if (!observations.features || observations.features.length === 0) {
380
- return {
381
- content: [
382
- {
383
- type: 'text',
384
- text: `No historical observations found for the specified date range (${start_date} to ${end_date}).\n\nThis may occur because:\n- The dates are outside the station's available data range\n- There are gaps in the observation records for this location\n- The weather station near this location may not have archived data for these dates\n\nNote: Historical weather data availability varies by location and weather station. Some stations have limited historical records.`
385
- }
386
- ]
387
- };
388
- }
389
- // Format the observations
390
- let output = `# Historical Weather Observations\n\n`;
391
- output += `**Period:** ${startTime.toLocaleDateString()} to ${endTime.toLocaleDateString()}\n`;
392
- output += `**Number of observations:** ${observations.features.length}\n`;
393
- output += `**Data source:** NOAA Real-time API\n\n`;
394
- for (const obs of observations.features) {
395
- const props = obs.properties;
396
- output += `## ${new Date(props.timestamp).toLocaleString()}\n`;
397
- if (props.temperature.value !== null) {
398
- const tempF = props.temperature.unitCode.includes('degC')
399
- ? (props.temperature.value * 9 / 5) + 32
400
- : props.temperature.value;
401
- output += `- **Temperature:** ${Math.round(tempF)}°F\n`;
402
- }
403
- if (props.textDescription) {
404
- output += `- **Conditions:** ${props.textDescription}\n`;
405
- }
406
- if (props.windSpeed.value !== null) {
407
- const windMph = props.windSpeed.unitCode.includes('km_h')
408
- ? props.windSpeed.value * 0.621371
409
- : props.windSpeed.value * 2.23694;
410
- output += `- **Wind:** ${Math.round(windMph)} mph\n`;
411
- }
412
- output += `\n`;
413
- }
414
- return {
415
- content: [
416
- {
417
- type: 'text',
418
- text: output
419
- }
420
- ]
421
- };
422
- }
423
- }
424
- case 'check_service_status': {
425
- // Check status of both services
426
- const noaaStatus = await noaaService.checkServiceStatus();
427
- const openMeteoStatus = await openMeteoService.checkServiceStatus();
428
- // Format the status report
429
- let output = `# Weather API Service Status\n\n`;
430
- output += `**Check Time:** ${new Date().toLocaleString()}\n\n`;
431
- // NOAA Status
432
- output += `## NOAA Weather API (Forecasts & Current Conditions)\n\n`;
433
- output += `**Status:** ${noaaStatus.operational ? '✅ Operational' : '❌ Issues Detected'}\n`;
434
- output += `**Message:** ${noaaStatus.message}\n`;
435
- output += `**Status Page:** ${noaaStatus.statusPage}\n`;
436
- output += `**Coverage:** United States locations only\n\n`;
437
- if (!noaaStatus.operational) {
438
- output += `**Recommended Actions:**\n`;
439
- output += `- Check planned outages: https://weather-gov.github.io/api/planned-outages\n`;
440
- output += `- View service notices: https://www.weather.gov/notification\n`;
441
- output += `- Report issues: nco.ops@noaa.gov or (301) 683-1518\n\n`;
442
- }
443
- // Open-Meteo Status
444
- output += `## Open-Meteo API (Historical Weather Data)\n\n`;
445
- output += `**Status:** ${openMeteoStatus.operational ? '✅ Operational' : '❌ Issues Detected'}\n`;
446
- output += `**Message:** ${openMeteoStatus.message}\n`;
447
- output += `**Status Page:** ${openMeteoStatus.statusPage}\n`;
448
- output += `**Coverage:** Global (worldwide locations)\n\n`;
449
- if (!openMeteoStatus.operational) {
450
- output += `**Recommended Actions:**\n`;
451
- output += `- Check production status: https://open-meteo.com/en/docs/model-updates\n`;
452
- output += `- View GitHub issues: https://github.com/open-meteo/open-meteo/issues\n`;
453
- output += `- Review documentation: https://open-meteo.com/en/docs\n\n`;
454
- }
455
- // Cache Statistics
456
- if (CacheConfig.enabled) {
457
- output += `## Cache Statistics\n\n`;
458
- const noaaStats = noaaService.getCacheStats();
459
- const openMeteoStats = openMeteoService.getCacheStats();
460
- const totalHits = noaaStats.hits + openMeteoStats.hits;
461
- const totalMisses = noaaStats.misses + openMeteoStats.misses;
462
- const totalRequests = totalHits + totalMisses;
463
- const overallHitRate = totalRequests > 0 ? ((totalHits / totalRequests) * 100).toFixed(1) : '0.0';
464
- output += `**Cache Status:** ✅ Enabled\n`;
465
- output += `**Overall Hit Rate:** ${overallHitRate}%\n`;
466
- output += `**Total Cache Hits:** ${totalHits}\n`;
467
- output += `**Total Cache Misses:** ${totalMisses}\n`;
468
- output += `**Total Requests:** ${totalRequests}\n\n`;
469
- output += `### NOAA Service Cache\n`;
470
- output += `- Entries: ${noaaStats.size} / ${noaaStats.maxSize}\n`;
471
- output += `- Hit Rate: ${noaaService.cache.getHitRate().toFixed(1)}%\n`;
472
- output += `- Hits: ${noaaStats.hits}\n`;
473
- output += `- Misses: ${noaaStats.misses}\n`;
474
- output += `- Evictions: ${noaaStats.evictions}\n\n`;
475
- output += `### Open-Meteo Service Cache\n`;
476
- output += `- Entries: ${openMeteoStats.size} / ${openMeteoStats.maxSize}\n`;
477
- output += `- Hit Rate: ${openMeteoService.cache.getHitRate().toFixed(1)}%\n`;
478
- output += `- Hits: ${openMeteoStats.hits}\n`;
479
- output += `- Misses: ${openMeteoStats.misses}\n`;
480
- output += `- Evictions: ${openMeteoStats.evictions}\n\n`;
481
- output += `*Cache reduces API calls and improves performance for repeated queries.*\n\n`;
482
- }
483
- else {
484
- output += `## Cache Statistics\n\n`;
485
- output += `**Cache Status:** ❌ Disabled\n`;
486
- output += `*Set CACHE_ENABLED=true in environment to enable caching.*\n\n`;
487
- }
488
- // Overall status summary
489
- const bothOperational = noaaStatus.operational && openMeteoStatus.operational;
490
- const neitherOperational = !noaaStatus.operational && !openMeteoStatus.operational;
491
- if (bothOperational) {
492
- output += `## Overall Status: ✅ All Services Operational\n\n`;
493
- output += `Both NOAA and Open-Meteo APIs are functioning normally. Weather data requests should succeed.\n`;
494
- }
495
- else if (neitherOperational) {
496
- output += `## Overall Status: ❌ Multiple Service Issues\n\n`;
497
- output += `Both weather APIs are experiencing issues. Please check the status pages above for updates.\n`;
498
- }
499
- else {
500
- output += `## Overall Status: ⚠️ Partial Service Availability\n\n`;
501
- if (noaaStatus.operational) {
502
- output += `NOAA API is operational: Forecasts and current conditions for US locations are available.\n`;
503
- output += `Open-Meteo API has issues: Historical weather data may be unavailable.\n`;
504
- }
505
- else {
506
- output += `Open-Meteo API is operational: Historical weather data is available globally.\n`;
507
- output += `NOAA API has issues: Forecasts and current conditions for US locations may be unavailable.\n`;
508
- }
509
- }
510
- return {
511
- content: [
512
- {
513
- type: 'text',
514
- text: output
515
- }
516
- ]
517
- };
518
- }
300
+ case 'get_forecast':
301
+ return await handleGetForecast(args, noaaService, openMeteoService);
302
+ case 'get_current_conditions':
303
+ return await handleGetCurrentConditions(args, noaaService);
304
+ case 'get_alerts':
305
+ return await handleGetAlerts(args, noaaService);
306
+ case 'get_historical_weather':
307
+ return await handleGetHistoricalWeather(args, noaaService, openMeteoService);
308
+ case 'check_service_status':
309
+ return await handleCheckServiceStatus(noaaService, openMeteoService);
310
+ case 'search_location':
311
+ return await handleSearchLocation(args, openMeteoService);
312
+ case 'get_air_quality':
313
+ return await handleGetAirQuality(args, openMeteoService);
314
+ case 'get_marine_conditions':
315
+ return await handleGetMarineConditions(args, openMeteoService);
519
316
  default:
520
317
  throw new Error(`Unknown tool: ${name}`);
521
318
  }
522
319
  }
523
320
  catch (error) {
524
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
321
+ // Log the error with full details
322
+ logger.error('Tool execution error', error, {
323
+ tool: name,
324
+ args: args ? JSON.stringify(args) : undefined,
325
+ });
326
+ // Format error for user display (sanitized)
327
+ const userMessage = formatErrorForUser(error);
525
328
  return {
526
329
  content: [
527
330
  {
528
331
  type: 'text',
529
- text: `Error: ${errorMessage}`
332
+ text: userMessage
530
333
  }
531
334
  ],
532
335
  isError: true
@@ -538,12 +341,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
341
  */
539
342
  async function main() {
540
343
  const transport = new StdioServerTransport();
541
- await server.connect(transport);
542
- // Log to stderr so it doesn't interfere with MCP communication
543
- console.error('Weather MCP Server running on stdio');
344
+ // Set up graceful shutdown handlers
345
+ const shutdown = async (signal) => {
346
+ logger.info(`Received ${signal}, shutting down gracefully...`);
347
+ try {
348
+ // Clean up resources
349
+ noaaService.clearCache();
350
+ openMeteoService.clearCache();
351
+ logger.info('Cache cleared');
352
+ // Close server connection
353
+ await server.close();
354
+ logger.info('Server closed');
355
+ process.exit(0);
356
+ }
357
+ catch (error) {
358
+ logger.error('Error during shutdown', error);
359
+ process.exit(1);
360
+ }
361
+ };
362
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
363
+ process.on('SIGINT', () => shutdown('SIGINT'));
364
+ try {
365
+ await server.connect(transport);
366
+ logger.info('Weather MCP Server started', {
367
+ version: SERVER_VERSION,
368
+ cacheEnabled: CacheConfig.enabled,
369
+ logLevel: process.env.LOG_LEVEL || 'INFO',
370
+ });
371
+ }
372
+ catch (error) {
373
+ logger.error('Failed to start server', error);
374
+ throw error;
375
+ }
544
376
  }
545
377
  main().catch((error) => {
546
- console.error('Fatal error in main():', error);
378
+ logger.error('Fatal error in main()', error);
379
+ // Log structured error for monitoring
380
+ console.error(JSON.stringify({
381
+ timestamp: new Date().toISOString(),
382
+ level: 'FATAL',
383
+ message: 'Application failed to start',
384
+ error: {
385
+ message: error instanceof Error ? error.message : String(error),
386
+ stack: error instanceof Error ? error.stack : undefined,
387
+ }
388
+ }));
547
389
  process.exit(1);
548
390
  });
549
391
  //# sourceMappingURL=index.js.map