@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.
- package/README.md +252 -53
- package/dist/config/cache.d.ts +1 -0
- package/dist/config/cache.d.ts.map +1 -1
- package/dist/config/cache.js +45 -2
- package/dist/config/cache.js.map +1 -1
- package/dist/config/displayThresholds.d.ts +83 -0
- package/dist/config/displayThresholds.d.ts.map +1 -0
- package/dist/config/displayThresholds.js +83 -0
- package/dist/config/displayThresholds.js.map +1 -0
- package/dist/errors/ApiError.d.ts +62 -0
- package/dist/errors/ApiError.d.ts.map +1 -0
- package/dist/errors/ApiError.js +147 -0
- package/dist/errors/ApiError.js.map +1 -0
- package/dist/handlers/alertsHandler.d.ts +11 -0
- package/dist/handlers/alertsHandler.d.ts.map +1 -0
- package/dist/handlers/alertsHandler.js +79 -0
- package/dist/handlers/alertsHandler.js.map +1 -0
- package/dist/handlers/currentConditionsHandler.d.ts +11 -0
- package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/currentConditionsHandler.js +168 -0
- package/dist/handlers/currentConditionsHandler.js.map +1 -0
- package/dist/handlers/forecastHandler.d.ts +13 -0
- package/dist/handlers/forecastHandler.d.ts.map +1 -0
- package/dist/handlers/forecastHandler.js +226 -0
- package/dist/handlers/forecastHandler.js.map +1 -0
- package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
- package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
- package/dist/handlers/historicalWeatherHandler.js +188 -0
- package/dist/handlers/historicalWeatherHandler.js.map +1 -0
- package/dist/handlers/locationHandler.d.ts +11 -0
- package/dist/handlers/locationHandler.d.ts.map +1 -0
- package/dist/handlers/locationHandler.js +113 -0
- package/dist/handlers/locationHandler.js.map +1 -0
- package/dist/handlers/statusHandler.d.ts +12 -0
- package/dist/handlers/statusHandler.d.ts.map +1 -0
- package/dist/handlers/statusHandler.js +106 -0
- package/dist/handlers/statusHandler.js.map +1 -0
- package/dist/index.js +148 -376
- package/dist/index.js.map +1 -1
- package/dist/services/noaa.d.ts +14 -1
- package/dist/services/noaa.d.ts.map +1 -1
- package/dist/services/noaa.js +74 -42
- package/dist/services/noaa.js.map +1 -1
- package/dist/services/openmeteo.d.ts +54 -3
- package/dist/services/openmeteo.d.ts.map +1 -1
- package/dist/services/openmeteo.js +256 -112
- package/dist/services/openmeteo.js.map +1 -1
- package/dist/types/noaa.d.ts +64 -12
- package/dist/types/noaa.d.ts.map +1 -1
- package/dist/types/openmeteo.d.ts +156 -0
- package/dist/types/openmeteo.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +11 -4
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +33 -4
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +128 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/temperatureConversion.d.ts +12 -0
- package/dist/utils/temperatureConversion.d.ts.map +1 -0
- package/dist/utils/temperatureConversion.js +17 -0
- package/dist/utils/temperatureConversion.js.map +1 -0
- package/dist/utils/validation.d.ts +89 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +177 -0
- package/dist/utils/validation.js.map +1 -0
- 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 =
|
|
33
|
+
const SERVER_VERSION = packageJson.version;
|
|
19
34
|
/**
|
|
20
35
|
* Initialize the NOAA service
|
|
21
36
|
*/
|
|
22
37
|
const noaaService = new NOAAService({
|
|
23
|
-
userAgent:
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|