MCP

July 31, 2025 · View on GitHub

Model Context Protocol (MCP) 是一个开放协议,旨在实现 LLM 应用与外部数据源和工具之间的无缝集成。

无论您是构建 AI 驱动的 IDE、增强聊天界面,还是创建自定义 AI 工作流,MCP 都提供了一种标准化的方式来连接 LLM 与外部世界。

简单来说,MCP 是一种客户端-服务器架构的协议,允许 LLM 应用程序(如 Claude、各种 IDE 等)通过标准化的接口访问外部数据和功能。这解决了 LLM 在实际应用中常见的一些痛点:

  • LLM 无法直接访问实时数据(如天气、股票行情等)
  • LLM 无法执行外部操作(如发送邮件、控制设备等)
  • LLM 无法访问用户的本地文件或其他私有数据

通过 MCP,这些限制得到了优雅的解决,同时保持了安全性和可扩展性。

MCP的核心架构

MCP 采用客户端-服务器架构,主要包含以下几个组件:

  • **MCP 主机(Host):**如 Claude Desktop、IDE 或其他 AI 工具,通过 MCP 访问数据
  • **MCP 客户端(Client):**与服务器保持 1:1 连接的协议客户端
  • **MCP 服务器(Server):**轻量级程序,通过标准化的 MCP 协议公开特定功能
  • **本地数据源:**计算机上的文件、数据库和服务,MCP 服务器可以安全访问这些内容
  • **远程服务:**通过互联网可用的外部系统(例如通过 API),MCP 服务器可以连接这些服务

主机可以同时连接多个服务器,每个服务器提供不同的功能,形成一个生态系统:

主机(Claude、IDE 等)<--MCP 协议--> 服务器 A <--> 本地数据源 A                     
                    <--MCP 协议--> 服务器 B <--> 本地数据源 B                     
                    <--MCP 协议--> 服务器 C <--> 远程服务 C

MCP的核心概念

MCP 服务器可以提供三种主要类型的功能:

  1. **资源(Resources):**客户端可以读取的文件类数据(如 API 响应或文件内容)
  2. **工具(Tools):**LLM 可以调用的函数(需要用户批准)
  3. **提示(Prompts):**帮助用户完成特定任务的预写模板

资源(Resources)

资源是可以被客户端读取的文件类数据。它们可以是文本或二进制形式,并有唯一的 URI 标识。

资源可以是:

  • 直接资源:固定内容的资源
  • 资源模板:可以通过参数动态生成的资源

例如,一个文件系统 MCP 服务器可以将本地文件作为资源提供给 LLM,使其能够读取用户的文件。

工具(Tools)

工具是 MCP 中最强大的原语之一,允许服务器向客户端公开可执行的功能。通过工具,LLM 可以与外部系统交互,执行计算,并在现实世界中采取行动。

每个工具都有明确的定义,包括:

  • 名称
  • 描述
  • 输入参数模式(使用 JSON Schema)
  • 输出格式

工具设计为由模型控制,但通常需要人类批准才能执行,这保证了安全性。

提示(Prompts)

提示是预定义的模板,可以帮助用户完成特定任务。它们可以包含动态部分,嵌入资源上下文,并支持多步工作流。

MCP 服务器开发案例:天气服务器

让我们通过一个实际例子来理解 MCP 服务器的开发。我们将构建一个简单的天气服务器,它提供两个工具:获取天气警报和获取天气预报。

步骤 1:设置环境

# 创建项目目录
uv init weather
cd weather

# 创建虚拟环境并激活
uv venv
source .venv/bin/activate  # MacOS/Linux
# 或 .venv\Scripts\activate  # Windows

# 安装依赖
uv add "mcp[cli]" httpx

# 创建服务器文件
touch weather.py

步骤 2:实现天气服务器

from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# 初始化 FastMCP 服务器
mcp = FastMCP("weather")

# 常量
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 发送请求并进行适当的错误处理。"""
    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"""Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}"""


@mcp.tool()
async def get_alerts(state: str) -> str:
    """获取美国州的天气警报。
    
    Args:
        state: 美国州的两字母代码(例如 CA, NY)
    """
    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 "该州没有活跃警报。"
    
    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """获取某个位置的天气预报。
    
    Args:
        latitude: 位置的纬度
        longitude: 位置的经度
    """
    # 首先获取预报网格端点
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)
    
    if not points_data:
        return "无法获取该位置的预报数据。"
    
    # 从 points 响应获取预报 URL
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)
    
    if not forecast_data:
        return "无法获取详细预报。"
    
    # 将周期格式化为可读的预报
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # 仅显示接下来的 5 个周期
        forecast = f"""{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}"""
        forecasts.append(forecast)
    
    return "\n---\n".join(forecasts)


if __name__ == "__main__":
    # 初始化并运行服务器
    mcp.run(transport='stdio')

步骤 3:配置 Claude Desktop 连接服务器

要使用 Claude Desktop 连接到我们的服务器,需要编辑配置文件:

{
	"mcpServers": {
		"weather": {
			"command": "uv",
			"args": ["--directory", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather", "run", "weather.py"]
		}
	}
}

此配置告诉 Claude Desktop 如何启动我们的天气服务器,并使 Claude 能够使用我们实现的工具。

MCP 客户端开发案例

接下来,让我们看看如何开发一个 MCP 客户端,该客户端可以连接到任何 MCP 服务器并利用其功能。

步骤 1:设置环境

# 创建项目目录
uv init mcp-client
cd mcp-client

# 创建虚拟环境
uv venv
source .venv/bin/activate  # MacOS/Linux
# 或 .venv\Scripts\activate  # Windows

# 安装依赖
uv add mcp anthropic python-dotenv

# 创建主文件
touch client.py

步骤 2:设置 API 密钥

创建 .env 文件存储 Anthropic API 密钥:

ANTHROPIC_API_KEY=<your key here>

步骤 3:实现 MCP 客户端

import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()  # 从 .env 加载环境变量
class MCPClient:
    def __init__(self):
        # 初始化会话和客户端对象
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.anthropic = Anthropic()
 
    async def connect_to_server(self, server_script_path: str):
        """连接到 MCP 服务器
        Args:
            server_script_path: 服务器脚本路径 (.py 或 .js)
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("服务器脚本必须是 .py 或 .js 文件")
        command = "python" if is_python else "node"
        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()
        # 列出可用工具
        response = await self.session.list_tools()
        tools = response.tools
        print("\n连接到服务器,可用工具:", [tool.name for tool in tools])
        
    async def process_query(self, query: str) -> str:
        """使用 Claude 和可用工具处理查询"""
        if not self.session:
            return "未连接到任何服务器"
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]
        response = await self.session.list_tools()
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        } for tool in response.tools]
        # 初始 Claude API 调用
        claude_response = await self.anthropic.messages.create(
            model="claude-3-opus-20240229",
            max_tokens=1000,
            messages=messages,
            tools=available_tools
        )
        # 处理工具调用
        message = claude_response.content[0]
        if hasattr(message, 'tool_calls') and message.tool_calls:
            print("\nClaude 请求使用工具:", message.tool_calls[0].name)
            # 执行工具调用
            tool_call = message.tool_calls[0]
            tool_name = tool_call.name
            tool_params = tool_call.params
            print(f"使用参数执行 {tool_name}:", tool_params)
            tool_response = await self.session.execute_tool(tool_name, tool_params)
            # 将工具结果发送回 Claude
            messages.append({
                "role": "assistant",
                "content": [message]
            })
            messages.append({
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_call_id": tool_call.id,
                        "content": tool_response
                    }
                ]
            })
            # 获取最终回答
            final_response = await self.anthropic.messages.create(
                model="claude-3-opus-20240229",
                max_tokens=1000,
                messages=messages
            )
            return final_response.content[0].text
        else:
            # 没有工具调用时直接返回回答
            return message.text
    async def close(self):
        """关闭连接和资源"""
        await self.exit_stack.aclose()
async def main():
    # 创建客户端实例
    client = MCPClient()
    try:
        # 连接到天气服务器
        print("连接到天气服务器...")
        await client.connect_to_server("../weather/weather.py")
        # 交互式循环
        while True:
            query = input("\n输入查询 (输入 'exit' 退出): ")
            if query.lower() == 'exit':
                break
            print("\n处理查询...")
            response = await client.process_query(query)
            print("\n回答:", response)
    finally:
        # 关闭连接
        await client.close()
if __name__ == "__main__":
    asyncio.run(main())

这个客户端可以连接到任何 MCP 服务器,获取其可用工具,然后将用户查询发送到 Claude 进行处理。Claude 可以调用服务器提供的工具,客户端将结果返回给 Claude 以生成最终回答。

FastMCP