@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
JavaScript
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}¤t_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}¤t_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}¤t_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);