MCP协议
MCP(Model Context Protocol),作为 Anthropic 推出的标准化接口协议,为 AI 模型与外部工具的交互带来了前所未有的便捷。想象一下,AI 模型是一个超级大脑,而 MCP 就是连接这个大脑与各种工具的神经系统,它定义了一套规范,使得 AI 模型能够轻松调用如天气查询、代码执行等外部功能模块。本篇以vs code下cline插件为例,实现一个基础的MCP天气查询服务。
前置步骤
- uv 依赖管理工具安装:uv 是本次开发中不可或缺的依赖管理工具,它负责虚拟环境创建、依赖解析及项目初始化等全流程管理,避免了传统方式的繁琐配置。我们可以使用官方提供的脚本快速安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh
安装完成后,我们可以通过uv --version命令来验证是否安装成功,如果成功安装,会显示 uv 的版本号。
- cline模型配置:这里以免费的硅基流动deepseek-v3接口为例
export SENIVERSE_API_KEY='你的API_KEY'
这样,在我们的项目中就可以通过os.getenv("SENIVERSE_API_KEY")来获取这个密钥。
项目初始化与基础架构搭建
- 创建项目与虚拟环境
# 创建项目目录
uv init weather
cd weather
# 创建虚拟环境并激活
uv venv
source .venv/bin/activate
激活虚拟环境后,就进入了一个与系统环境隔离的 Python 运行环境,避免了不同项目之间的依赖冲突。
- 核心依赖安装
uv add "mcp[cli]" httpx
mcp[cli]包含了 FastMCP 服务器的核心组件,是实现 MCP 协议的关键。而httpx则用于异步 HTTP 请求,相比传统的 HTTP 请求库,它在性能上有显著提升
实现天气服务
服务初始化
新建weather.py文件,构建 MCP 服务器的基础结构。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("domestic_weather")
这里创建的mcp对象是整个服务器的核心,它负责管理服务器的各种功能和工具。FastMCP 通过 Python 的类型提示和自动文档生成功能,可以极大简化工具函数的注册与调用过程。
心知天气API通信层构建
请求封装
async def make_domestic_request(
url: str, params: dict[str, Any]
) -> Optional[dict]:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
base_params = {"key": API_KEY, "language": LANGUAGE, "unit": UNIT}
base_params.update(params)
async with httpx.AsyncClient() as client:
try:
response = await client.get(
url, headers=headers, params=base_params, timeout=30.0
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"[WeatherAPI] 请求失败: {str(e)}")
return None
使用httpx库进行异步 HTTP 请求,并且支持 30 秒的超时控制。在请求过程中,它会自动处理各种异常情况,如网络连接失败、HTTP 状态码错误等,并返回相应的错误信息。
数据格式化
def format_realtime_weather(data: dict) -> str:
"""格式化实时天气数据"""
try:
result = data.get("results", [{}])[0]
now = result.get("now", {})
location = result.get("location", {})
return (
f"实时天气 - {location.get('name', '未知')}\n"
f"温度: {now.get('temperature', '未知')}°C\n"
f"天气状况: {now.get('text', '未知')}\n"
f"更新时间: {result.get('last_update', '未知')}\n"
)
except Exception as e:
return f"格式化实时天气数据出错: {e}"
def format_forecast(data: dict) -> str:
"""格式化未来3天天气预报"""
try:
periods = data.get("results", [{}])[0].get("daily", [])
forecast_list = []
for period in periods[:3]:
forecast_list.append(
f"日期: {period.get('date', '未知')}\n"
f"白天: {period.get('text_day', '未知')},温度 {period.get('high', '未知')}°C\n"
f"夜间: {period.get('text_night', '未知')},温度 {period.get('low', '未知')}°C\n"
f"风速: {period.get('wind_speed', '未知')} km/h,风向: {period.get('wind_direction', '未知')}\n"
f"湿度: {period.get('humidity', '未知')}%\n"
f"降水量: {period.get('rainfall', '未知')} mm\n"
)
return "\n---\n".join(forecast_list) if forecast_list else "无天气预报数据"
except Exception as e:
return f"格式化天气预报数据出错: {e}"
这两个函数负责处理心知天气 API 返回的嵌套 JSON 结构数据,将其转换为人类可读的格式。在处理过程中,它们会统一处理可能出现的错误情况,如数据结构异常、键值缺失等,确保返回的格式化数据准确可靠。
MCP 工具函数注册
@mcp.tool()
async def get_realtime_weather(city: str) -> str:
"""获取国内城市实时天气"""
url = f"{DOMESTIC_API_BASE}/now.json"
params = {"location": city}
data = await make_domestic_request(url, params)
if not data or not data.get("results"):
error_code = data.get("status_code", "未知错误码") if data else "无返回"
error_message = data.get("status_message", "未知错误信息") if data else "无返回"
return f"未能获取到该城市的实时信息 (错误码: {error_code}, 错误信息: {error_message})"
return format_realtime_weather(data)
@mcp.tool()
async def get_three_day_forecast(city: str) -> str:
"""获取国内城市未来3天天气预报"""
url = f"{DOMESTIC_API_BASE}/daily.json"
params = {"location": city, "days": 3}
data = await make_domestic_request(url, params)
if not data or not data.get("results"):
error_code = data.get("status_code", "未知错误码") if data else "无返回"
error_message = data.get("status_message", "未知错误信息") if data else "无返回"
return f"未能获取到该城市的天气预报信息 (错误码: {error_code}, 错误信息: {error_message})"
return format_forecast(data)
通过@mcp.tool()装饰器,我们可以将天气查询功能暴露为 MCP 工具,这一过程自动生成 CLI 调用接口。以get_realtime_weather和get_three_day_forecast函数为例,它们分别用于获取国内城市实时天气和未来 3 天天气预报。在注册为 MCP 工具后,这些函数可以被 MCP 客户端轻松调用,实现与 AI 模型的交互。
完整代码
from typing import Any, Optional
import httpx
import os
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("domestic_weather")
# 心知天气API配置(API_KEY 建议用环境变量管理)
API_KEY = os.getenv("SENIVERSE_API_KEY", "你的API key") # 申请地址:https://www.seniverse.com/
DOMESTIC_API_BASE = "https://api.seniverse.com/v3/weather"
USER_AGENT = "domestic-weather-app/1.0"
UNIT = "c" # 摄氏度
LANGUAGE = "zh-Hans" # 中文返回
async def make_domestic_request(
url: str, params: dict[str, Any]
) -> Optional[dict]:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
base_params = {"key": API_KEY, "language": LANGUAGE, "unit": UNIT}
base_params.update(params)
async with httpx.AsyncClient() as client:
try:
response = await client.get(
url, headers=headers, params=base_params, timeout=30.0
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"[WeatherAPI] 请求失败: {str(e)}")
return None
def format_realtime_weather(data: dict) -> str:
"""格式化实时天气数据"""
try:
result = data.get("results", [{}])[0]
now = result.get("now", {})
location = result.get("location", {})
return (
f"实时天气 - {location.get('name', '未知')}\n"
f"温度: {now.get('temperature', '未知')}°C\n"
f"天气状况: {now.get('text', '未知')}\n"
f"更新时间: {result.get('last_update', '未知')}\n"
)
except Exception as e:
return f"格式化实时天气数据出错: {e}"
def format_forecast(data: dict) -> str:
"""格式化未来3天天气预报"""
try:
periods = data.get("results", [{}])[0].get("daily", [])
forecast_list = []
for period in periods[:3]:
forecast_list.append(
f"日期: {period.get('date', '未知')}\n"
f"白天: {period.get('text_day', '未知')},温度 {period.get('high', '未知')}°C\n"
f"夜间: {period.get('text_night', '未知')},温度 {period.get('low', '未知')}°C\n"
f"风速: {period.get('wind_speed', '未知')} km/h,风向: {period.get('wind_direction', '未知')}\n"
f"湿度: {period.get('humidity', '未知')}%\n"
f"降水量: {period.get('rainfall', '未知')} mm\n"
)
return "\n---\n".join(forecast_list) if forecast_list else "无天气预报数据"
except Exception as e:
return f"格式化天气预报数据出错: {e}"
@mcp.tool()
async def get_realtime_weather(city: str) -> str:
"""获取国内城市实时天气"""
url = f"{DOMESTIC_API_BASE}/now.json"
params = {"location": city}
data = await make_domestic_request(url, params)
if not data or not data.get("results"):
error_code = data.get("status_code", "未知错误码") if data else "无返回"
error_message = data.get("status_message", "未知错误信息") if data else "无返回"
return f"未能获取到该城市的实时信息 (错误码: {error_code}, 错误信息: {error_message})"
return format_realtime_weather(data)
@mcp.tool()
async def get_three_day_forecast(city: str) -> str:
"""获取国内城市未来3天天气预报(包含今日)"""
url = f"{DOMESTIC_API_BASE}/daily.json"
params = {"location": city, "days": 3}
data = await make_domestic_request(url, params)
if not data or not data.get("results"):
error_code = data.get("status_code", "未知错误码") if data else "无返回"
error_message = data.get("status_message", "未知错误信息") if data else "无返回"
return f"未能获取到该城市的天气预报信息 (错误码: {error_code}, 错误信息: {error_message})"
return format_forecast(data)
if __name__ == "__main__":
# 初始化并运行服务器
mcp.run(transport="stdio")
调试与验证
环境配置
更新cline或claude下的config文件
{
"mcpServers": {
"weather": {
"timeout": 60,
"command": "uv",
"args": [
"--directory",
"weather.py所在绝对目录",
"run",
"weather.py"
],
"transportType": "stdio"
}
}
}
查询当前的上海天气
查询未来三天上海的天气预报
调试response可以发现开始日期为26号。
查询api可以看到这是由于默认从当天开始查询导致的,且免费用户最多仅支持三天查询。
重新修改一下接口和提示词,重启服务测试,符合预期。
最后再检查token消耗,可以看到当前cline的上行token消耗非常惊人,未来依然存在巨大的优化空间。
参考资料
modelcontextprotocol.io/quickstart/… seniverse.yuque.com/hyper_data/…