@dangahagan/weather-mcp 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +252 -53
  2. package/dist/config/cache.d.ts +1 -0
  3. package/dist/config/cache.d.ts.map +1 -1
  4. package/dist/config/cache.js +45 -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/alertsHandler.d.ts +11 -0
  15. package/dist/handlers/alertsHandler.d.ts.map +1 -0
  16. package/dist/handlers/alertsHandler.js +79 -0
  17. package/dist/handlers/alertsHandler.js.map +1 -0
  18. package/dist/handlers/currentConditionsHandler.d.ts +11 -0
  19. package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
  20. package/dist/handlers/currentConditionsHandler.js +168 -0
  21. package/dist/handlers/currentConditionsHandler.js.map +1 -0
  22. package/dist/handlers/forecastHandler.d.ts +13 -0
  23. package/dist/handlers/forecastHandler.d.ts.map +1 -0
  24. package/dist/handlers/forecastHandler.js +226 -0
  25. package/dist/handlers/forecastHandler.js.map +1 -0
  26. package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
  27. package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
  28. package/dist/handlers/historicalWeatherHandler.js +188 -0
  29. package/dist/handlers/historicalWeatherHandler.js.map +1 -0
  30. package/dist/handlers/locationHandler.d.ts +11 -0
  31. package/dist/handlers/locationHandler.d.ts.map +1 -0
  32. package/dist/handlers/locationHandler.js +113 -0
  33. package/dist/handlers/locationHandler.js.map +1 -0
  34. package/dist/handlers/statusHandler.d.ts +12 -0
  35. package/dist/handlers/statusHandler.d.ts.map +1 -0
  36. package/dist/handlers/statusHandler.js +106 -0
  37. package/dist/handlers/statusHandler.js.map +1 -0
  38. package/dist/index.js +148 -376
  39. package/dist/index.js.map +1 -1
  40. package/dist/services/noaa.d.ts +14 -1
  41. package/dist/services/noaa.d.ts.map +1 -1
  42. package/dist/services/noaa.js +74 -42
  43. package/dist/services/noaa.js.map +1 -1
  44. package/dist/services/openmeteo.d.ts +54 -3
  45. package/dist/services/openmeteo.d.ts.map +1 -1
  46. package/dist/services/openmeteo.js +256 -112
  47. package/dist/services/openmeteo.js.map +1 -1
  48. package/dist/types/noaa.d.ts +64 -12
  49. package/dist/types/noaa.d.ts.map +1 -1
  50. package/dist/types/openmeteo.d.ts +156 -0
  51. package/dist/types/openmeteo.d.ts.map +1 -1
  52. package/dist/utils/cache.d.ts +11 -4
  53. package/dist/utils/cache.d.ts.map +1 -1
  54. package/dist/utils/cache.js +33 -4
  55. package/dist/utils/cache.js.map +1 -1
  56. package/dist/utils/logger.d.ts +59 -0
  57. package/dist/utils/logger.d.ts.map +1 -0
  58. package/dist/utils/logger.js +128 -0
  59. package/dist/utils/logger.js.map +1 -0
  60. package/dist/utils/temperatureConversion.d.ts +12 -0
  61. package/dist/utils/temperatureConversion.d.ts.map +1 -0
  62. package/dist/utils/temperatureConversion.js +17 -0
  63. package/dist/utils/temperatureConversion.js.map +1 -0
  64. package/dist/utils/validation.d.ts +89 -0
  65. package/dist/utils/validation.d.ts.map +1 -0
  66. package/dist/utils/validation.js +177 -0
  67. package/dist/utils/validation.js.map +1 -0
  68. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -11,16 +11,31 @@ 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';
14
22
  /**
15
23
  * Server information
16
24
  */
25
+ import { readFileSync } from 'fs';
26
+ import { fileURLToPath } from 'url';
27
+ import { dirname, join } from 'path';
28
+ // Read version from package.json to ensure single source of truth
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
17
32
  const SERVER_NAME = 'weather-mcp';
18
- const SERVER_VERSION = '0.1.0';
33
+ const SERVER_VERSION = packageJson.version;
19
34
  /**
20
35
  * Initialize the NOAA service
21
36
  */
22
37
  const noaaService = new NOAAService({
23
- userAgent: '(weather-mcp, github.com/weather-mcp)'
38
+ userAgent: `weather-mcp/${SERVER_VERSION} (https://github.com/dgahagan/weather-mcp)`
24
39
  });
25
40
  /**
26
41
  * Initialize the Open-Meteo service for historical data
@@ -46,7 +61,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
46
61
  tools: [
47
62
  {
48
63
  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.',
64
+ 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
65
  inputSchema: {
51
66
  type: 'object',
52
67
  properties: {
@@ -64,10 +79,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
64
79
  },
65
80
  days: {
66
81
  type: 'number',
67
- description: 'Number of days to include in forecast (1-7, default: 7)',
82
+ description: 'Number of days to include in forecast (1-16 for global, 1-7 for US NOAA, default: 7)',
68
83
  minimum: 1,
69
- maximum: 7,
84
+ maximum: 16,
70
85
  default: 7
86
+ },
87
+ granularity: {
88
+ type: 'string',
89
+ description: 'Forecast granularity: "daily" for day/night periods or "hourly" for hour-by-hour detail (default: "daily")',
90
+ enum: ['daily', 'hourly'],
91
+ default: 'daily'
92
+ },
93
+ include_precipitation_probability: {
94
+ type: 'boolean',
95
+ description: 'Include precipitation probability in the forecast output (default: true)',
96
+ default: true
97
+ },
98
+ source: {
99
+ type: 'string',
100
+ description: 'Data source: "auto" (default, selects NOAA for US or Open-Meteo for international), "noaa" (US only), or "openmeteo" (global)',
101
+ enum: ['auto', 'noaa', 'openmeteo'],
102
+ default: 'auto'
71
103
  }
72
104
  },
73
105
  required: ['latitude', 'longitude']
@@ -95,6 +127,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
95
127
  required: ['latitude', 'longitude']
96
128
  }
97
129
  },
130
+ {
131
+ name: 'get_alerts',
132
+ 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.',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ latitude: {
137
+ type: 'number',
138
+ description: 'Latitude of the location (-90 to 90)',
139
+ minimum: -90,
140
+ maximum: 90
141
+ },
142
+ longitude: {
143
+ type: 'number',
144
+ description: 'Longitude of the location (-180 to 180)',
145
+ minimum: -180,
146
+ maximum: 180
147
+ },
148
+ active_only: {
149
+ type: 'boolean',
150
+ description: 'Whether to show only active alerts (default: true)',
151
+ default: true
152
+ }
153
+ },
154
+ required: ['latitude', 'longitude']
155
+ }
156
+ },
98
157
  {
99
158
  name: 'get_historical_weather',
100
159
  description: 'Get historical weather data for a specific date range in the past. Use this when the user asks about weather on specific past dates (e.g., "yesterday", "last week", "November 4, 2024", "30 years ago"). Automatically uses NOAA API for recent dates (last 7 days, US only) or Open-Meteo API for older dates (worldwide, back to 1940). Do NOT use for current conditions - use get_current_conditions 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.',
@@ -140,6 +199,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
140
199
  properties: {},
141
200
  required: []
142
201
  }
202
+ },
203
+ {
204
+ name: 'search_location',
205
+ 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.',
206
+ inputSchema: {
207
+ type: 'object',
208
+ properties: {
209
+ query: {
210
+ type: 'string',
211
+ description: 'Location name to search for (e.g., "Paris", "New York, NY", "Tokyo")'
212
+ },
213
+ limit: {
214
+ type: 'number',
215
+ description: 'Maximum number of results to return (1-100, default: 5)',
216
+ minimum: 1,
217
+ maximum: 100,
218
+ default: 5
219
+ }
220
+ },
221
+ required: ['query']
222
+ }
143
223
  }
144
224
  ]
145
225
  };
@@ -151,382 +231,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
151
231
  const { name, arguments: args } = request.params;
152
232
  try {
153
233
  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
- }
234
+ case 'get_forecast':
235
+ return await handleGetForecast(args, noaaService, openMeteoService);
236
+ case 'get_current_conditions':
237
+ return await handleGetCurrentConditions(args, noaaService);
238
+ case 'get_alerts':
239
+ return await handleGetAlerts(args, noaaService);
240
+ case 'get_historical_weather':
241
+ return await handleGetHistoricalWeather(args, noaaService, openMeteoService);
242
+ case 'check_service_status':
243
+ return await handleCheckServiceStatus(noaaService, openMeteoService);
244
+ case 'search_location':
245
+ return await handleSearchLocation(args, openMeteoService);
519
246
  default:
520
247
  throw new Error(`Unknown tool: ${name}`);
521
248
  }
522
249
  }
523
250
  catch (error) {
524
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
251
+ // Log the error with full details
252
+ logger.error('Tool execution error', error, {
253
+ tool: name,
254
+ args: args ? JSON.stringify(args) : undefined,
255
+ });
256
+ // Format error for user display (sanitized)
257
+ const userMessage = formatErrorForUser(error);
525
258
  return {
526
259
  content: [
527
260
  {
528
261
  type: 'text',
529
- text: `Error: ${errorMessage}`
262
+ text: userMessage
530
263
  }
531
264
  ],
532
265
  isError: true
@@ -538,12 +271,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
271
  */
539
272
  async function main() {
540
273
  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');
274
+ // Set up graceful shutdown handlers
275
+ const shutdown = async (signal) => {
276
+ logger.info(`Received ${signal}, shutting down gracefully...`);
277
+ try {
278
+ // Clean up resources
279
+ noaaService.clearCache();
280
+ openMeteoService.clearCache();
281
+ logger.info('Cache cleared');
282
+ // Close server connection
283
+ await server.close();
284
+ logger.info('Server closed');
285
+ process.exit(0);
286
+ }
287
+ catch (error) {
288
+ logger.error('Error during shutdown', error);
289
+ process.exit(1);
290
+ }
291
+ };
292
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
293
+ process.on('SIGINT', () => shutdown('SIGINT'));
294
+ try {
295
+ await server.connect(transport);
296
+ logger.info('Weather MCP Server started', {
297
+ version: SERVER_VERSION,
298
+ cacheEnabled: CacheConfig.enabled,
299
+ logLevel: process.env.LOG_LEVEL || 'INFO',
300
+ });
301
+ }
302
+ catch (error) {
303
+ logger.error('Failed to start server', error);
304
+ throw error;
305
+ }
544
306
  }
545
307
  main().catch((error) => {
546
- console.error('Fatal error in main():', error);
308
+ logger.error('Fatal error in main()', error);
309
+ // Log structured error for monitoring
310
+ console.error(JSON.stringify({
311
+ timestamp: new Date().toISOString(),
312
+ level: 'FATAL',
313
+ message: 'Application failed to start',
314
+ error: {
315
+ message: error instanceof Error ? error.message : String(error),
316
+ stack: error instanceof Error ? error.stack : undefined,
317
+ }
318
+ }));
547
319
  process.exit(1);
548
320
  });
549
321
  //# sourceMappingURL=index.js.map