Skip to main content
Glama
index.ts45.5 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const QWEATHER_API_BASE = process.env.QWEATHER_API_BASE if (!QWEATHER_API_BASE) { throw new Error("QWEATHER_API_BASE env is not set") } const QWEATHER_API_KEY = process.env.QWEATHER_API_KEY if (!QWEATHER_API_KEY) { throw new Error("QWEATHER_API_KEY env is not set") } // Create server instance const server = new McpServer({ name: "qweather", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }); async function makeQWeatherRequest<T>(endpoint: string, params: Record<string, string>, pathParams?: string[]): Promise<T | null> { let url: URL; if (pathParams && pathParams.length > 0) { // Replace placeholders in endpoint with actual values let endpointWithParams = endpoint; pathParams.forEach(param => { endpointWithParams = endpointWithParams.replace('{}', param); }); url = new URL(`${QWEATHER_API_BASE}${endpointWithParams}`); } else { url = new URL(`${QWEATHER_API_BASE}${endpoint}`); } Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); try { const response = await fetch(url.toString(), { headers: { 'X-QW-Api-Key': QWEATHER_API_KEY as string } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}, URL: ${url.toString()}`); } return await response.json() as T; } catch (error) { console.error("Error making QWeather request:", error, "URL:", url.toString()); return null; } } interface QWeatherNowResponse { code: string; updateTime: string; now: { obsTime: string; temp: string; feelsLike: string; text: string; windDir: string; windScale: string; humidity: string; precip: string; pressure: string; vis: string; }; } interface QWeatherDailyResponse { code: string; updateTime: string; fxLink: string; daily: Array<{ fxDate: string; sunrise: string; sunset: string; moonrise: string; moonset: string; moonPhase: string; moonPhaseIcon: string; tempMax: string; tempMin: string; iconDay: string; textDay: string; iconNight: string; textNight: string; wind360Day: string; windDirDay: string; windScaleDay: string; windSpeedDay: string; wind360Night: string; windDirNight: string; windScaleNight: string; windSpeedNight: string; humidity: string; precip: string; pressure: string; vis: string; cloud: string; uvIndex: string; }>; } interface QWeatherLocationResponse { code: string; location: Array<{ name: string; id: string; lat: string; lon: string; adm2: string; adm1: string; country: string; type: string; rank: string; }>; } interface QWeatherWarningResponse { code: string; updateTime: string; fxLink: string; warning: Array<{ id: string; sender: string; pubTime: string; title: string; startTime: string; endTime: string; status: string; severity: string; severityColor: string; type: string; typeName: string; urgency: string; certainty: string; text: string; related: string; }>; } interface QWeatherIndicesResponse { code: string; updateTime: string; fxLink: string; daily: Array<{ date: string; type: string; name: string; level: string; category: string; text: string; }>; } interface QWeatherHourlyResponse { code: string; updateTime: string; fxLink: string; hourly: Array<{ fxTime: string; temp: string; icon: string; text: string; wind360: string; windDir: string; windScale: string; windSpeed: string; humidity: string; precip: string; pressure: string; cloud: string; dew: string; }>; } interface QWeatherMinutelyResponse { code: string; updateTime: string; fxLink: string; summary: string; minutely: Array<{ fxTime: string; precip: string; type: string; }>; } interface QWeatherAirQualityResponse { code: string; metadata: { tag: string; }; indexes: Array<{ code: string; name: string; aqi: number; aqiDisplay: string; level?: string; category?: string; color: { red: number; green: number; blue: number; alpha: number; }; primaryPollutant?: { code: string; name: string; fullName: string; } | null; health?: { effect: string; advice: { generalPopulation: string; sensitivePopulation: string; }; }; }>; pollutants: Array<{ code: string; name: string; fullName: string; concentration: { value: number; unit: string; }; subIndexes?: Array<{ code: string; aqi: number; aqiDisplay: string; }>; }>; stations?: Array<{ id: string; name: string; }>; } interface QWeatherAirQualityHourlyResponse { code: string; metadata: { tag: string; }; hours: Array<{ forecastTime: string; indexes: Array<{ code: string; name: string; aqi: number; aqiDisplay: string; level?: string; category?: string; color: { red: number; green: number; blue: number; alpha: number; }; primaryPollutant?: { code: string; name: string; fullName: string; } | null; health?: { effect: string; advice: { generalPopulation: string; sensitivePopulation: string; }; }; }>; pollutants: Array<{ code: string; name: string; fullName: string; concentration: { value: number; unit: string; }; subIndexes?: Array<{ code: string; aqi: number; aqiDisplay: string; }>; }>; }>; } interface QWeatherAirQualityDailyResponse { code: string; metadata: { tag: string; }; days: Array<{ forecastStartTime: string; forecastEndTime: string; indexes: Array<{ code: string; name: string; aqi: number; aqiDisplay: string; level?: string; category?: string; color: { red: number; green: number; blue: number; alpha: number; }; primaryPollutant?: { code: string; name: string; fullName: string; } | null; health?: { effect: string; advice: { generalPopulation: string; sensitivePopulation: string; }; }; }>; pollutants: Array<{ code: string; name: string; fullName: string; concentration: { value: number; unit: string; }; subIndexes?: Array<{ code: string; aqi: number; aqiDisplay: string; }>; }>; }>; } server.tool( "get-weather-now", "Real-time weather API provides current weather conditions for global cities. Available data includes: temperature, feels-like temperature, weather conditions, wind direction, wind force scale, relative humidity, precipitation, atmospheric pressure, and visibility. The data is updated in real-time to provide the most accurate current weather information.", { cityName: z.string().describe("Name of the city to look up weather for"), }, async ({ cityName }) => { // First, look up the city to get its ID const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's ID const cityId = locationData.location[0].id; const cityInfo = locationData.location[0]; const weatherData = await makeQWeatherRequest<QWeatherNowResponse>("/v7/weather/now", { location: cityId, }); if (!weatherData || weatherData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve current weather data", }, ], }; } const now = weatherData.now; const weatherText = [ `Current Weather for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Temperature: ${now.temp}°C (Feels like: ${now.feelsLike}°C)`, `Condition: ${now.text}`, `Wind: ${now.windDir} Scale ${now.windScale}`, `Humidity: ${now.humidity}%`, `Precipitation: ${now.precip}mm`, `Pressure: ${now.pressure}hPa`, `Visibility: ${now.vis}km`, `Last Updated: ${weatherData.updateTime}`, ].join("\n"); return { content: [ { type: "text", text: weatherText, }, ], }; } ); server.tool( "get-weather-forecast", "Weather forecast API provides detailed weather predictions for global cities, supporting forecasts from 3 to 30 days. Available data includes: sunrise/sunset times, moonrise/moonset times, temperature range, weather conditions, wind direction and speed, relative humidity, precipitation, atmospheric pressure, cloud cover, and UV index. The forecast is updated daily to ensure accuracy.", { cityName: z.string().describe("Name of the city to look up weather for"), days: z.enum(["3d", "7d", "10d", "15d", "30d"]).describe("Number of forecast days"), }, async ({ cityName, days }) => { // First, look up the city to get its ID const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's ID const cityId = locationData.location[0].id; const cityInfo = locationData.location[0]; const weatherData = await makeQWeatherRequest<QWeatherDailyResponse>(`/v7/weather/${days}`, { location: cityId, }); if (!weatherData || weatherData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve weather forecast data", }, ], }; } const forecastText = [ `${days.replace('d', ' Days')} Weather Forecast for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Last Updated: ${weatherData.updateTime}`, '', ...weatherData.daily.map(day => [ `Date: ${day.fxDate}`, `Temperature: ${day.tempMin}°C ~ ${day.tempMax}°C`, `Daytime: ${day.textDay}`, `Night: ${day.textNight}`, `Sunrise: ${day.sunrise || 'N/A'} Sunset: ${day.sunset || 'N/A'}`, `Precipitation: ${day.precip}mm`, `Humidity: ${day.humidity}%`, `Wind: Day-${day.windDirDay}(Scale ${day.windScaleDay}), Night-${day.windDirNight}(Scale ${day.windScaleNight})`, `UV Index: ${day.uvIndex}`, '---' ].join('\n')) ].join('\n'); return { content: [ { type: "text", text: forecastText, }, ], }; } ); server.tool( "get-minutely-precipitation", "Minute-level precipitation forecast API provides precise precipitation predictions for the next 2 hours in global cities. Available data includes precipitation type (rain/snow) and precipitation amount for each minute. This high-precision forecast is particularly useful for outdoor activity planning and real-time weather monitoring.", { cityName: z.string().describe("Name of the city to look up precipitation forecast for"), }, async ({ cityName }) => { // First, look up the city to get its location coordinates const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's coordinates const cityInfo = locationData.location[0]; const location = `${cityInfo.lon},${cityInfo.lat}`; const precipData = await makeQWeatherRequest<QWeatherMinutelyResponse>("/v7/minutely/5m", { location: location, }); if (!precipData || precipData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve precipitation forecast data", }, ], }; } const precipText = [ `Minutely Precipitation Forecast - ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Forecast Description: ${precipData.summary}`, `Last Updated: ${precipData.updateTime}`, '', '2-Hour Precipitation Forecast:', ...precipData.minutely.map(minute => `Time: ${minute.fxTime.split('T')[1].split('+')[0]} - ${minute.type === 'rain' ? 'Rain' : 'Snow'}: ${minute.precip}mm` ), '', `Data Source: ${precipData.fxLink}`, ].join('\n'); return { content: [ { type: "text", text: precipText, }, ], }; } ); server.tool( "get-hourly-forecast", "Hourly weather forecast API provides detailed weather information for global cities for the next 24-168 hours. Available data includes: temperature, weather conditions, wind force, wind speed, wind direction, relative humidity, atmospheric pressure, precipitation probability, dew point temperature, and cloud cover. The forecast data is updated hourly to ensure accuracy.", { cityName: z.string().describe("Name of the city to look up weather for"), hours: z.enum(["24h", "72h", "168h"]).default("24h").describe("Number of forecast hours (24h, 72h, or 168h)"), }, async ({ cityName, hours }) => { // First, look up the city to get its ID const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's ID const cityId = locationData.location[0].id; const cityInfo = locationData.location[0]; const hourlyData = await makeQWeatherRequest<QWeatherHourlyResponse>(`/v7/weather/${hours}`, { location: cityId, }); if (!hourlyData || hourlyData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve hourly forecast data", }, ], }; } const hourlyText = [ `${hours.replace('h', '-Hour')} Weather Forecast - ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Last Updated: ${hourlyData.updateTime}`, '', ...hourlyData.hourly.map(hour => [ `Time: ${hour.fxTime.split('T')[1].split('+')[0]}`, `Temperature: ${hour.temp}°C`, `Weather: ${hour.text}`, `Wind: ${hour.windDir} (Scale ${hour.windScale}, ${hour.windSpeed}km/h)`, `Humidity: ${hour.humidity}%`, `Precipitation: ${hour.precip}mm`, `Pressure: ${hour.pressure}hPa`, hour.cloud ? `Cloud Cover: ${hour.cloud}%` : null, hour.dew ? `Dew Point: ${hour.dew}°C` : null, '---' ].filter(Boolean).join('\n')) ].join('\n'); return { content: [ { type: "text", text: hourlyText, }, ], }; } ); server.tool( "get-weather-warning", "Weather Warning API provides real-time weather warning data issued by official authorities in China and multiple countries/regions worldwide. The data includes warning issuer, publication time, warning title, detailed warning information, warning level, warning type, and other relevant information.", { cityName: z.string().describe("Name of the city to look up weather warnings for"), }, async ({ cityName }) => { // First, look up the city to get its ID const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's ID const cityId = locationData.location[0].id; const cityInfo = locationData.location[0]; const warningData = await makeQWeatherRequest<QWeatherWarningResponse>("/v7/warning/now", { location: cityId, }); if (!warningData || warningData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve weather warning data", }, ], }; } if (!warningData.warning || warningData.warning.length === 0) { return { content: [ { type: "text", text: `No active weather warnings for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2})`, }, ], }; } const warningText = [ `Weather Warnings for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Last Updated: ${warningData.updateTime}`, '', ...warningData.warning.map(warning => [ `Warning Title: ${warning.title}`, `Issued By: ${warning.sender}`, `Publication Time: ${warning.pubTime}`, `Warning Type: ${warning.typeName}`, `Severity Level: ${warning.severity} (${warning.severityColor})`, `Valid Period: ${warning.startTime || 'Not specified'} to ${warning.endTime || 'Not specified'}`, `Status: ${warning.status}`, `Details: ${warning.text}`, '---' ].join('\n')) ].join('\n'); return { content: [ { type: "text", text: warningText, }, ], }; } ); server.tool( "get-weather-indices", "Weather indices forecast API provides various life indices for cities worldwide. Supports both 1-day and 3-day forecasts. Available indices types:\n\n" + "- Type 0: All indices types\n" + "- Type 1: Sport (Indicates suitability for outdoor sports activities)\n" + "- Type 2: Car Wash (Suggests whether it's suitable for washing cars)\n" + "- Type 3: Dressing (Provides clothing recommendations based on weather)\n" + "- Type 4: Fishing (Shows how favorable conditions are for fishing)\n" + "- Type 5: UV (Ultraviolet radiation intensity level)\n" + "- Type 6: Travel (Indicates suitability for traveling and sightseeing)\n" + "- Type 7: Allergy (Risk level for allergies and pollen)\n" + "- Type 8: Cold Risk (Risk level for catching a cold)\n" + "- Type 9: Comfort (Overall comfort level of the weather)\n" + "- Type 10: Wind (Wind conditions and their effects)\n" + "- Type 11: Sunglasses (Need for wearing sunglasses)\n" + "- Type 12: Makeup (How weather affects makeup wear)\n" + "- Type 13: Sunscreen (Need for sun protection)\n" + "- Type 14: Traffic (Weather impact on traffic conditions)\n" + "- Type 15: Sports Spectating (Suitability for watching outdoor sports)\n" + "- Type 16: Air Quality (Air pollution diffusion conditions)\n\n" + "Note: Not all indices are available for every city. International cities mainly support types 1, 2, 4, and 5.", { cityName: z.string().describe("Name of the city to look up weather indices for"), type: z.enum([ "0", // All Types "1", // Sport "2", // Car Wash "3", // Dressing "4", // Fishing "5", // UV "6", // Travel "7", // Allergy "8", // Cold Risk "9", // Comfort "10", // Wind "11", // Sunglasses "12", // Makeup "13", // Sunscreen "14", // Traffic "15", // Sports Spectating "16", // Air Quality ]).describe("Type of weather index to retrieve"), days: z.enum(["1d", "3d"]).default("1d").describe("Number of forecast days (1d or 3d)"), }, async ({ cityName, type, days }) => { // First, look up the city to get its ID const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's ID const cityId = locationData.location[0].id; const cityInfo = locationData.location[0]; const indicesData = await makeQWeatherRequest<QWeatherIndicesResponse>(`/v7/indices/${days}`, { location: cityId, type: type, }); if (!indicesData || indicesData.code !== "200") { return { content: [ { type: "text", text: "Failed to retrieve weather indices data", }, ], }; } const indicesText = [ `${days === "1d" ? "1-Day" : "3-Day"} Weather Indices for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, `Last Updated: ${indicesData.updateTime}`, '', ...indicesData.daily.map(index => [ `Date: ${index.date}`, `Index Type: ${index.name}`, `Level: ${index.level}`, `Category: ${index.category}`, `Suggestion: ${index.text}`, '---' ].join('\n')) ].join('\n'); return { content: [ { type: "text", text: indicesText, }, ], }; } ); server.tool( "get-air-quality", "Real-time Air Quality API provides air quality data for specific locations with 1x1 kilometer precision. Includes AQI based on local standards of different countries/regions, AQI levels, colors, primary pollutants, QWeather universal AQI, pollutant concentrations, sub-indices, health advice, and related monitoring station information.", { cityName: z.string().describe("Name of the city to look up air quality for"), }, async ({ cityName }) => { // First, look up the city to get its coordinates const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's coordinates const cityInfo = locationData.location[0]; // Format coordinates to have at most 2 decimal places const lat = Number(cityInfo.lat).toFixed(2); const lon = Number(cityInfo.lon).toFixed(2); // Update API endpoint to use path parameters (latitude first, then longitude) const airQualityEndpoint = `/airquality/v1/current/${lat}/${lon}`; const airQualityData = await makeQWeatherRequest<QWeatherAirQualityResponse>( airQualityEndpoint, {}, // No query parameters needed [lat, lon] // Path parameters in correct order: latitude, longitude ); if (!airQualityData || !airQualityData.indexes || airQualityData.indexes.length === 0) { return { content: [ { type: "text", text: "Failed to retrieve air quality data", }, ], }; } // Format output text const airQualityText = [ `Real-time Air Quality for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, '', 'Air Quality Indices:', ...airQualityData.indexes.map(index => [ `${index.name}: ${index.aqiDisplay}`, index.level ? `Level: ${index.level}` : null, index.category ? `Category: ${index.category}` : null, index.primaryPollutant ? `Primary Pollutant: ${index.primaryPollutant.fullName}` : null, index.health ? [ 'Health Effects:', `- ${index.health.effect}`, 'Health Advice:', `- General Population: ${index.health.advice.generalPopulation}`, `- Sensitive Population: ${index.health.advice.sensitivePopulation}`, ].join('\n') : null, '---' ].filter(Boolean).join('\n')), '', 'Pollutant Concentrations:', ...airQualityData.pollutants.map(pollutant => `${pollutant.fullName}: ${pollutant.concentration.value}${pollutant.concentration.unit}` ), '', airQualityData.stations ? [ 'Related Monitoring Stations:', ...airQualityData.stations.map(station => `- ${station.name}`), ].join('\n') : null, ].filter(Boolean).join('\n'); return { content: [ { type: "text", text: airQualityText, }, ], }; } ); server.tool( "get-air-quality-hourly", "Hourly Air Quality Forecast API provides air quality data for the next 24 hours, including AQI, pollutant concentrations, sub-indices, and health advice. The data includes various air quality standards (such as QAQI, GB-DEFRA, etc.) and specific concentrations of pollutants like PM2.5, PM10, NO2, O3, SO2.", { cityName: z.string().describe("Name of the city to look up air quality forecast for"), }, async ({ cityName }) => { // First, look up the city to get its coordinates const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's coordinates const cityInfo = locationData.location[0]; // Format coordinates to have at most 2 decimal places const lat = Number(cityInfo.lat).toFixed(2); const lon = Number(cityInfo.lon).toFixed(2); // Use path parameters to call the hourly air quality forecast API (latitude first, then longitude) const airQualityHourlyEndpoint = `/airquality/v1/hourly/${lat}/${lon}`; const airQualityData = await makeQWeatherRequest<QWeatherAirQualityHourlyResponse>( airQualityHourlyEndpoint, {}, // No query parameters needed [lat, lon] // Path parameters in order: latitude, longitude ); if (!airQualityData || !airQualityData.hours || airQualityData.hours.length === 0) { return { content: [ { type: "text", text: "Failed to retrieve air quality forecast data", }, ], }; } // Format output text const hourlyText = [ `24-Hour Air Quality Forecast for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, '', ...airQualityData.hours.map(hour => { const time = new Date(hour.forecastTime).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' }) + ' UTC'; const indexInfo = hour.indexes.map(index => { const healthInfo = index.health ? [ `Health Effects: ${translateHealthEffect(index.health.effect)}`, `Health Advice:`, ` General Population: ${translateAdvice(index.health.advice.generalPopulation)}`, ` Sensitive Population: ${translateAdvice(index.health.advice.sensitivePopulation)}` ].join('\n') : ''; return [ `Air Quality Indices:`, ` ${index.name}: ${index.aqiDisplay}`, ` Level: ${index.level || 'N/A'}`, ` Category: ${translateCategory(index.category || 'Unknown')}`, index.primaryPollutant ? ` Primary Pollutant: ${translatePollutant(index.primaryPollutant.fullName)}` : '', healthInfo ].filter(Boolean).join('\n'); }).join('\n'); const pollutantInfo = hour.pollutants.length > 0 ? [ 'Pollutant Concentrations:', ...hour.pollutants.map(pollutant => ` ${translatePollutant(pollutant.fullName)}: ${pollutant.concentration.value}${pollutant.concentration.unit}` ) ].join('\n') : 'No pollutant data available'; return [ `Forecast Time: ${time}`, indexInfo, pollutantInfo, '---' ].join('\n\n'); }).join('\n') ].join('\n'); return { content: [ { type: "text", text: hourlyText, }, ], }; } ); server.tool( "get-air-quality-daily", "Daily Air Quality Forecast API provides air quality predictions for the next 3 days, including AQI values, pollutant concentrations, and health recommendations. The data includes various air quality standards and specific concentrations of pollutants like PM2.5, PM10, NO2, O3, SO2.", { cityName: z.string().describe("Name of the city to look up air quality forecast for"), }, async ({ cityName }) => { // First, look up the city to get its coordinates const locationData = await makeQWeatherRequest<QWeatherLocationResponse>("/geo/v2/city/lookup", { location: cityName, }); if (!locationData || locationData.code !== "200") { return { content: [ { type: "text", text: "Failed to find the specified city", }, ], }; } if (!locationData.location || locationData.location.length === 0) { return { content: [ { type: "text", text: "No matching city found", }, ], }; } // Use the first matching city's coordinates const cityInfo = locationData.location[0]; // Format coordinates to have at most 2 decimal places const lat = Number(cityInfo.lat).toFixed(2); const lon = Number(cityInfo.lon).toFixed(2); // Use path parameters to call the daily air quality forecast API const airQualityDailyEndpoint = `/airquality/v1/daily/${lat}/${lon}`; const airQualityData = await makeQWeatherRequest<QWeatherAirQualityDailyResponse>( airQualityDailyEndpoint, {}, // No query parameters needed [lat, lon] // Path parameters in order: latitude, longitude ); if (!airQualityData || !airQualityData.days || airQualityData.days.length === 0) { return { content: [ { type: "text", text: "Failed to retrieve air quality forecast data", }, ], }; } // Format output text const dailyText = [ `3-Day Air Quality Forecast for ${cityInfo.name} (${cityInfo.adm1} ${cityInfo.adm2}):`, '', ...airQualityData.days.map(day => { const startTime = new Date(day.forecastStartTime).toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' }) + ' UTC'; const endTime = new Date(day.forecastEndTime).toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' }) + ' UTC'; const indexInfo = day.indexes.map(index => { const healthInfo = index.health ? [ `Health Effects: ${index.health.effect}`, `Health Advice:`, ` General Population: ${index.health.advice.generalPopulation}`, ` Sensitive Population: ${index.health.advice.sensitivePopulation}` ].join('\n') : ''; return [ `Air Quality Index:`, ` ${index.name}: ${index.aqiDisplay}`, ` Level: ${index.level || 'N/A'}`, ` Category: ${index.category || 'Unknown'}`, index.primaryPollutant ? ` Primary Pollutant: ${index.primaryPollutant.fullName}` : '', healthInfo ].filter(Boolean).join('\n'); }).join('\n\n'); const pollutantInfo = day.pollutants.length > 0 ? [ 'Pollutant Concentrations:', ...day.pollutants.map(pollutant => ` ${pollutant.fullName}: ${pollutant.concentration.value}${pollutant.concentration.unit}` ) ].join('\n') : 'No pollutant data available'; return [ `Forecast Period: ${startTime} to ${endTime}`, indexInfo, pollutantInfo, '---' ].join('\n\n'); }).join('\n') ].join('\n'); return { content: [ { type: "text", text: dailyText, }, ], }; } ); // Helper functions for translation function translateCategory(category: string): string { const categoryMap: Record<string, string> = { '优': 'Excellent', '良': 'Good', '轻度污染': 'Light Pollution', '中度污染': 'Moderate Pollution', '重度污染': 'Heavy Pollution', '严重污染': 'Severe Pollution' }; return categoryMap[category] || category; } function translatePollutant(pollutant: string): string { const pollutantMap: Record<string, string> = { '颗粒物(粒径小于等于2.5μm)': 'PM2.5', '颗粒物(粒径小于等于10μm)': 'PM10', '二氧化氮': 'NO2', '臭氧': 'O3', '二氧化硫': 'SO2', '一氧化碳': 'CO' }; return pollutantMap[pollutant] || pollutant; } function translateHealthEffect(effect: string): string { const effectMap: Record<string, string> = { '空气质量令人满意,基本无空气污染。': 'Air quality is satisfactory with minimal air pollution.', '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响。': 'Air quality is acceptable, but some pollutants may have a slight impact on the health of extremely sensitive individuals.', '易感人群症状有轻度加剧,健康人群出现刺激症状。': 'Sensitive individuals may experience mild symptom aggravation, while healthy individuals may experience irritation symptoms.', '进一步加剧易感人群症状,可能对健康人群心脏、呼吸系统有影响。': 'Further aggravation of symptoms in sensitive individuals, possible effects on the cardiovascular and respiratory systems of healthy individuals.', '心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状。': 'Significant aggravation of symptoms in patients with heart and lung conditions, reduced exercise tolerance, and general symptoms in healthy individuals.', '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病。': 'Reduced exercise tolerance in healthy individuals, obvious and severe symptoms, early onset of certain diseases.' }; return effectMap[effect] || effect; } function translateAdvice(advice: string): string { const adviceMap: Record<string, string> = { '各类人群可正常活动。': 'All groups can maintain normal activities.', '一般人群可正常活动。': 'General population can maintain normal activities.', '极少数异常敏感人群应减少户外活动。': 'Extremely sensitive individuals should reduce outdoor activities.', '儿童、老年人及心脏病、呼吸系统疾病患者应减少长时间、高强度的户外锻炼。': 'Children, elderly, and individuals with heart or respiratory conditions should reduce prolonged, high-intensity outdoor exercise.', '儿童、老年人及心脏病、呼吸系统疾病患者应停留在室内,停止户外运动,一般人群减少户外运动。': 'Children, elderly, and individuals with heart or respiratory conditions should stay indoors and avoid outdoor activities. General population should reduce outdoor activities.', '儿童、老年人和病人应停留在室内,避免体力消耗,一般人群避免户外活动。': 'Children, elderly, and patients should stay indoors and avoid physical exertion. General population should avoid outdoor activities.' }; return adviceMap[advice] || advice; } async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Weather MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

Latest Blog Posts

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/overstarry/qweather-mcp'

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