""" FastMCP quickstart example. cd to the `examples/snippets/clients` directory and run: uv run server fastmcp_quickstart stdio """ import os from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv, find_dotenv import requests from typing import Dict, Optional, Union _ = load_dotenv(find_dotenv()) # Create an MCP server mcp = FastMCP("Demo") # Add an addition tool @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers""" return a + b @mcp.tool() def get_weather_by_location(user_id: str, user_key: str, province: str, place: str) -> Dict[ str, Union[str, float, int] ]: """ 调用中国气象局天气预报API,获取指定省份和地点的完整当日天气信息(含基础预报、实时数据、气象预警) ### 参数说明 - user_id: str - 接口调用身份标识,需从http://www.apihz.cn注册获取,不可为空(例:"10007673") - user_key: str - 接口通讯秘钥,与user_id对应,注册后获取,不可为空(例:"be63d2fb5354f76abb18f62583edffae") - province: str - 查询省份/直辖市,建议去除"省""市"后缀(例:"四川"而非"四川省","北京"而非"北京市") - place: str - 查询城市/区/县,建议去除"市""区""县"后缀(例:"绵阳"而非"绵阳市","大兴"而非"大兴区") ### 返回说明(成功,code=200) 基础预报信息: - code: int - 状态码,成功固定为200 - guo: str - 国家名称(例:"中国") - sheng: str - 标准化省份/直辖市名称(例:"四川") - shi: str - 标准化城市/地区名称(例:"绵阳") - name: str - 与shi一致,冗余字段(例:"绵阳") - weather1: str - 当日主要天气(例:"阵雨") - weather2: str - 当日次要天气(例:"阵雨") - wd1: str - 当日最高温度(单位:℃,例:"25") - wd2: str - 当日最低温度(单位:℃,例:"18") - winddirection1: str - 当日主要风向(例:"无持续风向") - winddirection2: str - 当日次要风向(例:"无持续风向") - windleve1: str - 当日主要风力等级(例:"微风") - windleve2: str - 当日次要风力等级(例:"微风") - weather1img: str - 主要天气图标URL(例:"https://rescdn.apihz.cn/resimg/tianqi/zhenyu.png") - weather2img: str - 次要天气图标URL(例:"https://rescdn.apihz.cn/resimg/tianqi/zhenyu.png") - lon: str - 地区经度(保留3位小数,例:"104.730") - lat: str - 地区纬度(保留3位小数,例:"31.440") - uptime: str - 预报数据更新时间(格式:YYYY-MM-DD HH:MM:SS,例:"2025-08-29 12:00:00") 实时天气数据(顶层字段): - now_precipitation: float - 当前降水量(单位:mm,例:0.0) - now_temperature: float - 当前温度(单位:℃,例:19.3) - now_pressure: int - 当前气压(单位:hPa,例:956) - now_humidity: int - 当前湿度(单位:%,例:85) - now_windDirection: str - 当前风向(例:"东北风") - now_windDirectionDegree: int - 当前风向角度(0°=北风,例:28) - now_windSpeed: float - 当前风速(单位:m/s,例:3.2) - now_windScale: str - 当前风力等级(例:"微风") - now_feelst: float - 当前体感温度(单位:℃,例:19.7) - now_uptime: str - 实时数据更新时间(格式:YYYY/MM/DD HH:MM,例:"2025/08/29 10:05") 气象预警(顶层字段,无预警时为空字符串): - alarm_id: str - 预警唯一ID(例:"51070041600000_20250828215515") - alarm_title: str - 预警标题(例:"绵阳市气象台更新大风蓝色预警信号[IV级/一般]") - alarm_signaltype: str - 预警类型(例:"大风") - alarm_signallevel: str - 预警等级(例:"蓝色") - alarm_effective: str - 预警生效时间(例:"2025/08/28 21:55") - alarm_eventType: str - 预警事件编码(例:"11B06") - alarm_severity: str - 预警等级英文编码(例:"BLUE") - alarm_type: str - 预警类型编码(例:"p0007004") ### 返回说明(失败,code=400) - code: int - 错误状态码,固定为400 - msg: str - 错误详情(例:"通讯秘钥错误。"、"API响应解析失败") """ api_url = "https://cn.apihz.cn/api/tianqi/tqyb.php" clean_province = province.replace("省", "").replace("市", "").strip() clean_place = place.replace("市", "").replace("区", "").replace("县", "").strip() final_user_id = os.getenv("APIHZ_ID", user_id) final_user_key = os.getenv("APIHZ_KEY", user_key) params = { "id": final_user_id, "key": final_user_key, "sheng": clean_province, "place": clean_place } try: # 1. 发送请求(增加超时重试,避免偶发网络波动) response = requests.get(api_url, params=params, timeout=10) response.raise_for_status() # 捕获4xx/5xx HTTP错误 # 2. 解析响应:先判断响应内容是否为空,再转JSON response_text = response.text.strip() if not response_text: return {"code": 400, "msg": "API响应为空,无法解析天气数据"} # 3. 转JSON并防御None(确保weather_data是字典) weather_data = response.json() if not isinstance(weather_data, dict): return {"code": 400, "msg": f"API响应格式错误,不是有效字典(实际类型:{type(weather_data).__name__})"} # 4. 处理API返回的错误状态(code=400) if weather_data.get("code") == 400: return { "code": 400, "msg": weather_data.get("msg", "API返回错误,原因未知") } # 5. 确保API返回成功状态(code=200) elif weather_data.get("code") != 200: return { "code": 400, "msg": f"API返回非成功状态码:{weather_data.get('code', '未知')}" } # 6. 提取嵌套数据(确保now_info/alarm_info是字典,避免None) now_info = weather_data.get("nowinfo", {}) if not isinstance(now_info, dict): now_info = {} # 若now_info不是字典,强制设为空字典 alarm_info = weather_data.get("alarm", {}) if not isinstance(alarm_info, dict): alarm_info = {} # 若alarm不是字典,强制设为空字典 # 7. 构造最终返回结果(全部顶层字段,无嵌套) return { # 基础预报字段 "code": 200, "guo": weather_data.get("guo", ""), "sheng": weather_data.get("sheng", ""), "shi": weather_data.get("shi", ""), "name": weather_data.get("name", ""), "weather1": weather_data.get("weather1", ""), "weather2": weather_data.get("weather2", ""), "wd1": weather_data.get("wd1", ""), "wd2": weather_data.get("wd2", ""), "winddirection1": weather_data.get("winddirection1", ""), "winddirection2": weather_data.get("winddirection2", ""), "windleve1": weather_data.get("windleve1", ""), "windleve2": weather_data.get("windleve2", ""), "weather1img": weather_data.get("weather1img", ""), "weather2img": weather_data.get("weather2img", ""), "lon": weather_data.get("lon", ""), "lat": weather_data.get("lat", ""), "uptime": weather_data.get("uptime", ""), # 实时天气字段 "now_precipitation": float(now_info.get("precipitation", 0.0)), "now_temperature": float(now_info.get("temperature", 0.0)), "now_pressure": int(now_info.get("pressure", 0)), "now_humidity": int(now_info.get("humidity", 0)), "now_windDirection": now_info.get("windDirection", ""), "now_windDirectionDegree": int(now_info.get("windDirectionDegree", 0)), "now_windSpeed": float(now_info.get("windSpeed", 0.0)), "now_windScale": now_info.get("windScale", ""), "now_feelst": float(now_info.get("feelst", 0.0)), "now_uptime": now_info.get("uptime", ""), # 预警字段 "alarm_id": alarm_info.get("id", ""), "alarm_title": alarm_info.get("title", ""), "alarm_signaltype": alarm_info.get("signaltype", ""), "alarm_signallevel": alarm_info.get("signallevel", ""), "alarm_effective": alarm_info.get("effective", ""), "alarm_eventType": alarm_info.get("eventType", ""), "alarm_severity": alarm_info.get("severity", ""), "alarm_type": alarm_info.get("type", "") } # 8. 捕获各类异常(明确错误原因) except requests.exceptions.HTTPError as e: status_code = response.status_code if "response" in locals() else "未知" return {"code": 400, "msg": f"HTTP请求错误(状态码:{status_code}):{str(e)}"} except requests.exceptions.ConnectionError: return {"code": 400, "msg": "网络连接错误:无法连接到天气API服务器"} except requests.exceptions.Timeout: return {"code": 400, "msg": "请求超时:API服务器10秒内未响应"} except ValueError as e: # 捕获JSON解析错误(如响应不是合法JSON) return {"code": 400, "msg": f"API响应解析失败(JSON格式错误):{str(e)}"} except Exception as e: # 捕获其他未知错误(附带具体错误信息,便于调试) return {"code": 400, "msg": f"未知错误:{str(e)}(错误类型:{type(e).__name__})"} # Add a dynamic greeting resource @mcp.resource("greeting://{name}") def get_greeting(name: str) -> str: """Get a personalized greeting""" return f"Hello, {name}!" # Add a prompt @mcp.prompt() def greet_user(name: str, style: str = "friendly") -> str: """Generate a greeting prompt""" styles = { "friendly": "Please write a warm, friendly greeting", "formal": "Please write a formal, professional greeting", "casual": "Please write a casual, relaxed greeting", } return f"{styles.get(style, styles['friendly'])} for someone named {name}." if __name__ == "__main__": mcp.run(transport="sse")