Skip to main content
Glama

MCP Weather Server

by dimonets
main.ts19.8 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from "zod"; // Type definitions for better type safety interface GeocodingResult { id: number; name: string; latitude: number; longitude: number; country: string; admin1?: string; } interface GeocodingResponse { results: GeocodingResult[]; generationtime_ms: number; } interface WeatherData { current: { time: string; temperature_2m: number; relative_humidity_2m: number; apparent_temperature: number; precipitation: number; weather_code: number; }; hourly: { time: string[]; temperature_2m: number[]; precipitation: number[]; }; } interface WeatherResponse { latitude: number; longitude: number; current: WeatherData['current']; hourly: WeatherData['hourly']; } interface DailyWeatherData { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; precipitation_sum: number[]; weather_code: number[]; sunrise: string[]; sunset: string[]; } interface ExtendedWeatherResponse { latitude: number; longitude: number; daily: DailyWeatherData; daily_units: { temperature_2m_max: string; temperature_2m_min: string; precipitation_sum: string; }; } interface AirQualityData { time: string[]; european_aqi: number[]; european_aqi_pm2_5: number[]; european_aqi_pm10: number[]; european_aqi_no2: number[]; european_aqi_o3: number[]; european_aqi_so2: number[]; } interface AirQualityResponse { latitude: number; longitude: number; current: { time: string; european_aqi: number; european_aqi_pm2_5: number; european_aqi_pm10: number; european_aqi_no2: number; european_aqi_o3: number; european_aqi_so2: number; }; hourly: AirQualityData; } // Configuration const CONFIG = { GEOCODING_API: 'https://geocoding-api.open-meteo.com/v1/search', WEATHER_API: 'https://api.open-meteo.com/v1/forecast', AIR_QUALITY_API: 'https://air-quality-api.open-meteo.com/v1/air-quality', REQUEST_TIMEOUT: 10000, // 10 seconds MAX_RETRIES: 3, RETRY_DELAY: 1000, // 1 second } as const; // Weather code mapping for human-readable descriptions const WEATHER_CODES: Record<number, string> = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 48: 'Depositing rime fog', 51: 'Light drizzle', 53: 'Moderate drizzle', 55: 'Dense drizzle', 61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain', 71: 'Slight snow', 73: 'Moderate snow', 75: 'Heavy snow', 77: 'Snow grains', 80: 'Slight rain showers', 81: 'Moderate rain showers', 82: 'Violent rain showers', 85: 'Slight snow showers', 86: 'Heavy snow showers', 95: 'Thunderstorm', 96: 'Thunderstorm with slight hail', 99: 'Thunderstorm with heavy hail', }; // Utility functions function delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } function getWeatherDescription(code: number): string { return WEATHER_CODES[code] || 'Unknown weather condition'; } function formatTemperature(temp: number): string { return `${temp.toFixed(1)}°C`; } function formatPrecipitation(precip: number): string { if (precip === 0) return 'No precipitation'; return `${precip.toFixed(1)}mm`; } function formatTime(timeString: string): string { return new Date(timeString).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } function formatDate(dateString: string): string { return new Date(dateString).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }); } function getAqiLevel(aqi: number): { level: string; description: string } { if (aqi <= 20) return { level: 'Good', description: 'Air quality is good' }; if (aqi <= 40) return { level: 'Fair', description: 'Air quality is acceptable' }; if (aqi <= 60) return { level: 'Moderate', description: 'Air quality is moderate' }; if (aqi <= 80) return { level: 'Poor', description: 'Air quality is poor' }; if (aqi <= 100) return { level: 'Very Poor', description: 'Air quality is very poor' }; return { level: 'Hazardous', description: 'Air quality is hazardous' }; } // Enhanced fetch with timeout and retry logic async function fetchWithRetry(url: string, options: RequestInit = {}, retries: number = CONFIG.MAX_RETRIES): Promise<Response> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (error) { clearTimeout(timeoutId); if (retries > 0 && (error instanceof Error && (error.name === 'AbortError' || error.message.includes('HTTP 5')))) { await delay(CONFIG.RETRY_DELAY); return fetchWithRetry(url, options, retries - 1); } throw error; } } // Enhanced weather data interface with processed information interface ProcessedWeatherData { location: { name: string; fullName: string; latitude: number; longitude: number; country: string; admin1?: string; }; current: { time: string; formattedTime: string; temperature: number; formattedTemperature: string; feelsLike: number; formattedFeelsLike: string; humidity: number; precipitation: number; formattedPrecipitation: string; weatherCode: number; weatherDescription: string; }; hourly: Array<{ time: string; formattedTime: string; temperature: number; formattedTemperature: string; precipitation: number; formattedPrecipitation: string; }>; raw: WeatherResponse; // Keep original data for compatibility } interface ProcessedForecastData { location: { name: string; fullName: string; latitude: number; longitude: number; country: string; admin1?: string; }; daily: Array<{ date: string; formattedDate: string; maxTemperature: number; formattedMaxTemperature: string; minTemperature: number; formattedMinTemperature: string; precipitation: number; formattedPrecipitation: string; weatherCode: number; weatherDescription: string; sunrise: string; formattedSunrise: string; sunset: string; formattedSunset: string; }>; raw: ExtendedWeatherResponse; } interface ProcessedAirQualityData { location: { name: string; fullName: string; latitude: number; longitude: number; country: string; admin1?: string; }; current: { time: string; formattedTime: string; europeanAqi: number; aqiLevel: string; aqiDescription: string; pm25: number; pm10: number; no2: number; o3: number; so2: number; }; hourly: Array<{ time: string; formattedTime: string; europeanAqi: number; aqiLevel: string; pm25: number; pm10: number; no2: number; o3: number; so2: number; }>; raw: AirQualityResponse; } // Main weather fetching function async function getWeatherForCity(city: string): Promise<ProcessedWeatherData | string> { // Step 1: Get coordinates for the city const geoUrl = `${CONFIG.GEOCODING_API}?name=${encodeURIComponent(city)}&count=1&language=en&format=json`; const geoResponse = await fetchWithRetry(geoUrl); const geoData: GeocodingResponse = await geoResponse.json(); // Handle city not found if (!geoData.results || geoData.results.length === 0) { return `❌ Sorry, I couldn't find a city named "${city}". Please check the spelling and try again.`; } const location = geoData.results[0]; // Step 2: Get weather data using coordinates const weatherUrl = `${CONFIG.WEATHER_API}?latitude=${location.latitude}&longitude=${location.longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code&hourly=temperature_2m,precipitation&forecast_days=1&timezone=auto`; const weatherResponse = await fetchWithRetry(weatherUrl); const weatherData: WeatherResponse = await weatherResponse.json(); // Process and structure the weather data const current = weatherData.current; const locationName = location.admin1 ? `${location.name}, ${location.admin1}, ${location.country}` : `${location.name}, ${location.country}`; const processedData: ProcessedWeatherData = { location: { name: location.name, fullName: locationName, latitude: location.latitude, longitude: location.longitude, country: location.country, admin1: location.admin1 }, current: { time: current.time, formattedTime: formatTime(current.time), temperature: current.temperature_2m, formattedTemperature: formatTemperature(current.temperature_2m), feelsLike: current.apparent_temperature, formattedFeelsLike: formatTemperature(current.apparent_temperature), humidity: current.relative_humidity_2m, precipitation: current.precipitation, formattedPrecipitation: formatPrecipitation(current.precipitation), weatherCode: current.weather_code, weatherDescription: getWeatherDescription(current.weather_code) }, hourly: weatherData.hourly.time.slice(0, 24).map((time, index) => ({ time: time, formattedTime: formatTime(time), temperature: weatherData.hourly.temperature_2m[index], formattedTemperature: formatTemperature(weatherData.hourly.temperature_2m[index]), precipitation: weatherData.hourly.precipitation[index], formattedPrecipitation: formatPrecipitation(weatherData.hourly.precipitation[index]) })), raw: weatherData }; return processedData; } // Get 7-day weather forecast async function getForecastForCity(city: string): Promise<ProcessedForecastData | string> { // Step 1: Get coordinates for the city const geoUrl = `${CONFIG.GEOCODING_API}?name=${encodeURIComponent(city)}&count=1&language=en&format=json`; const geoResponse = await fetchWithRetry(geoUrl); const geoData: GeocodingResponse = await geoResponse.json(); // Handle city not found if (!geoData.results || geoData.results.length === 0) { return `❌ Sorry, I couldn't find a city named "${city}". Please check the spelling and try again.`; } const location = geoData.results[0]; // Step 2: Get 7-day forecast data const forecastUrl = `${CONFIG.WEATHER_API}?latitude=${location.latitude}&longitude=${location.longitude}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code,sunrise,sunset&timezone=auto`; const forecastResponse = await fetchWithRetry(forecastUrl); const forecastData: ExtendedWeatherResponse = await forecastResponse.json(); // Process and structure the forecast data const locationName = location.admin1 ? `${location.name}, ${location.admin1}, ${location.country}` : `${location.name}, ${location.country}`; const processedData: ProcessedForecastData = { location: { name: location.name, fullName: locationName, latitude: location.latitude, longitude: location.longitude, country: location.country, admin1: location.admin1 }, daily: forecastData.daily.time.map((date, index) => ({ date: date, formattedDate: formatDate(date), maxTemperature: forecastData.daily.temperature_2m_max[index], formattedMaxTemperature: formatTemperature(forecastData.daily.temperature_2m_max[index]), minTemperature: forecastData.daily.temperature_2m_min[index], formattedMinTemperature: formatTemperature(forecastData.daily.temperature_2m_min[index]), precipitation: forecastData.daily.precipitation_sum[index], formattedPrecipitation: formatPrecipitation(forecastData.daily.precipitation_sum[index]), weatherCode: forecastData.daily.weather_code[index], weatherDescription: getWeatherDescription(forecastData.daily.weather_code[index]), sunrise: forecastData.daily.sunrise[index], formattedSunrise: formatTime(forecastData.daily.sunrise[index]), sunset: forecastData.daily.sunset[index], formattedSunset: formatTime(forecastData.daily.sunset[index]) })), raw: forecastData }; return processedData; } // Get air quality data async function getAirQualityForCity(city: string): Promise<ProcessedAirQualityData | string> { // Step 1: Get coordinates for the city const geoUrl = `${CONFIG.GEOCODING_API}?name=${encodeURIComponent(city)}&count=1&language=en&format=json`; const geoResponse = await fetchWithRetry(geoUrl); const geoData: GeocodingResponse = await geoResponse.json(); // Handle city not found if (!geoData.results || geoData.results.length === 0) { return `❌ Sorry, I couldn't find a city named "${city}". Please check the spelling and try again.`; } const location = geoData.results[0]; // Step 2: Get air quality data const aqiUrl = `${CONFIG.AIR_QUALITY_API}?latitude=${location.latitude}&longitude=${location.longitude}&hourly=european_aqi,european_aqi_pm2_5,european_aqi_pm10,european_aqi_no2,european_aqi_o3,european_aqi_so2&current=european_aqi,european_aqi_pm2_5,european_aqi_pm10,european_aqi_no2,european_aqi_o3,european_aqi_so2`; const aqiResponse = await fetchWithRetry(aqiUrl); const aqiData: AirQualityResponse = await aqiResponse.json(); // Process and structure the air quality data const locationName = location.admin1 ? `${location.name}, ${location.admin1}, ${location.country}` : `${location.name}, ${location.country}`; const current = aqiData.current; const aqiInfo = getAqiLevel(current.european_aqi); const processedData: ProcessedAirQualityData = { location: { name: location.name, fullName: locationName, latitude: location.latitude, longitude: location.longitude, country: location.country, admin1: location.admin1 }, current: { time: current.time, formattedTime: formatTime(current.time), europeanAqi: current.european_aqi, aqiLevel: aqiInfo.level, aqiDescription: aqiInfo.description, pm25: current.european_aqi_pm2_5, pm10: current.european_aqi_pm10, no2: current.european_aqi_no2, o3: current.european_aqi_o3, so2: current.european_aqi_so2 }, hourly: aqiData.hourly.time.slice(0, 24).map((time, index) => { const aqi = aqiData.hourly.european_aqi[index]; const aqiInfo = getAqiLevel(aqi); return { time: time, formattedTime: formatTime(time), europeanAqi: aqi, aqiLevel: aqiInfo.level, pm25: aqiData.hourly.european_aqi_pm2_5[index], pm10: aqiData.hourly.european_aqi_pm10[index], no2: aqiData.hourly.european_aqi_no2[index], o3: aqiData.hourly.european_aqi_o3[index], so2: aqiData.hourly.european_aqi_so2[index] }; }), raw: aqiData }; return processedData; } // Create MCP server const server = new McpServer({ name: "Weather Server", version: "1.0.0" }); // Register the weather tool server.tool( 'get-weather', 'Get detailed weather information for any city including current conditions and hourly forecast', { city: z.string() .min(1, "City name cannot be empty") .max(100, "City name is too long") .describe("The name of the city to get weather information for (e.g., 'New York', 'London', 'Tokyo')") }, async({ city }) => { try { const result = await getWeatherForCity(city); // If it's an error string, return it as text if (typeof result === 'string') { return { content: [ { type: "text", text: result } ] }; } // If it's processed data, return it as JSON string for structured access return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { console.error('Weather fetch error:', error); const errorMessage = error instanceof Error && error.message.includes('fetch') ? `❌ Unable to fetch weather data. Please check your internet connection and try again.` : `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`; return { content: [ { type: "text", text: errorMessage } ] }; } } ); // Register the forecast tool server.tool( 'get-forecast', 'Get 7-day weather forecast for any city including daily temperatures, precipitation, and sunrise/sunset times', { city: z.string() .min(1, "City name cannot be empty") .max(100, "City name is too long") .describe("The name of the city to get forecast information for (e.g., 'New York', 'London', 'Tokyo')") }, async({ city }) => { try { const result = await getForecastForCity(city); // If it's an error string, return it as text if (typeof result === 'string') { return { content: [ { type: "text", text: result } ] }; } // If it's processed data, return it as JSON string for structured access return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { console.error('Forecast fetch error:', error); const errorMessage = error instanceof Error && error.message.includes('fetch') ? `❌ Unable to fetch forecast data. Please check your internet connection and try again.` : `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`; return { content: [ { type: "text", text: errorMessage } ] }; } } ); // Register the air quality tool server.tool( 'get-air-quality', 'Get air pollution data for any city including European Air Quality Index and pollutant levels', { city: z.string() .min(1, "City name cannot be empty") .max(100, "City name is too long") .describe("The name of the city to get air quality information for (e.g., 'New York', 'London', 'Tokyo')") }, async({ city }) => { try { const result = await getAirQualityForCity(city); // If it's an error string, return it as text if (typeof result === 'string') { return { content: [ { type: "text", text: result } ] }; } // If it's processed data, return it as JSON string for structured access return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { console.error('Air quality fetch error:', error); const errorMessage = error instanceof Error && error.message.includes('fetch') ? `❌ Unable to fetch air quality data. Please check your internet connection and try again.` : `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`; return { content: [ { type: "text", text: errorMessage } ] }; } } ); // Start the server const transport = new StdioServerTransport(); server.connect(transport);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dimonets/mcp-weather-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server