mcp_test/main.py
sun 842e8f4f72 feat(client): 重构客户端以支持 OpenAI 兼容模型并增强日志功能
- 新增 OpenAIClientModel 类,用于调用 OpenAI 兼容模型
- 重构 AutoToolChatSession 类,支持 OpenAI 兼容模型- 增加了更多日志输出,以便调试和跟踪程序执行流程
- 优化了工具调用和结果处理的逻辑- 调整了环境变量加载方式,使用 dotenv 库
2025-08-29 14:12:15 +08:00

210 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 - 当前降水量单位mm0.0
- now_temperature: float - 当前温度单位19.3
- now_pressure: int - 当前气压单位hPa956
- now_humidity: int - 当前湿度(单位:%85
- now_windDirection: str - 当前风向(例:"东北风"
- now_windDirectionDegree: int - 当前风向角度0°=北风28
- now_windSpeed: float - 当前风速单位m/s3.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")