1.4 你绝对不能错过的天气查询工具:MCP 标准化接入实战

2 阅读1分钟

导语:在上一章中,我们深入了解了 MCP 协议的核心概念和设计哲学。理论固然重要,但真正的掌握来自于实践。本章将带你从零开始,构建一个完整的、符合 MCP 标准的天气查询工具。这不仅仅是一个简单的天气 API 封装,而是一个展示 MCP 标准化接入最佳实践的完整案例。通过这个项目,你将学会如何设计 MCP 插件、如何实现控制器、如何编写客户端,以及如何将这个工具集成到 LangChain 和 DeepSeek 等主流框架中。

目录

  1. 项目目标:构建一个生产级的 MCP 天气查询服务

    • 功能需求:查询实时天气、天气预报、历史天气
    • 技术选型:FastMCP + 天气 API + 标准化接口设计
    • 为什么选择天气查询作为示例?
  2. 步骤一:环境准备与依赖安装

    • 安装 FastMCP 和相关依赖
    • 获取天气 API Key(OpenWeatherMap 或其他服务)
    • 项目结构设计
  3. 步骤二:设计 MCP 插件:定义工具函数

    • 工具 1:get_current_weather - 获取实时天气
    • 工具 2:get_weather_forecast - 获取天气预报
    • 工具 3:get_weather_history - 获取历史天气(可选)
    • 使用 @plugin.tool() 装饰器定义工具
  4. 步骤三:实现控制器:启动 MCP 服务

    • 使用 FastMCP 创建控制器
    • 配置路由和中间件
    • 启动服务并测试
  5. 步骤四:编写 MCP 客户端:让 Agent 使用天气服务

    • 客户端的核心功能:发现服务、构造请求、解析响应
    • 使用 requests 库实现标准 MCP 客户端
    • 错误处理和重试机制
  6. 步骤五:集成 LangChain:将 MCP 服务封装为 LangChain Tool

    • 创建 McpWeatherTool
    • 实现 _run 方法
    • 在 LangChain Agent 中使用
  7. 步骤六:集成 DeepSeek:使用国产大模型调用 MCP 服务

    • 配置 DeepSeek API
    • 使用 LangChain 的 DeepSeek 集成
    • 构建完整的 Agent 应用
  8. 高级特性:缓存、限流与监控

    • 实现响应缓存,减少 API 调用
    • 添加限流机制,防止滥用
    • 集成监控和日志
  9. 总结:MCP 标准化接入的最佳实践


1. 项目目标:构建一个生产级的 MCP 天气查询服务

1.1 功能需求

我们要构建一个天气查询服务,它能够:

  1. 查询实时天气:获取指定城市的当前天气状况

    • 温度、湿度、风速
    • 天气描述(晴天、雨天等)
    • 体感温度
  2. 查询天气预报:获取未来几天的天气预报

    • 未来 3-7 天的天气预测
    • 每日的最高/最低温度
    • 降水概率
  3. 查询历史天气(可选):获取过去某天的天气数据

    • 用于数据分析或对比

1.2 技术选型

  • MCP 框架:FastMCP(快速构建 MCP 服务)
  • 天气 API:OpenWeatherMap(免费且稳定)
  • HTTP 客户端requests(简单易用)
  • 集成框架:LangChain(与 Agent 框架集成)

1.3 为什么选择天气查询作为示例?

天气查询是一个完美的 MCP 示例,因为:

  1. 需求明确:功能简单,易于理解
  2. 外部依赖:需要调用第三方 API,展示 MCP 的“桥接”作用
  3. 实用性强:天气查询是 Agent 的常见需求
  4. 标准化价值:展示如何将任意 API 封装为 MCP 服务

2. 步骤一:环境准备与依赖安装

2.1 安装依赖

pip install fastmcp uvicorn requests python-dotenv

2.2 获取天气 API Key

  1. 访问 OpenWeatherMap
  2. 注册账号并获取免费的 API Key
  3. 创建 .env 文件:
OPENWEATHER_API_KEY=your_api_key_here

2.3 项目结构

weather-mcp-service/
├── main.py              # MCP 服务主文件
├── weather_api.py       # 天气 API 封装
├── client.py            # MCP 客户端示例
├── langchain_integration.py  # LangChain 集成
├── .env                 # 环境变量
└── requirements.txt     # 依赖列表

3. 步骤二:设计 MCP 插件:定义工具函数

3.1 天气 API 封装

首先,我们创建一个封装 OpenWeatherMap API 的模块:

# weather_api.py
import os
import requests
from typing import Dict, Optional, List
from datetime import datetime, timedelta

class WeatherAPI:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.openweathermap.org/data/2.5"
    
    def get_current_weather(self, city: str, units: str = "metric") -> Dict:
        """
        获取实时天气
        
        Args:
            city: 城市名称(如 "Beijing" 或 "北京")
            units: 单位系统("metric" 表示摄氏度,"imperial" 表示华氏度)
        
        Returns:
            天气数据字典
        """
        url = f"{self.base_url}/weather"
        params = {
            "q": city,
            "appid": self.api_key,
            "units": units,
            "lang": "zh_cn"  # 中文描述
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            return {
                "city": data["name"],
                "country": data["sys"]["country"],
                "temperature": data["main"]["temp"],
                "feels_like": data["main"]["feels_like"],
                "humidity": data["main"]["humidity"],
                "pressure": data["main"]["pressure"],
                "description": data["weather"][0]["description"],
                "wind_speed": data.get("wind", {}).get("speed", 0),
                "wind_direction": data.get("wind", {}).get("deg", 0),
                "visibility": data.get("visibility", 0) / 1000,  # 转换为公里
                "timestamp": datetime.now().isoformat()
            }
        except requests.exceptions.RequestException as e:
            return {"error": f"获取天气失败: {str(e)}"}
    
    def get_weather_forecast(self, city: str, days: int = 5, units: str = "metric") -> Dict:
        """
        获取天气预报
        
        Args:
            city: 城市名称
            days: 预报天数(最多 5 天)
            units: 单位系统
        
        Returns:
            天气预报数据
        """
        url = f"{self.base_url}/forecast"
        params = {
            "q": city,
            "appid": self.api_key,
            "units": units,
            "lang": "zh_cn",
            "cnt": days * 8  # 每 3 小时一个数据点,一天 8 个
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            # 按日期分组
            forecasts = {}
            for item in data["list"]:
                date = datetime.fromtimestamp(item["dt"]).date()
                if date not in forecasts:
                    forecasts[date] = {
                        "date": date.isoformat(),
                        "temperatures": [],
                        "descriptions": [],
                        "humidity": [],
                        "wind_speed": []
                    }
                
                forecasts[date]["temperatures"].append(item["main"]["temp"])
                forecasts[date]["descriptions"].append(item["weather"][0]["description"])
                forecasts[date]["humidity"].append(item["main"]["humidity"])
                forecasts[date]["wind_speed"].append(item.get("wind", {}).get("speed", 0))
            
            # 计算每日统计
            daily_forecasts = []
            for date, data in sorted(forecasts.items())[:days]:
                daily_forecasts.append({
                    "date": data["date"],
                    "min_temp": min(data["temperatures"]),
                    "max_temp": max(data["temperatures"]),
                    "avg_temp": sum(data["temperatures"]) / len(data["temperatures"]),
                    "description": max(set(data["descriptions"]), key=data["descriptions"].count),  # 最常见的描述
                    "avg_humidity": sum(data["humidity"]) / len(data["humidity"]),
                    "avg_wind_speed": sum(data["wind_speed"]) / len(data["wind_speed"])
                })
            
            return {
                "city": data["city"]["name"],
                "country": data["city"]["country"],
                "forecasts": daily_forecasts
            }
        except requests.exceptions.RequestException as e:
            return {"error": f"获取天气预报失败: {str(e)}"}

3.2 使用 FastMCP 定义工具

现在,我们使用 FastMCP 创建 MCP 插件:

# main.py
import os
from dotenv import load_dotenv
from fastmcp import FastMCP
from weather_api import WeatherAPI

# 加载环境变量
load_dotenv()

# 初始化天气 API
weather_api = WeatherAPI(api_key=os.getenv("OPENWEATHER_API_KEY"))

# 创建 FastMCP 应用
app = FastMCP("Weather Service")

# 工具 1:获取实时天气
@app.tool()
def get_current_weather(
    city: str,
    units: str = "metric"
) -> dict:
    """
    获取指定城市的实时天气信息
    
    Args:
        city: 城市名称,例如 "Beijing"、"北京"、"New York"
        units: 温度单位,"metric" 表示摄氏度(默认),"imperial" 表示华氏度
    
    Returns:
        包含温度、湿度、风速、天气描述等信息的字典
    """
    return weather_api.get_current_weather(city, units)

# 工具 2:获取天气预报
@app.tool()
def get_weather_forecast(
    city: str,
    days: int = 5,
    units: str = "metric"
) -> dict:
    """
    获取指定城市的天气预报
    
    Args:
        city: 城市名称
        days: 预报天数,1-5 天(默认 5 天)
        units: 温度单位,"metric" 或 "imperial"
    
    Returns:
        包含未来几天天气预报的字典
    """
    if days < 1 or days > 5:
        return {"error": "预报天数必须在 1-5 之间"}
    
    return weather_api.get_weather_forecast(city, days, units)

# 启动服务
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

3.3 测试 MCP 服务

启动服务:

python main.py

使用 curl 测试:

# 测试获取实时天气
curl -X POST http://localhost:8000/mcp/tool/get_current_weather \
  -H "Content-Type: application/json" \
  -d '{"city": "Beijing", "units": "metric"}'

4. 步骤三:实现控制器:启动 MCP 服务

FastMCP 已经帮我们处理了大部分控制器逻辑,但我们还可以添加一些增强功能:

4.1 添加中间件(可选)

# main.py (增强版)
from fastmcp import FastMCP
from fastapi.middleware.cors import CORSMiddleware

app = FastMCP("Weather Service")

# 添加 CORS 中间件(如果需要跨域访问)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ... 工具定义 ...

4.2 添加健康检查端点

@app.get("/health")
def health_check():
    return {"status": "healthy", "service": "Weather MCP Service"}

5. 步骤四:编写 MCP 客户端:让 Agent 使用天气服务

5.1 标准 MCP 客户端实现

# client.py
import requests
import json
from typing import Dict, Optional, List

class McpClient:
    def __init__(self, base_url: str = "http://localhost:8000"):
        self.base_url = base_url
        self.mcp_base = f"{base_url}/mcp"
    
    def list_tools(self) -> List[Dict]:
        """
        列出所有可用的工具
        """
        try:
            response = requests.get(f"{self.mcp_base}/tools", timeout=5)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"获取工具列表失败: {e}")
            return []
    
    def call_tool(self, tool_name: str, **kwargs) -> Dict:
        """
        调用指定的工具
        
        Args:
            tool_name: 工具名称
            **kwargs: 工具参数
        
        Returns:
            工具执行结果
        """
        url = f"{self.mcp_base}/tool/{tool_name}"
        payload = kwargs
        
        try:
            response = requests.post(
                url,
                json=payload,
                headers={"Content-Type": "application/json"},
                timeout=10
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            return {"error": f"调用工具失败: {str(e)}"}
    
    def get_tool_schema(self, tool_name: str) -> Optional[Dict]:
        """
        获取工具的 JSON Schema
        """
        tools = self.list_tools()
        for tool in tools:
            if tool.get("name") == tool_name:
                return tool
        return None

# 使用示例
if __name__ == "__main__":
    client = McpClient()
    
    # 列出所有工具
    print("=== 可用工具 ===")
    tools = client.list_tools()
    for tool in tools:
        print(f"- {tool.get('name')}: {tool.get('description', '')}")
    
    # 调用工具
    print("\n=== 查询北京天气 ===")
    result = client.call_tool("get_current_weather", city="Beijing")
    print(json.dumps(result, ensure_ascii=False, indent=2))
    
    # 查询天气预报
    print("\n=== 查询北京未来 3 天天气预报 ===")
    forecast = client.call_tool("get_weather_forecast", city="Beijing", days=3)
    print(json.dumps(forecast, ensure_ascii=False, indent=2))

6. 步骤五:集成 LangChain:将 MCP 服务封装为 LangChain Tool

6.1 创建 LangChain Tool 包装器

# langchain_integration.py
from langchain.tools import BaseTool
from typing import Optional, Type
from pydantic import BaseModel, Field
from client import McpClient

class WeatherInput(BaseModel):
    """天气查询工具的输入"""
    city: str = Field(description="城市名称,例如 'Beijing'、'北京'、'New York'")
    units: str = Field(default="metric", description="温度单位,'metric' 表示摄氏度,'imperial' 表示华氏度")

class WeatherForecastInput(BaseModel):
    """天气预报工具的输入"""
    city: str = Field(description="城市名称")
    days: int = Field(default=5, description="预报天数,1-5 天")
    units: str = Field(default="metric", description="温度单位")

class McpWeatherTool(BaseTool):
    """通过 MCP 服务查询实时天气的 LangChain Tool"""
    name = "get_current_weather"
    description = "获取指定城市的实时天气信息,包括温度、湿度、风速、天气描述等"
    args_schema: Type[BaseModel] = WeatherInput
    
    def __init__(self, mcp_client: McpClient):
        super().__init__()
        self.client = mcp_client
    
    def _run(self, city: str, units: str = "metric") -> str:
        """执行工具"""
        result = self.client.call_tool("get_current_weather", city=city, units=units)
        
        if "error" in result:
            return f"查询失败: {result['error']}"
        
        # 格式化输出
        return f"""
{city} 当前天气:
- 温度: {result.get('temperature', 'N/A')}°C
- 体感温度: {result.get('feels_like', 'N/A')}°C
- 湿度: {result.get('humidity', 'N/A')}%
- 气压: {result.get('pressure', 'N/A')} hPa
- 天气: {result.get('description', 'N/A')}
- 风速: {result.get('wind_speed', 'N/A')} m/s
- 能见度: {result.get('visibility', 'N/A')} km
""".strip()
    
    async def _arun(self, city: str, units: str = "metric") -> str:
        """异步执行(可选)"""
        return self._run(city, units)

class McpWeatherForecastTool(BaseTool):
    """通过 MCP 服务查询天气预报的 LangChain Tool"""
    name = "get_weather_forecast"
    description = "获取指定城市的天气预报,可以查询未来 1-5 天的天气"
    args_schema: Type[BaseModel] = WeatherForecastInput
    
    def __init__(self, mcp_client: McpClient):
        super().__init__()
        self.client = mcp_client
    
    def _run(self, city: str, days: int = 5, units: str = "metric") -> str:
        """执行工具"""
        result = self.client.call_tool("get_weather_forecast", city=city, days=days, units=units)
        
        if "error" in result:
            return f"查询失败: {result['error']}"
        
        # 格式化输出
        output = f"{city} 未来 {days} 天天气预报:\n\n"
        for forecast in result.get("forecasts", []):
            output += f"""
{forecast['date']}:
- 最高温度: {forecast['max_temp']}°C
- 最低温度: {forecast['min_temp']}°C
- 平均温度: {forecast['avg_temp']:.1f}°C
- 天气: {forecast['description']}
- 平均湿度: {forecast['avg_humidity']:.1f}%
- 平均风速: {forecast['avg_wind_speed']:.1f} m/s
"""
        
        return output.strip()

6.2 在 LangChain Agent 中使用

# langchain_agent_example.py
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
from langchain_integration import McpWeatherTool, McpWeatherForecastTool
from client import McpClient

# 初始化 MCP 客户端
mcp_client = McpClient(base_url="http://localhost:8000")

# 创建工具
tools = [
    McpWeatherTool(mcp_client),
    McpWeatherForecastTool(mcp_client)
]

# 初始化 LLM(可以使用 OpenAI 或 DeepSeek)
llm = ChatOpenAI(
    model="gpt-4",
    temperature=0
)

# 创建 Agent
agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True
)

# 使用示例
if __name__ == "__main__":
    # 查询实时天气
    result = agent.run("北京今天天气怎么样?")
    print(result)
    
    # 查询天气预报
    result = agent.run("帮我查一下上海未来 3 天的天气预报")
    print(result)

7. 步骤六:集成 DeepSeek:使用国产大模型调用 MCP 服务

7.1 配置 DeepSeek API

DeepSeek 提供了 OpenAI 兼容的 API,我们可以直接使用:

# deepseek_integration.py
from langchain_openai import ChatOpenAI
from langchain.agents import initialize_agent, AgentType
from langchain_integration import McpWeatherTool, McpWeatherForecastTool
from client import McpClient
import os

# 配置 DeepSeek API
os.environ["OPENAI_API_KEY"] = "your_deepseek_api_key"
os.environ["OPENAI_API_BASE"] = "https://api.deepseek.com"

# 初始化 MCP 客户端和工具
mcp_client = McpClient(base_url="http://localhost:8000")
tools = [
    McpWeatherTool(mcp_client),
    McpWeatherForecastTool(mcp_client)
]

# 使用 DeepSeek 模型
llm = ChatOpenAI(
    model="deepseek-chat",  # 或 "deepseek-coder"
    temperature=0,
    openai_api_base="https://api.deepseek.com"
)

# 创建 Agent
agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True
)

# 使用示例
if __name__ == "__main__":
    result = agent.run("帮我查一下深圳今天的天气,还有未来 3 天的预报")
    print(result)

8. 高级特性:缓存、限流与监控

8.1 实现响应缓存

# weather_api.py (增强版)
from functools import lru_cache
from datetime import datetime, timedelta

class WeatherAPI:
    def __init__(self, api_key: str, cache_ttl: int = 300):
        self.api_key = api_key
        self.base_url = "https://api.openweathermap.org/data/2.5"
        self.cache_ttl = cache_ttl  # 缓存时间(秒)
        self._cache = {}
    
    def _get_cache_key(self, city: str, endpoint: str) -> str:
        return f"{endpoint}:{city.lower()}"
    
    def _is_cache_valid(self, cache_entry: dict) -> bool:
        if not cache_entry:
            return False
        age = (datetime.now() - cache_entry["timestamp"]).total_seconds()
        return age < self.cache_ttl
    
    def get_current_weather(self, city: str, units: str = "metric") -> Dict:
        cache_key = self._get_cache_key(city, "current")
        
        # 检查缓存
        if cache_key in self._cache:
            cached = self._cache[cache_key]
            if self._is_cache_valid(cached):
                return cached["data"]
        
        # 调用 API
        result = self._fetch_current_weather(city, units)
        
        # 更新缓存
        self._cache[cache_key] = {
            "data": result,
            "timestamp": datetime.now()
        }
        
        return result
    
    def _fetch_current_weather(self, city: str, units: str) -> Dict:
        # ... 原有的 API 调用逻辑 ...
        pass

8.2 添加限流机制

# main.py (增强版)
from collections import defaultdict
from datetime import datetime, timedelta

class RateLimiter:
    def __init__(self, max_requests: int = 60, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = defaultdict(list)
    
    def is_allowed(self, client_id: str) -> bool:
        now = datetime.now()
        window_start = now - timedelta(seconds=self.window_seconds)
        
        # 清理过期请求
        self.requests[client_id] = [
            req_time for req_time in self.requests[client_id]
            if req_time > window_start
        ]
        
        # 检查是否超过限制
        if len(self.requests[client_id]) >= self.max_requests:
            return False
        
        # 记录本次请求
        self.requests[client_id].append(now)
        return True

# 在 FastMCP 中使用
rate_limiter = RateLimiter(max_requests=60, window_seconds=60)

@app.middleware("http")
async def rate_limit_middleware(request, call_next):
    client_id = request.client.host
    if not rate_limiter.is_allowed(client_id):
        return JSONResponse(
            status_code=429,
            content={"error": "请求过于频繁,请稍后再试"}
        )
    return await call_next(request)

8.3 集成监控和日志

# main.py (增强版)
import logging
from datetime import datetime

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# 在工具函数中添加日志
@app.tool()
def get_current_weather(city: str, units: str = "metric") -> dict:
    logger.info(f"查询天气: city={city}, units={units}")
    start_time = datetime.now()
    
    try:
        result = weather_api.get_current_weather(city, units)
        duration = (datetime.now() - start_time).total_seconds()
        logger.info(f"查询成功: duration={duration:.2f}s")
        return result
    except Exception as e:
        logger.error(f"查询失败: {e}")
        return {"error": str(e)}

9. 总结:MCP 标准化接入的最佳实践

通过本章的学习,我们:

  1. 构建了一个完整的 MCP 天气查询服务

    • 使用 FastMCP 快速构建插件
    • 实现了实时天气和天气预报功能
    • 遵循 MCP 标准协议
  2. 学会了编写 MCP 客户端

    • 实现了服务发现
    • 实现了工具调用
    • 添加了错误处理
  3. 集成了主流框架

    • 将 MCP 服务封装为 LangChain Tool
    • 使用 DeepSeek 模型调用 MCP 服务
    • 构建了完整的 Agent 应用
  4. 掌握了生产级特性

    • 响应缓存
    • 限流机制
    • 监控和日志

关键要点

  • 标准化:遵循 MCP 协议,确保兼容性
  • 可扩展:设计清晰的接口,便于扩展新功能
  • 健壮性:添加错误处理、缓存、限流等机制
  • 可观测性:集成日志和监控,便于调试和优化

现在,你已经掌握了 MCP 标准化接入的完整流程。在下一章中,我们将学习如何在 LangChain 和 DeepSeek 之间进行选择,以及如何构建更复杂的多 Agent 系统。