导语:在上一章中,我们深入了解了 MCP 协议的核心概念和设计哲学。理论固然重要,但真正的掌握来自于实践。本章将带你从零开始,构建一个完整的、符合 MCP 标准的天气查询工具。这不仅仅是一个简单的天气 API 封装,而是一个展示 MCP 标准化接入最佳实践的完整案例。通过这个项目,你将学会如何设计 MCP 插件、如何实现控制器、如何编写客户端,以及如何将这个工具集成到 LangChain 和 DeepSeek 等主流框架中。
目录
-
项目目标:构建一个生产级的 MCP 天气查询服务
- 功能需求:查询实时天气、天气预报、历史天气
- 技术选型:FastMCP + 天气 API + 标准化接口设计
- 为什么选择天气查询作为示例?
-
步骤一:环境准备与依赖安装
- 安装 FastMCP 和相关依赖
- 获取天气 API Key(OpenWeatherMap 或其他服务)
- 项目结构设计
-
步骤二:设计 MCP 插件:定义工具函数
- 工具 1:
get_current_weather- 获取实时天气 - 工具 2:
get_weather_forecast- 获取天气预报 - 工具 3:
get_weather_history- 获取历史天气(可选) - 使用
@plugin.tool()装饰器定义工具
- 工具 1:
-
步骤三:实现控制器:启动 MCP 服务
- 使用 FastMCP 创建控制器
- 配置路由和中间件
- 启动服务并测试
-
步骤四:编写 MCP 客户端:让 Agent 使用天气服务
- 客户端的核心功能:发现服务、构造请求、解析响应
- 使用
requests库实现标准 MCP 客户端 - 错误处理和重试机制
-
步骤五:集成 LangChain:将 MCP 服务封装为 LangChain Tool
- 创建
McpWeatherTool类 - 实现
_run方法 - 在 LangChain Agent 中使用
- 创建
-
步骤六:集成 DeepSeek:使用国产大模型调用 MCP 服务
- 配置 DeepSeek API
- 使用 LangChain 的 DeepSeek 集成
- 构建完整的 Agent 应用
-
高级特性:缓存、限流与监控
- 实现响应缓存,减少 API 调用
- 添加限流机制,防止滥用
- 集成监控和日志
-
总结:MCP 标准化接入的最佳实践
1. 项目目标:构建一个生产级的 MCP 天气查询服务
1.1 功能需求
我们要构建一个天气查询服务,它能够:
-
查询实时天气:获取指定城市的当前天气状况
- 温度、湿度、风速
- 天气描述(晴天、雨天等)
- 体感温度
-
查询天气预报:获取未来几天的天气预报
- 未来 3-7 天的天气预测
- 每日的最高/最低温度
- 降水概率
-
查询历史天气(可选):获取过去某天的天气数据
- 用于数据分析或对比
1.2 技术选型
- MCP 框架:FastMCP(快速构建 MCP 服务)
- 天气 API:OpenWeatherMap(免费且稳定)
- HTTP 客户端:
requests(简单易用) - 集成框架:LangChain(与 Agent 框架集成)
1.3 为什么选择天气查询作为示例?
天气查询是一个完美的 MCP 示例,因为:
- 需求明确:功能简单,易于理解
- 外部依赖:需要调用第三方 API,展示 MCP 的“桥接”作用
- 实用性强:天气查询是 Agent 的常见需求
- 标准化价值:展示如何将任意 API 封装为 MCP 服务
2. 步骤一:环境准备与依赖安装
2.1 安装依赖
pip install fastmcp uvicorn requests python-dotenv
2.2 获取天气 API Key
- 访问 OpenWeatherMap
- 注册账号并获取免费的 API Key
- 创建
.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 标准化接入的最佳实践
通过本章的学习,我们:
-
构建了一个完整的 MCP 天气查询服务:
- 使用 FastMCP 快速构建插件
- 实现了实时天气和天气预报功能
- 遵循 MCP 标准协议
-
学会了编写 MCP 客户端:
- 实现了服务发现
- 实现了工具调用
- 添加了错误处理
-
集成了主流框架:
- 将 MCP 服务封装为 LangChain Tool
- 使用 DeepSeek 模型调用 MCP 服务
- 构建了完整的 Agent 应用
-
掌握了生产级特性:
- 响应缓存
- 限流机制
- 监控和日志
关键要点:
- 标准化:遵循 MCP 协议,确保兼容性
- 可扩展:设计清晰的接口,便于扩展新功能
- 健壮性:添加错误处理、缓存、限流等机制
- 可观测性:集成日志和监控,便于调试和优化
现在,你已经掌握了 MCP 标准化接入的完整流程。在下一章中,我们将学习如何在 LangChain 和 DeepSeek 之间进行选择,以及如何构建更复杂的多 Agent 系统。