跳转到主要内容

category

模型上下文协议 (MCP):让AI真正连接现实世界的"USB-C接口"

是否曾幻想过你的AI聊天机器人能真正执行操作 —— 比如查询天气、访问数据库或操作本地文件,而无需编写繁琐的自定义集成代码?这正是模型上下文协议 (Model Context Protocol,简称MCP) 要解决的痛点。你可以把MCP理解为"AI领域的USB-C":一种标准化的AI模型与外部数据工具对接方式。

本文将通过真实代码示例,演示如何用最简单的方式构建和使用MCP服务器。

MCP的本质是什么?

MCP本质上是一种通信协议,为AI应用(如聊天机器人或IDE助手)定义了与数据源和服务交互的"通用语言"。开发者无需为每个集成对象(如Google Drive、Slack、内部数据库等)编写定制连接器,只需编写一个符合MCP标准的连接器,任何支持MCP的AI都能调用。

为什么这很重要?

  1. ​告别重复造轮子​​:开发者无需为每个数据源重复编写集成代码
  2. ​互操作性​​:为一个AI平台(如Claude、OpenAI)构建的工具,可在其他支持MCP的AI主机(如基于Python的聊天机器人)中复用
  3. ​简洁与安全​​:明确分离数据访问与操作执行,简化审计与权限管理
  4. ​可发现性​​:任何MCP客户端都能查询服务器提供的功能(工具/数据/提示词),消除调用猜测

MCP架构:客户端、服务器与主机

MCP采用客户端-服务器模式,包含以下角色:

  • ​主机 (Host)​​:AI应用本身(如运行GPT的聊天机器人)
  • ​客户端 (Client)​​:运行在主机内部,管理一个或多个MCP服务器的连接
  • ​服务器 (Server)​​:通过MCP暴露特定能力(工具/数据)的独立程序(如天气API访问器)
  • ​数据源 (Data Sources)​​:MCP服务器可读写的底层资源(本地文件/远程API等)

通信流程演示

  1. 主机启动MCP客户端
  2. 客户端连接MCP服务器(如weather.py)
  3. 客户端询问服务器能力(发现工具/资源)
  4. 用户向AI提问
  5. AI通过客户端调用工具(如get_forecast)
  6. 服务器获取天气数据并返回客户端
  7. 客户端将结果传回AI
  8. AI整合数据生成友好响应

就像通过USB-C接口连接不同外设,AI可以通过MCP接入任意服务器。


构建天气MCP服务器

以下是完整的weather.py服务器代码,演示如何从美国国家气象局(NWS)获取数据并通过MCP暴露。我们定义两个工具:

  • ​get_alerts(state)​​:返回指定美国州的活跃天气警报
  • ​get_forecast(latitude, longitude)​​:返回指定坐标的短期天气预报
python
复制
import httpx  # 用于向气象API发起请求
from typing import Any
from mcp.server.fastmcp import FastMCP  # MCP服务器库

# 1. 初始化服务器
mcp = FastMCP("weather")  # 服务器命名为"weather"

# 外部API常量
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
    """向NWS API发起GET请求并返回JSON,出错时返回None"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None


def format_alert(feature: dict) -> str:
    """将天气警报特征转换为可读字符串"""
    props = feature["properties"]
    return (
        f"事件: {props.get('event', '未知')}\n"
        f"区域: {props.get('areaDesc', '未知')}\n"
        f"严重性: {props.get('severity', '未知')}\n"
        f"描述: {props.get('description', '无描述信息')}\n"
        f"应对指南: {props.get('instruction', '无应对指南')}"
    )


@mcp.tool()
async def get_alerts(state: str) -> str:
    """获取美国州级天气警报(使用两字母代码,如CA)"""
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)
    if not data or "features" not in data:
        return "无法获取警报或未找到警报"
    if not data["features"]:
        return f"{state}当前无活跃警报"

    alerts = [format_alert(f) for f in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """获取坐标位置短期天气预报"""
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)
    if not points_data or "properties" not in points_data:
        return "无法获取该位置气象数据"

    forecast_url = points_data["properties"].get("forecast")
    if not forecast_url:
        return "未找到该位置预报URL"

    forecast_data = await make_nws_request(forecast_url)
    if not forecast_data or "properties" not in forecast_data:
        return "无法获取详细预报"

    periods = forecast_data["properties"].get("periods", [])
    if not periods:
        return "无可用预报时段"

    lines = []
    for period in periods[:5]:  # 仅取前5个时段
        lines.append(
            f"{period['name']}:\n"
            f"  温度: {period['temperature']}°{period['temperatureUnit']}\n"
            f"  风力: {period['windSpeed']} {period['windDirection']}\n"
            f"  预报: {period['detailedForecast']}"
        )
    return "\n\n".join(lines)


if __name__ == "__main__":
    mcp.run(transport="stdio")

​关键点:​

  • mcp.tool()装饰器:将get_alerts和get_forecast暴露给客户端
  • 异步处理:使用async/await实现网络请求,确保服务器响应性
  • mcp.run(transport="stdio"):指定通过标准输入输出与客户端通信

运行python weather.py时可能无输出,因为服务器正在等待客户端请求。


构建OpenAI版MCP客户端

接下来构建client.py,实现以下功能:

  1. 将weather.py作为子进程启动
  2. 通过MCP连接服务器
  3. 使用OpenAI函数调用API让GPT决定调用哪个工具
  4. 在服务器执行选定工具并将结果返回GPT生成最终答案
  5. 提供终端聊天界面方便测试
python
复制
import os
import sys
import json
import asyncio
from typing import Optional
from dotenv import load_dotenv

load_dotenv()  # 从.env加载OPENAI_API_KEY

from openai import AsyncOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from contextlib import AsyncExitStack


class MCPClient:
    def __init__(self):
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        # 使用异步OpenAI客户端
        self.openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
        self.model = "gpt-4o"  

    async def connect_to_server(self, server_script_path: str):
        """启动MCP服务器子进程并建立连接"""
        if server_script_path.endswith(".py"):
            command = "python"
        elif server_script_path.endswith(".js"):
            command = "node"
        else:
            raise ValueError("服务器脚本必须是.py或.js")

        server_params = StdioServerParameters(command=command, args=[server_script_path], env=None)
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
        await self.session.initialize()

        # 打印可用工具列表
        tool_list = await self.session.list_tools()
        print(f"已连接服务器。可用工具: {[t.name for t in tool_list.tools]}")

    async def _get_openai_functions(self):
        """将MCP工具转换为OpenAI函数调用格式"""
        tool_list = await self.session.list_tools()
        functions = []
        for tool in tool_list.tools:
            fn_schema = {
                "name": tool.name,
                "description": tool.description or tool.name,
                "parameters": {
                    "type": "object",
                    "properties": {}
                },
                "required": []
            }
            if tool.inputSchema and tool.inputSchema.get("type") == "object":
                fn_schema["parameters"] = tool.inputSchema
            functions.append(fn_schema)
        return functions

    async def process_user_message(self, user_message: str) -> str:
        if not self.session:
            raise RuntimeError("未找到MCP会话,请先调用connect_to_server")

        functions = await self._get_openai_functions()
        # 第一次GPT调用:提供用户消息+可用函数
        response = await self.openai.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": user_message}],
            functions=functions,
            function_call="auto"
        )
        assistant_message = response.choices[0].message

        if hasattr(assistant_message, "function_call") and assistant_message.function_call:
            fn_name = assistant_message.function_call.name
            fn_args_json = assistant_message.function_call.arguments
            try:
                fn_args = json.loads(fn_args_json) if fn_args_json else {}
            except json.JSONDecodeError:
                return "[错误] GPT生成非法JSON参数"

            print(f"\n[工具请求] GPT希望调用{fn_name},参数:{fn_args}")
            tool_result = await self.session.call_tool(fn_name, fn_args)

            # 第二次GPT调用:提供工具执行结果
            followup = await self.openai.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "user", "content": user_message},
                    assistant_message.to_dict(),
                    {"role": "function", "name": fn_name, "content": tool_result.content}
                ]
            )
            return followup.choices[0].message.content
        else:
            # GPT不需要调用函数
            return assistant_message.content

    async def chat_loop(self):
        print("MCP + OpenAI 客户端。输入'quit'或'exit'退出")
        while True:
            user_inp = input("\n你: ")
            if user_inp.strip().lower() in ("quit", "exit"):
                break
            answer = await self.process_user_message(user_inp)
            print(f"\n助手: {answer}")

    async def cleanup(self):
        await self.exit_stack.aclose()


async def main():
    if len(sys.argv) < 2:
        print("用法: python client.py <服务器脚本路径>")
        sys.exit(1)
    server_path = sys.argv[1]

    client = MCPClient()
    try:
        await client.connect_to_server(server_path)
        await client.chat_loop()
    finally:
        await client.cleanup()


if __name__ == "__main__":
    asyncio.run(main())

​关键点:​

  1. ​connect_to_server​​:启动服务器子进程并通过标准输入输出建立MCP会话
  2. ​函数模式转换​​:_get_openai_functions将MCP工具模式转换为OpenAI函数调用需要的JSON格式
  3. ​process_user_message​​:
    • 首次GPT调用:GPT根据用户消息和函数定义决定是否调用工具
    • 执行工具:若需要调用工具,客户端通过await self.session.call_tool(fn_name, fn_args)与MCP服务器通信
    • 二次GPT调用:将工具结果发送给GPT生成最终响应

运行示例

  1. ​安装依赖​​:
bash
复制
pip install "mcp[cli]" openai httpx python-dotenv
  1. ​创建.env文件存放OpenAI API密钥​​:
env
复制
OPENAI_API_KEY=sk-12345...
  1. ​运行客户端(自动启动服务器)​​:
bash
复制
python client.py weather.py
  1. ​开始对话​​:
已连接服务器。可用工具: ['get_alerts', 'get_forecast']
MCP + OpenAI 客户端。输入'quit'或'exit'退出

你: 怀俄明州有哪些警报?
[工具请求] GPT希望调用get_alerts,参数:{'state': 'WY'}

助手: 怀俄明州当前有以下活跃天气警报:
(此处显示格式化后的详细警报信息)

你: 北纬38.58西经121.49的天气预报是?
[工具请求] GPT希望调用get_forecast,参数:{'latitude': 38.58, 'longitude': -121.49}

助手: 该坐标位置未来天气预测如下:
(显示格式化后的天气预报)

GPT4o根据用户输入自动选择MCP工具,服务器从NWS获取数据后,OpenAI API生成简洁的天气摘要。


核心价值

  1. ​标准化AI集成​​:定义服务器(如weather.py)后,任何MCP兼容客户端都能使用 —— 无需为不同AI平台重写代码
  2. ​函数调用模式​​:结合OpenAI函数调用API,实现AI"智能体"的精准工具调用
  3. ​安全的客户端-服务器分离​​:敏感数据和API操作由MCP服务器在受控环境中处理
  4. ​无限扩展性​​:添加邮件发送、数据库查询、文件操作等工具,快速增强AI能力

通过本文示例,您可以快速搭建自己的MCP服务器和客户端,让GPT或其他AI安全高效地访问真实数据。快去构建一些令人惊叹的东西吧!

​延伸思考​​:MCP正在重新定义AI的能力边界 —— 不仅让AI更"聪明",更能安全可靠地与现实世界交互。这种标准化接口模式,或将开启下一代AI应用开发的新范式。

文章链接

标签