手动实现一个简单的MCP Server及原理(Cursor版)

实现步骤参考的是MCP官方教程配合Cursor,原理分析部分使用cloudflare抓取大模型中间请求

一. 手动写一个MCP Server(windows为例)

1.  安装uv包管理器(其他python包管理器也可)

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

2. 初始化python项目

# Create a new directory for our project
uv init weather
cd weather

# Create virtual environment and activate it
uv venv
.venv\Scripts\activate

# Install dependencies
uv add mcp[cli] httpx

# Create our server file
new-item weather.py

3  构建服务器

在weather.py中添加如下代码:

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

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

# 定义常量
NWS_API_BASE = "https://api.weather.gov"  # 美国国家气象服务API的基础URL
USER_AGENT = "weather-app/1.0"           # 用户代理字符串,用于标识请求来源

4. 定义辅助函数(Helper Function)

这段代码包含两个函数,分别用于:

  1. make_nws_request:向美国国家气象局(NWS)的 API 发送异步 HTTP 请求,并处理可能发生的错误。

  2. format_alert:将从 NWS 获取到的单个天气预警数据格式化为可读的字符串,方便用户阅读。

async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with proper error handling."""
    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:
    """Format an alert feature into a readable string."""
    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')}
"""

5. MCP工具函数定义

这段代码定义了两个 MCP 工具函数,用于从美国国家气象局(NWS)的 API 获取天气信息。这两个函数分别通过装饰器 @mcp.tool() 注册为 MCP 工具,供大型语言模型(如 Claude)调用。

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. 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 "Unable to fetch alerts or no alerts found."

    if not data["features"]:
        return "No active alerts for this state."

    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:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    # First get the forecast grid endpoint
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "Unable to fetch forecast data for this location."

    # Get the forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    # Format the periods into a readable forecast
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # Only show next 5 periods
        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)

6. 最后设置入口函数

if __name__ == "__main__":
    # Initialize and run the server
    mcp.run(transport='stdio')

目前为止,一个最简单的MCP Server就完成了,接下来将把他注册到Cursor中

二. 在cursor中使用MCP

1. 确保Cursor更新至最新版本

2. 点击右上角Setting,然后选择MCP Server

3. 点击Add new global MCP Server,并添加如下json,注意,一定要填绝对路径

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

4. 完成后应该能看到出现一个叫weather的server并且显示绿色,以及tools,那就大功告成

5.简单测试,在我的测试下Cursor得选择gpt4o,MCP server才能被正常调用

三. 原理解析

我翻看了B站及抖音一些大佬的分析(cloudflare AI Gateway截取prompt等).

我的理解是:MCP 的本质是通过客户端将用户的 Prompt 与可用工具列表一并发送给 LLM。LLM 在理解意图后,会返回需要调用的工具名称及其对应参数。随后,MCP 客户端通过定义好的统一接口调用对应工具,完成实际执行。

用户输入 Query
        │
        ▼
MCP 客户端构造请求(Prompt + 工具列表)
        │
        ▼
       LLM
        │
        ▼
解析 Prompt,返回工具名 + 参数
        │
        ▼
MCP 客户端通过统一接口调用对应工具
        │
        ▼
     工具执行并返回结果

以官方的client 代码举例,通过self.session.list_tools()方法获取所有的tool,然后构建一个Claudi api call,让大语言模型去判断是否调用工具

 """Process a query using Claude and available tools"""
    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]

    # Initial Claude API call
    response = self.anthropic.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1000,
        messages=messages,
        tools=available_tools
    )

接下来如果返回内容中content的类型是tool_use,则获取tool的名称及入参,然后调用call_tool方法(mcp库提供),进行方法调用,并获得最终的结果

 assistant_message_content = []
    for content in response.content:
        if content.type == 'text':
            final_text.append(content.text)
            assistant_message_content.append(content)
        elif content.type == 'tool_use':
            tool_name = content.name
            tool_args = content.input

            # Execute tool call
            result = await self.session.call_tool(tool_name, tool_args)