MCP(模型上下文协议)保姆级教程实战篇(三)----MCP服务器端搭建

一、MCP服务器的概念

MCP服务器(Model Context Protocol Server)是一种轻量级程序,旨在将大型语言模型(LLM)与外部数据源和工具无缝集成。它通过标准化的协议,使得LLM能够访问实时数据并执行更复杂的任务。

概念

  • 核心定义:MCP服务器作为连接AI与外部世界的标准化桥梁,类似于USB-C接口的"统一协议",使AI能够轻松对接各种业务接口,提供智能化功能。

  • 主要功能

    • 资源(Resources):读取数据,如查询SQLite课程数据库、读取本地CSV文件等。

    • 工具(Tools):执行函数,如微信消息实时抓取与总结、网页内容转Markdown等。

    • 提示(Prompts):预置任务模板,如生成Git提交信息模板、自动编写代码注释等。

  • 架构优势:MCP服务器基于JSON-RPC构建,提供了一个有状态会话协议,专注于客户端和服务器之间的上下文交换和采样协调。

通讯机制

  • 传输层:MCP使用JSON-RPC 2.0作为其数据传输格式,传输层负责将MCP协议消息转换为JSON-RPC格式进行传输,并将接收到的JSON-RPC消息转换回MCP协议消息。

  • 内置传输类型

    • 标准输入/输出(stdio):适用于本地集成和命令行工具,客户端将MCP服务器启动为子进程,服务器通过标准输入接收消息,通过标准输出写入响应。

    • 服务器发送事件(SSE):支持服务器到客户端的流式传输,客户端通过HTTP POST请求向服务器发送消息,服务器通过SSE端点向客户端发送消息。

  • 消息格式:包括请求、响应和通知三种类型。请求消息包含唯一标识符、方法名称和可选参数;响应消息包含结果或错误信息;通知消息用于服务器向客户端发送消息,无需唯一标识符。

应用场景

  • AI模型服务:如大语言模型(GPT系列)、图像生成模型(Stable Diffusion)、语音识别/合成模型等。

  • 工具和功能服务:如代码分析工具、文本处理工具、数据转换工具等。

  • 集成服务:如API网关、模型编排服务、资源调度服务等

二、MCP服务器的搭建

2.1 服务器依赖安装

uv add mcp httpx

2.2 服务器代码

天气查询服务器的代码如下,我的项目与新能源发电相关,所以返回的信息也与新能源发电相关联,具体代码如下,每一行都给了解释和说明,供大家参考。

 导入必要的模块
import json  # 导入 JSON 处理模块
import os  # 导入 os 模块,用于环境变量和系统操作
import time  # 导入 time 模块,用于时间相关操作
import logging  # 导入日志模块,用于记录程序运行状态
import asyncio  # 导入异步 IO 模块
from datetime import datetime, timedelta  # 导入日期时间处理模块
from typing import Any, Optional, Dict  # 导入类型提示模块
import httpx  # 导入异步 HTTP 客户端模块
from mcp.server.fastmcp import FastMCP  # 导入 MCP 服务器模块

# 配置日志系统
logging.basicConfig(  # 配置日志基本设置
    level=logging.INFO,  # 设置日志级别为 INFO
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'  # 设置日志格式
)
logger = logging.getLogger(__name__)  # 创建日志记录器

# 初始化 MCP 服务器
mcp = FastMCP(title="Weather Server")  # 创建 MCP 服务器实例

# OpenWeather API 配置
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather"  # OpenWeather API 的基础 URL
API_KEY = os.getenv("OPENWEATHER_API_KEY", "YOUR_API_KEY")  # 从环境变量获取 API 密钥,如果没有则使用默认值
USER_AGENT = "weather-app/1.0"  # 设置用户代理标识
MAX_RETRIES = 3  # 设置最大重试次数
RETRY_DELAY = 1  # 设置重试延迟时间(秒)
CACHE_DURATION = 1800  # 设置缓存持续时间(30分钟)

# 简单的内存缓存
weather_cache: Dict[str, Dict[str, Any]] = {}  # 创建天气数据缓存字典

async def fetch_weather(city: str) -> Optional[Dict[str, Any]]:  # 定义异步获取天气数据的函数
    """
    从 OpenWeather API 获取天气信息,包含重试机制和缓存。
    
    Args:
        city: 城市名称(英文)
    
    Returns:
        天气数据字典或 None(如果发生错误)
    """
    # 检查缓存
    cache_key = city.lower()  # 将城市名转换为小写作为缓存键
    if cache_key in weather_cache:  # 检查缓存中是否存在数据
        cache_data = weather_cache[cache_key]  # 获取缓存数据
        if datetime.now().timestamp() - cache_data['timestamp'] < CACHE_DURATION:  # 检查缓存是否过期
            logger.info(f"返回缓存数据: {city}")  # 记录日志
            return cache_data['data']  # 返回缓存的数据
    
    params = {  # 设置 API 请求参数
        "q": city,  # 城市名称
        "appid": API_KEY,  # API 密钥
        "units": "metric",  # 使用公制单位
        "lang": "zh_cn"  # 使用中文返回结果
    }
    headers = {"User-Agent": USER_AGENT}  # 设置请求头

    for attempt in range(MAX_RETRIES):  # 重试循环
        try:
            async with httpx.AsyncClient() as client:  # 创建异步 HTTP 客户端
                response = await client.get(  # 发送 GET 请求
                    OPENWEATHER_API_BASE,  # API URL
                    params=params,  # 请求参数
                    headers=headers,  # 请求头
                    timeout=30.0  # 超时时间
                )
                response.raise_for_status()  # 检查响应状态
                data = response.json()  # 解析 JSON 响应
                
                # 更新缓存
                weather_cache[cache_key] = {  # 将数据存入缓存
                    'data': data,  # 天气数据
                    'timestamp': datetime.now().timestamp()  # 缓存时间戳
                }
                
                logger.info(f"成功获取天气数据: {city}")  # 记录成功日志
                return data  # 返回天气数据
                
        except httpx.HTTPStatusError as e:  # 捕获 HTTP 错误
            logger.error(f"HTTP错误 ({attempt + 1}/{MAX_RETRIES}): {e.response.status_code}")  # 记录错误日志
            if attempt < MAX_RETRIES - 1:  # 如果还有重试机会
                await asyncio.sleep(RETRY_DELAY * (attempt + 1))  # 等待一段时间后重试
            else:
                return {"error": f"HTTP错误: {e.response.status_code}"}  # 返回错误信息
                
        except Exception as e:  # 捕获其他异常
            logger.error(f"请求失败 ({attempt + 1}/{MAX_RETRIES}): {str(e)}")  # 记录错误日志
            if attempt < MAX_RETRIES - 1:  # 如果还有重试机会
                await asyncio.sleep(RETRY_DELAY * (attempt + 1))  # 等待一段时间后重试
            else:
                return {"error": f"请求失败: {str(e)}"}  # 返回错误信息
    
    return None  # 如果所有重试都失败,返回 None

def format_weather(data: Dict[str, Any] | str) -> str:  # 定义格式化天气数据的函数
    """
    将天气数据格式化为易读文本,重点关注对新能源发电有影响的天气信息。
    
    Args:
        data: 天气数据(字典或JSON字符串)
    
    Returns:
        格式化后的天气信息
    """
    if isinstance(data, str):  # 如果输入是字符串
        try:
            data = json.loads(data)  # 尝试解析 JSON 字符串
        except json.JSONDecodeError as e:  # 捕获 JSON 解析错误
            logger.error(f"JSON解析错误: {e}")  # 记录错误日志
            return f"⚠️ 数据格式错误: {e}"  # 返回错误信息

    if "error" in data:  # 检查是否包含错误信息
        return f"⚠️ {data['error']}"  # 返回错误信息

    try:
        city = data.get("name", "未知")  # 获取城市名称
        country = data.get("sys", {}).get("country", "未知")  # 获取国家代码
        temp = data.get("main", {}).get("temp", "N/A")  # 获取温度
        humidity = data.get("main", {}).get("humidity", "N/A")  # 获取湿度
        
        # 风能相关数据
        wind_speed = data.get("wind", {}).get("speed", "N/A")  # 获取风速
        wind_deg = data.get("wind", {}).get("deg", "N/A")  # 获取风向角度
        wind_direction = get_wind_direction(wind_deg)  # 转换风向角度为方向描述
        
        # 太阳能相关数据
        clouds = data.get("clouds", {}).get("all", "N/A")  # 获取云量百分比
        weather_list = data.get("weather", [{}])  # 获取天气描述列表
        description = weather_list[0].get("description", "未知")  # 获取天气描述
        
        # 获取紫外线指数(如果API提供)
        uvi = data.get("uvi", "N/A")  # 获取紫外线指数
        
        # 获取降雨量(如果API提供)
        rain_1h = data.get("rain", {}).get("1h", "N/A")  # 获取1小时降雨量
        
        # 评估风能发电条件
        wind_condition = "良好" if isinstance(wind_speed, (int, float)) and wind_speed >= 3.0 else "不足"
        wind_power = "适合" if wind_condition == "良好" else "不适合"
        
        # 评估太阳能发电条件
        solar_condition = "良好" if isinstance(clouds, (int, float)) and clouds <= 30 else "不足"
        solar_power = "适合" if solar_condition == "良好" else "不适合"
        
        return (  # 返回格式化后的天气信息
            f" {city}, {country}\n"  # 城市和国家
            f" 温度: {temp}°C\n"  # 温度
            f" 湿度: {humidity}%\n"  # 湿度
            f" 天气: {description}\n"  # 天气描述
            f"☁️ 云量: {clouds}% (太阳能发电条件: {solar_condition}, {solar_power}光伏发电)\n"  # 云量和太阳能发电评估
            f"☀️ 紫外线指数: {uvi} (影响太阳能板发电效率)\n"  # 紫外线指数
            f" 1小时降雨量: {rain_1h}mm (可能影响设备运行)\n"  # 降雨量
            f" 风速: {wind_speed} m/s ({wind_direction}) (风能发电条件: {wind_condition}, {wind_power}风力发电)\n"  # 风速和风向
            f"\n新能源发电评估:\n"  # 新能源发电总体评估
            f"1️⃣ 风能发电: {wind_power} (风速需≥3.0m/s)\n"  # 风能发电评估
            f"2️⃣ 太阳能发电: {solar_power} (云量需≤30%)\n"  # 太阳能发电评估
            f"3️⃣ 综合建议: {'建议开启新能源发电' if wind_condition == '良好' or solar_condition == '良好' else '建议使用传统能源'}"  # 综合建议
        )
    except Exception as e:  # 捕获所有异常
        logger.error(f"数据格式化错误: {e}")  # 记录错误日志
        return f"⚠️ 数据格式化错误: {str(e)}"  # 返回错误信息

def get_wind_direction(degrees: float | str) -> str:  # 定义获取风向描述的函数
    """
    将风向角度转换为方向描述。
    
    Args:
        degrees: 风向角度(0-360)
    
    Returns:
        风向描述(如:东北风)
    """
    if isinstance(degrees, str) or degrees == "N/A":  # 检查输入是否有效
        return "未知"  # 返回未知
        
    directions = ["北", "东北", "东", "东南", "南", "西南", "西", "西北"]  # 定义八个方向
    index = round(degrees / 45) % 8  # 计算方向索引
    return f"{directions[index]}风"  # 返回风向描述

@mcp.tool()  # 注册为 MCP 工具,修正装饰器语法
async def query_weather(city: str) -> str:  # 定义查询天气的异步函数
    """
    查询指定城市的天气信息。
    
    Args:
        city: 城市名称(英文)
    
    Returns:
        格式化后的天气信息
    """
    logger.info(f"收到天气查询请求: {city}")  # 记录请求日志
    data = await fetch_weather(city)  # 获取天气数据
    if data is None:  # 如果获取数据失败
        return "⚠️ 获取天气数据失败"  # 返回错误信息
    return format_weather(data)  # 返回格式化后的天气信息

if __name__ == "__main__":  # 如果直接运行此文件
    logger.info("启动天气查询服务器...")  # 记录启动日志
    mcp.run()  # 启动 MCP 服务器

你可能感兴趣的:(人工智能,python)