MCP搭建天气查询服务:从环境搭建到实战调试

1,329 阅读7分钟

MCP协议

MCP(Model Context Protocol),作为 Anthropic 推出的标准化接口协议,为 AI 模型与外部工具的交互带来了前所未有的便捷。想象一下,AI 模型是一个超级大脑,而 MCP 就是连接这个大脑与各种工具的神经系统,它定义了一套规范,使得 AI 模型能够轻松调用如天气查询、代码执行等外部功能模块。本篇以vs code下cline插件为例,实现一个基础的MCP天气查询服务。

前置步骤

  1. uv 依赖管理工具安装:uv 是本次开发中不可或缺的依赖管理工具,它负责虚拟环境创建、依赖解析及项目初始化等全流程管理,避免了传统方式的繁琐配置。我们可以使用官方提供的脚本快速安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh

安装完成后,我们可以通过uv --version命令来验证是否安装成功,如果成功安装,会显示 uv 的版本号。

  1. cline模型配置:这里以免费的硅基流动deepseek-v3接口为例

image.png

  1. 心知天气 API 密钥申请:在心知天气官网(www.seniverse.com/)进行注册申请,获取免费 API_KEY。在生产环境中,为了确保密钥的安全性,建议通过环境变量来管理。
export SENIVERSE_API_KEY='你的API_KEY'

这样,在我们的项目中就可以通过os.getenv("SENIVERSE_API_KEY")来获取这个密钥。

项目初始化与基础架构搭建

  1. 创建项目与虚拟环境
# 创建项目目录
uv init weather
cd weather

# 创建虚拟环境并激活
uv venv
source .venv/bin/activate

激活虚拟环境后,就进入了一个与系统环境隔离的 Python 运行环境,避免了不同项目之间的依赖冲突。

  1. 核心依赖安装
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"
    }
  }
}

查询当前的上海天气

image.png

查询未来三天上海的天气预报

image.png

image.png 调试response可以发现开始日期为26号。

image.png 查询api可以看到这是由于默认从当天开始查询导致的,且免费用户最多仅支持三天查询。

image.png

重新修改一下接口和提示词,重启服务测试,符合预期。

image.png 最后再检查token消耗,可以看到当前cline的上行token消耗非常惊人,未来依然存在巨大的优化空间。

image.png

参考资料

modelcontextprotocol.io/quickstart/… seniverse.yuque.com/hyper_data/…