UNPKG

@mariox/weather-mcp-server

Version:

MCP server for querying weather by current IP location using Open-Meteo free API

627 lines (539 loc) 22.8 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; class WeatherMCPServer { constructor() { this.server = new Server( { name: 'weather-mcp-server', version: '1.1.1', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } setupToolHandlers() { // 列出可用工具 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_weather', description: '获取天气信息,不指定地点时查询当前IP位置天气,指定地点时查询该地点天气', inputSchema: { type: 'object', properties: { location: { type: 'string', description: '地点名称(可选),如"北京"、"上海"、"Tokyo"、"New York"等,不指定时查询当前IP位置', }, }, required: [], }, }, { name: 'get_weather_forecast', description: '获取天气预报(未来7天),不指定地点时查询当前IP位置预报,指定地点时查询该地点预报', inputSchema: { type: 'object', properties: { location: { type: 'string', description: '地点名称(可选),如"北京"、"上海"、"Tokyo"、"New York"等,不指定时查询当前IP位置', }, days: { type: 'integer', description: '预报天数(1-7天),默认3天', minimum: 1, maximum: 7, }, }, required: [], }, }, { name: 'get_current_ip_info', description: '获取当前IP地址的详细位置信息', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'get_weather_by_coordinates', description: '根据精确的经纬度坐标获取天气信息(高级用法)', inputSchema: { type: 'object', properties: { latitude: { type: 'number', description: '纬度', }, longitude: { type: 'number', description: '经度', }, }, required: ['latitude', 'longitude'], }, }, ], }; }); // 处理工具调用 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_weather': if (args.location) { return await this.getWeatherByLocation(args.location); } else { return await this.getCurrentIpWeather(); } case 'get_weather_forecast': if (args.location) { return await this.getWeatherForecastByLocation(args.location, args.days || 3); } else { return await this.getCurrentIpWeatherForecast(args.days || 3); } case 'get_weather_by_coordinates': return await this.getWeatherByCoordinates(args.latitude, args.longitude); case 'get_current_ip_info': return await this.getCurrentIpInfo(); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); } // 获取当前IP信息 async getCurrentIpInfo() { try { const response = await axios.get('http://ip-api.com/json/', { timeout: 5000, }); const data = response.data; if (data.status === 'fail') { throw new Error('无法获取IP信息'); } const ipInfo = { ip: data.query, country: data.country, countryCode: data.countryCode, region: data.region, regionName: data.regionName, city: data.city, lat: data.lat, lon: data.lon, timezone: data.timezone, isp: data.isp, }; return { content: [ { type: 'text', text: `🌍 **IP地址信息**\n\n🌐 IP地址: ${ipInfo.ip}\n🏳️ 国家: ${ipInfo.country} (${ipInfo.countryCode})\n🏙️ 地区: ${ipInfo.regionName}\n🏘️ 城市: ${ipInfo.city}\n📊 坐标: ${ipInfo.lat}, ${ipInfo.lon}\n🕐 时区: ${ipInfo.timezone}\n📡 ISP: ${ipInfo.isp}`, }, ], }; } catch (error) { throw new Error(`获取IP信息失败: ${error.message}`); } } // 地理编码:将地点名称转换为坐标 async geocodeLocation(location) { // 尝试多个地理编码服务 const services = [ { name: 'Nominatim', url: `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(location)}&limit=1`, headers: { 'User-Agent': 'Weather-MCP-Server/1.1.1' }, timeout: 8000 }, { name: 'OpenCage(备用)', url: `https://api.opencagedata.com/geocode/v1/json?q=${encodeURIComponent(location)}&key=no-key&limit=1`, headers: {}, timeout: 8000 } ]; for (const service of services) { try { console.error(`正在尝试地理编码服务: ${service.name}`); const response = await axios.get(service.url, { timeout: service.timeout, headers: service.headers }); const data = response.data; // 处理不同服务的响应格式 let result = null; if (service.name === 'Nominatim') { if (data && data.length > 0) { result = { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon), display_name: data[0].display_name, }; } } else if (service.name.includes('OpenCage')) { if (data && data.results && data.results.length > 0) { const geometry = data.results[0].geometry; result = { lat: geometry.lat, lon: geometry.lng, display_name: data.results[0].formatted, }; } } if (result) { console.error(`✅ ${service.name} 地理编码成功`); return result; } else { console.error(`❌ ${service.name} 未找到位置: ${location}`); } } catch (error) { console.error(`❌ ${service.name} 地理编码失败: ${error.message}`); // 继续尝试下一个服务 continue; } } // 所有服务都失败,尝试简单的城市名称映射 const cityMapping = { '北京': { lat: 39.9042, lon: 116.4074, display_name: '北京市, 中国' }, '上海': { lat: 31.2304, lon: 121.4737, display_name: '上海市, 中国' }, '广州': { lat: 23.1291, lon: 113.2644, display_name: '广州市, 中国' }, '深圳': { lat: 22.5431, lon: 114.0579, display_name: '深圳市, 中国' }, '杭州': { lat: 30.2741, lon: 120.1551, display_name: '杭州市, 中国' }, '成都': { lat: 30.5728, lon: 104.0668, display_name: '成都市, 中国' }, 'beijing': { lat: 39.9042, lon: 116.4074, display_name: 'Beijing, China' }, 'shanghai': { lat: 31.2304, lon: 121.4737, display_name: 'Shanghai, China' }, 'tokyo': { lat: 35.6762, lon: 139.6503, display_name: 'Tokyo, Japan' }, 'new york': { lat: 40.7128, lon: -74.0060, display_name: 'New York, USA' }, 'london': { lat: 51.5074, lon: -0.1278, display_name: 'London, UK' }, 'paris': { lat: 48.8566, lon: 2.3522, display_name: 'Paris, France' }, }; const normalizedLocation = location.toLowerCase().trim(); if (cityMapping[normalizedLocation]) { console.error(`✅ 使用内置城市映射: ${location}`); return cityMapping[normalizedLocation]; } throw new Error(`无法找到地点: ${location}。请尝试使用更常见的城市名称,如:北京、上海、Tokyo、New York 等,或者使用 get_weather_by_coordinates 工具通过精确坐标查询。`); } // 根据地点名称获取天气信息 async getWeatherByLocation(location) { try { // 1. 地理编码获取坐标 const geoInfo = await this.geocodeLocation(location); // 2. 根据坐标获取天气信息 const response = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${geoInfo.lat}&longitude=${geoInfo.lon}&current_weather=true&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,pressure_msl&timezone=auto`, { timeout: 10000 } ); const data = response.data; const weather = data.current_weather; const hourly = data.hourly; // 获取当前小时的湿度和气压 const currentHour = new Date().getHours(); const humidity = hourly.relative_humidity_2m[currentHour] || 'N/A'; const pressure = hourly.pressure_msl[currentHour] || 'N/A'; return { content: [ { type: 'text', text: `🌤️ **${location} 天气信息**\n\n📍 **位置信息:**\n🗺️ 地点: ${geoInfo.display_name}\n📊 坐标: ${geoInfo.lat}, ${geoInfo.lon}\n\n🌤️ **天气信息:**\n🌡️ 温度: ${weather.temperature}°C\n☁️ 天气: ${this.getWeatherDescription(weather.weathercode)}\n💨 风速: ${weather.windspeed} km/h\n🧭 风向: ${weather.winddirection}°\n💧 湿度: ${humidity !== 'N/A' ? humidity + '%' : 'N/A'}\n📊 气压: ${pressure !== 'N/A' ? pressure + ' hPa' : 'N/A'}\n🕐 时间: ${weather.time}\n\n📡 数据源: Open-Meteo (免费API)`, }, ], }; } catch (error) { throw new Error(`获取${location}天气信息失败: ${error.message}`); } } // 根据地点名称获取天气预报 async getWeatherForecastByLocation(location, days = 3) { try { // 1. 地理编码获取坐标 const geoInfo = await this.geocodeLocation(location); // 2. 获取天气预报 const response = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${geoInfo.lat}&longitude=${geoInfo.lon}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto&forecast_days=${days}`, { timeout: 10000 } ); const data = response.data; const daily = data.daily; let forecastText = `🌤️ **${location} ${days}天天气预报**\n\n📍 **位置信息:**\n🗺️ 地点: ${geoInfo.display_name}\n📊 坐标: ${geoInfo.lat}, ${geoInfo.lon}\n\n`; for (let i = 0; i < days; i++) { const date = daily.time[i]; const maxTemp = daily.temperature_2m_max[i]; const minTemp = daily.temperature_2m_min[i]; const weatherCode = daily.weathercode[i]; const precipitation = daily.precipitation_sum[i]; const dayName = i === 0 ? '今天' : i === 1 ? '明天' : `第${i+1}天`; forecastText += `📅 **${dayName}** (${date})\n`; forecastText += `🌡️ 温度: ${minTemp}°C - ${maxTemp}°C\n`; forecastText += `☁️ 天气: ${this.getWeatherDescription(weatherCode)}\n`; forecastText += `🌧️ 降水量: ${precipitation} mm\n\n`; } forecastText += `📡 数据源: Open-Meteo (免费API)`; return { content: [ { type: 'text', text: forecastText, }, ], }; } catch (error) { throw new Error(`获取${location}天气预报失败: ${error.message}`); } } // 获取当前IP位置的天气预报 async getCurrentIpWeatherForecast(days = 3) { try { // 1. 获取IP信息 const ipResponse = await axios.get('http://ip-api.com/json/', { timeout: 5000, }); const ipData = ipResponse.data; if (ipData.status === 'fail') { throw new Error('无法获取IP位置信息'); } // 2. 获取天气预报 const response = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${ipData.lat}&longitude=${ipData.lon}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto&forecast_days=${days}`, { timeout: 10000 } ); const data = response.data; const daily = data.daily; let forecastText = `🌤️ **当前位置 ${days}天天气预报**\n\n📍 **位置信息:**\n🌐 IP地址: ${ipData.query}\n🏳️ 国家: ${ipData.country}\n🏙️ 地区: ${ipData.regionName}\n🏘️ 城市: ${ipData.city}\n📊 坐标: ${ipData.lat}, ${ipData.lon}\n\n`; for (let i = 0; i < days; i++) { const date = daily.time[i]; const maxTemp = daily.temperature_2m_max[i]; const minTemp = daily.temperature_2m_min[i]; const weatherCode = daily.weathercode[i]; const precipitation = daily.precipitation_sum[i]; const dayName = i === 0 ? '今天' : i === 1 ? '明天' : `第${i+1}天`; forecastText += `📅 **${dayName}** (${date})\n`; forecastText += `🌡️ 温度: ${minTemp}°C - ${maxTemp}°C\n`; forecastText += `☁️ 天气: ${this.getWeatherDescription(weatherCode)}\n`; forecastText += `🌧️ 降水量: ${precipitation} mm\n\n`; } forecastText += `📡 数据源: Open-Meteo (免费API)`; return { content: [ { type: 'text', text: forecastText, }, ], }; } catch (error) { throw new Error(`获取当前位置天气预报失败: ${error.message}`); } } // 天气代码转换为中文描述 getWeatherDescription(code) { const weatherCodes = { 0: '晴朗', 1: '基本晴朗', 2: '部分多云', 3: '阴天', 45: '雾', 48: '雾凇', 51: '小雨', 53: '中雨', 55: '大雨', 56: '冻雨(轻)', 57: '冻雨(重)', 61: '小雨', 63: '中雨', 65: '大雨', 66: '冻雨', 67: '冻雨(重)', 71: '小雪', 73: '中雪', 75: '大雪', 77: '雪粒', 80: '阵雨(轻)', 81: '阵雨(中)', 82: '阵雨(重)', 85: '阵雪(轻)', 86: '阵雪(重)', 95: '雷雨', 96: '雷雨伴冰雹(轻)', 99: '雷雨伴冰雹(重)', }; return weatherCodes[code] || `天气代码: ${code}`; } // 根据经纬度获取天气信息(使用Open-Meteo) async getWeatherByCoordinates(lat, lon) { try { const response = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,pressure_msl&timezone=auto`, { timeout: 10000 } ); const data = response.data; const weather = data.current_weather; const hourly = data.hourly; // 获取当前小时的湿度和气压 const currentHour = new Date().getHours(); const humidity = hourly.relative_humidity_2m[currentHour] || 'N/A'; const pressure = hourly.pressure_msl[currentHour] || 'N/A'; const result = { temperature: `${weather.temperature}°C`, weather_description: this.getWeatherDescription(weather.weathercode), wind_speed: `${weather.windspeed} km/h`, wind_direction: `${weather.winddirection}°`, humidity: humidity !== 'N/A' ? `${humidity}%` : 'N/A', pressure: pressure !== 'N/A' ? `${pressure} hPa` : 'N/A', coordinates: `${lat}, ${lon}`, data_source: 'Open-Meteo (免费API)', time: weather.time, }; return { content: [ { type: 'text', text: `🌤️ **天气信息**\n\n📍 坐标: ${result.coordinates}\n🌡️ 温度: ${result.temperature}\n☁️ 天气: ${result.weather_description}\n💨 风速: ${result.wind_speed}\n🧭 风向: ${result.wind_direction}\n💧 湿度: ${result.humidity}\n📊 气压: ${result.pressure}\n🕐 时间: ${result.time}\n📡 数据源: ${result.data_source}`, }, ], }; } catch (error) { throw new Error(`获取天气信息失败: ${error.message}`); } } // 获取天气预报 async getWeatherForecast(lat, lon, days = 3) { try { const response = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto&forecast_days=${days}`, { timeout: 10000 } ); const data = response.data; const daily = data.daily; let forecastText = `🌤️ **${days}天天气预报**\n\n📍 坐标: ${lat}, ${lon}\n\n`; for (let i = 0; i < days; i++) { const date = daily.time[i]; const maxTemp = daily.temperature_2m_max[i]; const minTemp = daily.temperature_2m_min[i]; const weatherCode = daily.weathercode[i]; const precipitation = daily.precipitation_sum[i]; const dayName = i === 0 ? '今天' : i === 1 ? '明天' : `第${i+1}天`; forecastText += `📅 **${dayName}** (${date})\n`; forecastText += `🌡️ 温度: ${minTemp}°C - ${maxTemp}°C\n`; forecastText += `☁️ 天气: ${this.getWeatherDescription(weatherCode)}\n`; forecastText += `🌧️ 降水量: ${precipitation} mm\n\n`; } forecastText += `📡 数据源: Open-Meteo (免费API)`; return { content: [ { type: 'text', text: forecastText, }, ], }; } catch (error) { throw new Error(`获取天气预报失败: ${error.message}`); } } // 获取当前IP的天气信息 async getCurrentIpWeather() { try { console.error('🌍 开始获取当前IP位置天气信息...'); // 1. 获取IP信息 console.error('📡 正在获取IP地理位置...'); const ipResponse = await axios.get('http://ip-api.com/json/', { timeout: 8000, }); const ipData = ipResponse.data; if (ipData.status === 'fail') { throw new Error('无法获取IP位置信息:' + (ipData.message || '未知错误')); } console.error(`✅ IP位置获取成功: ${ipData.city}, ${ipData.country} (${ipData.lat}, ${ipData.lon})`); // 2. 根据经纬度获取天气信息 console.error('🌤️ 正在获取天气数据...'); const weatherResponse = await axios.get( `https://api.open-meteo.com/v1/forecast?latitude=${ipData.lat}&longitude=${ipData.lon}&current_weather=true&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,pressure_msl&timezone=auto`, { timeout: 15000 } ); const weatherData = weatherResponse.data; if (!weatherData || !weatherData.current_weather) { throw new Error('天气API返回的数据格式无效'); } const weather = weatherData.current_weather; const hourly = weatherData.hourly; console.error('✅ 天气数据获取成功'); // 获取当前小时的湿度和气压 const currentHour = new Date().getHours(); const humidity = (hourly && hourly.relative_humidity_2m) ? (hourly.relative_humidity_2m[currentHour] || 'N/A') : 'N/A'; const pressure = (hourly && hourly.pressure_msl) ? (hourly.pressure_msl[currentHour] || 'N/A') : 'N/A'; return { content: [ { type: 'text', text: `🌍 **当前IP位置天气信息**\n\n📍 **位置信息:**\n🌐 IP地址: ${ipData.query}\n🏳️ 国家: ${ipData.country}\n🏙️ 地区: ${ipData.regionName}\n🏘️ 城市: ${ipData.city}\n📊 坐标: ${ipData.lat}, ${ipData.lon}\n\n🌤️ **天气信息:**\n🌡️ 温度: ${weather.temperature}°C\n☁️ 天气: ${this.getWeatherDescription(weather.weathercode)}\n💨 风速: ${weather.windspeed} km/h\n🧭 风向: ${weather.winddirection}°\n💧 湿度: ${humidity !== 'N/A' ? humidity + '%' : 'N/A'}\n📊 气压: ${pressure !== 'N/A' ? pressure + ' hPa' : 'N/A'}\n🕐 时间: ${weather.time}\n\n📡 数据源: Open-Meteo (免费API)`, }, ], }; } catch (error) { console.error('❌ 获取当前IP天气信息失败:', error.message); // 提供更友好的错误信息 let errorMessage = '获取当前IP天气信息失败: '; if (error.message.includes('timeout')) { errorMessage += '网络连接超时,请检查网络连接后重试'; } else if (error.message.includes('ENOTFOUND') || error.message.includes('Network Error')) { errorMessage += '网络连接错误,请检查网络连接'; } else if (error.response && error.response.status) { errorMessage += `服务器错误 (${error.response.status}): ${error.response.statusText}`; } else { errorMessage += error.message; } throw new Error(errorMessage); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Weather MCP Server 已启动'); } } const server = new WeatherMCPServer(); // 处理命令行参数 const args = process.argv.slice(2); if (args.includes('--version') || args.includes('-v')) { console.log('1.1.1'); process.exit(0); } if (args.includes('--help') || args.includes('-h')) { console.log(`Weather MCP Server v1.1.1 用法: weather-mcp-server [选项] 选项: -v, --version 显示版本号 -h, --help 显示帮助信息 这是一个MCP服务器,提供基于IP地址的天气查询功能。 更多信息请访问: https://github.com/mariox/weather-mcp-server`); process.exit(0); } server.run().catch(console.error);