Skip to main content
Glama

HeFeng Weather MCP Server

by ctermiii
index.ts13.7 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 { z } from "zod"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; // 获取当前文件路径和目录 const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")); // wttr.in API 的基础 URL // wttr.in API base URL const WTTR_IN_API_URL = "https://wttr.in"; // 缓存配置 const cache = new Map<string, { data: any; expiry: number }>(); const CACHE_TTL = 10 * 60 * 1000; // 10分钟 // --------------------------------------------------------------------------------------------------------------------- // 工具函数 / Utility Functions // --------------------------------------------------------------------------------------------------------------------- /** * 日志工具 * Logger utility */ const logger = { info: (message: string, ...args: any[]) => console.error(`[INFO] ${message}`, ...args), error: (message: string, ...args: any[]) => console.error(`[ERROR] ${message}`, ...args), warn: (message: string, ...args: any[]) => console.error(`[WARN] ${message}`, ...args), }; /** * 带超时的 fetch 请求 * Fetch with timeout */ async function fetchWithTimeout(url: string, timeout = 5000): Promise<Response> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if ((error as Error).name === 'AbortError') { throw new Error(`请求超时: ${url}`); } throw error; } } /** * 获取缓存数据 * Get cached data */ function getCachedData(key: string): any | null { const cached = cache.get(key); if (cached && cached.expiry > Date.now()) { return cached.data; } cache.delete(key); return null; } /** * 设置缓存数据 * Set cached data */ function setCachedData(key: string, data: any): void { cache.set(key, { data, expiry: Date.now() + CACHE_TTL }); } /** * 格式化实时天气数据 * Format current weather data */ function formatCurrentWeather(current: WttrInCurrentCondition, area: string): string { return `地点: ${area}\n` + `当前天气:\n` + ` 天气描述: ${current.weatherDesc[0].value}\n` + ` 温度: ${current.temp_C}°C (体感温度: ${current.FeelsLikeC}°C)\n` + ` 风速: ${current.windspeedKmph} km/h, 风向: ${current.winddir16Point}\n` + ` 湿度: ${current.humidity}%, 气压: ${current.pressure}hPa\n` + ` 紫外线指数: ${current.uvIndex}`; } /** * 格式化三天天气预报数据 * Format 3-day weather forecast data */ function formatForecast(weather: WttrInDayForecast[], area: string): string { let text = `地点: ${area}\n三天天气预报:\n`; weather.slice(0, 3).forEach(day => { text += `------------------------\n` + `日期: ${day.date} (日出: ${day.sun_rise}, 日落: ${day.sun_set})\n` + ` 最高温: ${day.maxtempC}°C, 最低温: ${day.mintempC}°C\n` + ` 平均温度: ${day.avgtempC}°C\n` + ` 天气: ${day.hourly[0].weatherDesc[0].value}\n` + ` 降水概率: ${day.hourly[0].chanceofrain}%\n`; }); return text + `------------------------`; } // --------------------------------------------------------------------------------------------------------------------- // 定义 wttr.in API 响应体的 TypeScript 接口 // Define TypeScript interfaces for wttr.in API response body // --------------------------------------------------------------------------------------------------------------------- interface WttrInFeelsLike { value: string; } interface WttrInCurrentCondition { temp_C: string; weatherDesc: [{ value: string }]; windspeedKmph: string; winddir16Point: string; humidity: string; pressure: string; uvIndex: string; FeelsLikeC: string; } interface WttrInDayForecast { date: string; maxtempC: string; mintempC: string; avgtempC: string; sun_rise: string; sun_set: string; moon_rise: string; moon_set: string; hourly: Array<{ time: string; tempC: string; weatherDesc: [{ value: string }]; chanceofrain: string; }>; } interface WttrInResponse { current_condition?: WttrInCurrentCondition[]; weather?: WttrInDayForecast[]; nearest_area?: Array<{ areaName: [{ value: string }]; country: [{ value: string }]; region: [{ value: string }]; }>; } // --------------------------------------------------------------------------------------------------------------------- // 集中定义工具的 Schema 和元数据 // Centralized definition of tool schemas and metadata // --------------------------------------------------------------------------------------------------------------------- // 定义 wttr.in 天气查询参数的 Zod schema const getWeatherSchema = z.object({ location: z.string().describe("需要查询的城市名称、邮政编码或经纬度坐标。例如: 'beijing', '90210', '40.71,-74.00'。支持中文城市名称,如'北京'、'上海'等。"), days: z.enum(['now', '3d']).default('3d').describe("预报类型。必须是以下值之一:'now' 表示获取实时天气,'3d' 表示获取未来三天预报。默认值为 '3d'。注意:请严格使用 'now' 或 '3d',不要使用数字 '3'。"), }); // 定义获取日期时间工具的参数 Zod schema const getDatetimeSchema = z.object({ timezone: z.string().optional().describe("可选的时区,例如 'America/New_York', 'Europe/London'。如果未提供,默认为 'Asia/Shanghai'。"), }); // 统一管理所有工具的定义 const tools = { get_weather_wttr: { description: "获取指定地点的天气预报。支持城市名称、邮政编码或经纬度坐标作为输入。可以获取实时天气(days='now')或未来三天预报(days='3d')。注意:days参数必须严格使用 'now' 或 '3d' 字符串,不要使用数字。", schema: getWeatherSchema, }, get_datetime: { description: "获取当前的日期和时间。可以提供一个可选的时区参数(如 'America/New_York', 'Europe/London'),默认为 'Asia/Shanghai'。", schema: getDatetimeSchema, }, }; /** * 辅助函数:从 Zod schema 生成 OpenAPI 格式的 inputSchema * Helper function: Generate OpenAPI format inputSchema from Zod schema */ function createInputSchemaFromZod(schema: z.AnyZodObject) { const properties: { [key: string]: any } = {}; const required: string[] = []; for (const key in schema.shape) { let zodType = schema.shape[key]; const originalType = zodType; // 处理可选和默认值包装 const isOptional = zodType.isOptional(); if (zodType._def.typeName === 'ZodOptional' || zodType._def.typeName === 'ZodDefault') { zodType = zodType._def.innerType || zodType; } const property: any = { type: 'string', description: zodType.description, }; // 处理枚举类型 if (zodType._def.typeName === 'ZodEnum') { property.enum = zodType._def.values; } // 正确处理 ZodDefault 的默认值 if (originalType._def.typeName === 'ZodDefault') { const defaultValue = originalType._def.defaultValue; property.default = typeof defaultValue === 'function' ? defaultValue() : defaultValue; } properties[key] = property; // 只有非可选且非默认值的字段才是必填的 if (!isOptional && originalType._def.typeName !== 'ZodOptional') { required.push(key); } } return { type: "object", properties, required, }; } // --------------------------------------------------------------------------------------------------------------------- // Server Setup // --------------------------------------------------------------------------------------------------------------------- // 创建服务器实例 // Create server instance const server = new Server( { name: packageJson.name, version: packageJson.version, }, { capabilities: { tools: {}, }, } ); // 列出可用工具 // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { // 动态生成工具列表 const toolList = Object.entries(tools).map(([name, tool]) => ({ name: name, description: tool.description, inputSchema: createInputSchemaFromZod(tool.schema), })); return { tools: toolList }; }); // 处理工具执行 // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === "get_weather_wttr") { const { location, days } = getWeatherSchema.parse(args); // 检查缓存 const cacheKey = `weather:${location}:${days}`; const cachedData = getCachedData(cacheKey); if (cachedData) { logger.info(`返回缓存的天气数据: ${location} (${days})`); return { content: [{ type: "text", text: cachedData }] }; } // 构建 wttr.in 的 API URL,请求 JSON 格式 const fullUrl = `${WTTR_IN_API_URL}/${encodeURIComponent(location)}?format=j1`; logger.info(`Fetching weather from wttr.in: ${fullUrl}`); const response = await fetchWithTimeout(fullUrl); if (!response.ok) { return { content: [{ type: "text", text: `从 wttr.in 获取天气数据失败,状态码: ${response.status}` }], }; } const data: WttrInResponse = await response.json(); if (!data.current_condition) { return { content: [{ type: "text", text: `未找到 ${location} 的天气数据,请检查输入是否正确。` }], }; } const area = data.nearest_area?.[0]?.areaName?.[0]?.value || location; let weatherText: string; // 处理实时天气 if (days === 'now') { const current = data.current_condition[0]; weatherText = formatCurrentWeather(current, area); } // 处理三天预报 else if (days === '3d' && data.weather) { weatherText = formatForecast(data.weather, area); } else { weatherText = `无法获取 ${location} 的天气预报数据`; } // 缓存结果 setCachedData(cacheKey, weatherText); return { content: [{ type: "text", text: weatherText }] }; } else if (name === "get_datetime") { const { timezone } = getDatetimeSchema.parse(args); const currentDateTime = new Date(); let dateTimeString: string; const targetTimezone = timezone || 'Asia/Shanghai'; // Default to Asia/Shanghai try { // Use toLocaleString with options to get time in the target timezone dateTimeString = currentDateTime.toLocaleString('zh-CN', { timeZone: targetTimezone }); } catch (error) { // Fallback to a known good default if specific timezone fails logger.warn(`无效的时区 "${targetTimezone}" 或格式化错误:`, error); const fallbackTimezone = 'Asia/Shanghai'; dateTimeString = currentDateTime.toLocaleString('zh-CN', { timeZone: fallbackTimezone }); dateTimeString += ` (无法识别提供的时区 "${timezone}", 已回退到 ${fallbackTimezone})`; } return { content: [{ type: "text", text: `当前日期时间 (${targetTimezone}): ${dateTimeString}` }], }; } else { throw new Error(`Unknown tool: ${name}`); } } catch (error) { if (error instanceof z.ZodError) { logger.error("参数校验错误:", error.errors); const errorMessages = error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join("; "); return { content: [{ type: "text", text: `输入参数无效: ${errorMessages}` }], }; } logger.error(`工具 "${name}" 执行出错:`, error); return { content: [{ type: "text", text: `执行工具 "${name}" 时发生内部错误: ${error instanceof Error ? error.message : String(error)}` }], }; } }); // 启动服务器 // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); logger.info("wttr-mcp-server MCP Server running on stdio. Waiting for requests..."); } main().catch((error) => { logger.error("主程序发生严重错误:", 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/ctermiii/hefeng-mcp-server'

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