30 分钟搭建你的第一个 MCP Server(Python 版)

周五下午四点,老李把 MCP 协议规范合上,揉了揉太阳穴。

“小王,理论我都看了,架构图也画了三张。但你跟我说实话——上手到底有多复杂?别像当年学 Kubernetes 那样,光环境就搭了两天。”

小王把咖啡杯往桌上一放:“老李,你敢不敢跟我打个赌?从现在开始,30 分钟内,你亲手写一个能用的 MCP Server,然后在你电脑上的 Claude Desktop 里跑起来。如果超时,我请你一周咖啡。”

老李眼睛一亮:“就 30 分钟?你确定?MCP 那套 Client-Server 架构、能力协商、JSON-RPC——光初始化流程就够写半天的吧?”

“那是协议的底层细节。你用 Python SDK,底层全被封装好了。你只需要关心三件事——你的 Server 能提供什么 Tools、什么 Resources、什么 Prompts。剩下的框架帮你搞定。”小王把老李的键盘往前一推,“计时开始。”


第 0-5 分钟:环境准备

小王指导老李打开终端:“先装 MCP 的 Python SDK。目前官方有两个核心包——mcp 是框架本体,uvicorn 是可选的 HTTP transport 依赖。我们先用最简单的 stdio 模式,不需要装额外的。”

老李敲下命令:

bash
pip install mcp

“就一个包?”老李有点不敢相信。

“就一个包。MCP 的 Python SDK 把 JSON-RPC 协议、stdio 通信、能力协商全封装了。你只需要从 mcp.server 里继承一个类,写你自己的业务逻辑。”

安装完毕。老李看了看表——才过了三分钟。


第 5-12 分钟:Hello MCP——第一个 Tool

“先写一个最简单的 Server,让它提供一个 hello 工具。”小王把代码模板投影到屏幕上。

老李新建一个文件,跟着敲起来:

python
# hello_server.py —— 第一个 MCP Server
from mcp.server import Server
from mcp.server.stdio import stdio_server

# 1. 创建 Server 实例,给它取个名字
app = Server("hello-world")

# 2. 用装饰器定义一个 Tool
@app.tool()
async def hello(name: str = "World") -> str:
    """向指定的人打招呼。当用户想问候或打招呼时使用。"""
    return f"你好,{name}!欢迎使用你的第一个 MCP Server。"

# 3. 启动 Server,用 stdio 通信
if __name__ == "__main__":
    import asyncio
    asyncio.run(stdio_server(app))

“就这么几行?”老李盯着屏幕,保温杯端在半空。

“就这么几行。@app.tool() 装饰器自动把函数签名转成 JSON Schema,函数名字 hello 变成工具名,文档字符串 """向指定的人打招呼...""" 变成工具描述,参数 name: str 自动生成 inputSchema。你完全不需要手写任何 JSON。”

老李若有所思:“所以我写一个普通的 Python 函数,加个装饰器,它就自动变成一个 MCP Tool 了?”

“对。而且参数类型、默认值、描述——全自动提取。这比你自己手写 JSON Schema 省了至少二十行代码。”小王催促道,“跑一下试试?”

老李在终端输入 python hello_server.py,Server 启动了,在 stdio 上等待请求。“但现在没 Client 连它,怎么验证?”

“先放着,等下直接在 Claude Desktop 里验证。继续写正题——天气查询。”


第 12-22 分钟:天气查询 Tool

“现在写个真正有用的——天气查询。我们用一个免费的天气 API 做后端。”小王打开一个公开的天气 API 地址。

老李在 hello_server.py 基础上扩展,添加了第二个工具:

python
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server

app = Server("weather-service")

@app.tool()
async def hello(name: str = "World") -> str:
    """向指定的人打招呼。当用户想问候或打招呼时使用。"""
    return f"你好,{name}!欢迎使用你的第一个 MCP Server。"

@app.tool()
async def get_weather(city: str) -> str:
    """查询指定城市的实时天气。当用户询问天气、气温、是否下雨时使用。
    
    Args:
        city: 城市名称,如"北京"、"上海"、"杭州"
    """
    # 调用免费天气 API(示例,替换成你的实际 API)
    url = f"https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": await get_city_lat(city),
        "longitude": await get_city_lon(city),
        "current": "temperature_2m,weather_code"
    }
    async with httpx.AsyncClient() as client:
        resp = await client.get(url, params=params, timeout=10)
        data = resp.json()
    
    temp = data["current"]["temperature_2m"]
    code = data["current"]["weather_code"]
    weather_desc = {0: "晴", 1: "多云", 2: "阴", 3: "小雨"}.get(code, "未知")
    
    return f"{city}当前天气:{weather_desc},气温 {temp}°C"

# 辅助函数:城市名→经纬度(简化,实际用地理编码 API)
async def get_city_lat(city: str) -> float:
    cities = {"北京": 39.9, "上海": 31.2, "杭州": 30.3, "深圳": 22.5}
    return cities.get(city, 39.9)

async def get_city_lon(city: str) -> float:
    cities = {"北京": 116.4, "上海": 121.5, "杭州": 120.2, "深圳": 114.1}
    return cities.get(city, 116.4)

if __name__ == "__main__":
    import asyncio
    asyncio.run(stdio_server(app))

“注意看 get_weather 的文档字符串,”小王指着屏幕,“当用户询问天气、气温、是否下雨时使用——这句话是给 AI 看的。它帮助 AI 判断什么时候该调用这个工具。描述越精准,AI 用对工具的概率越高。”

老李点头:“那参数描述 city: 城市名称,如"北京"、"上海"、"杭州" 也会自动变成 Schema?”

“对。MCP SDK 会解析你的类型注解和文档字符串,自动生成 inputSchema。如果参数是枚举类型,你还可以用 typing.Literal["北京","上海"] 来限制取值范围,Schema 会更精确。”


第 22-28 分钟:加上 Resources 和 Prompts

“只有 Tools 不够完整,”小王提醒道,“MCP Server 还可以提供 Resources 和 Prompts。各加一个,凑齐三件套。”

老李在同一个文件里继续添加:

python
# 添加一个静态 Resource:支持的城市列表
from mcp.server.resources import Resource

@app.resource("cities://supported")
async def get_supported_cities() -> str:
    """返回当前支持查询天气的城市列表"""
    return "支持的城市:北京、上海、杭州、深圳"

# 添加一个 Prompt 模板:天气报告
@app.prompt()
async def weather_report(city: str) -> str:
    """生成指定城市的天气报告提示词模板"""
    return f"""请根据 {city} 的天气信息,生成一份简洁的天气报告。
报告需要包含:
1. 当前天气状况和气温
2. 出行建议(是否需要带伞、穿衣建议)
3. 未来趋势简述(根据当前天气推测)
"""

“Resource 和 Prompt 的定义方式跟 Tool 几乎一样,”小王解释道,“@app.resource() 需要指定一个 URI,@app.prompt() 返回一个提示词模板字符串。Client 连上你的 Server 后,会自动发现它们。”

老李看了看表——28 分钟。一个包含 Tools、Resources、Prompts 三种能力的 MCP Server,全部代码不到 60 行。

图:weather-service MCP Server 的完整能力清单


第 28-30 分钟:接入 Claude Desktop

“最后一步——让 Claude 用上你的 Server。”小王打开 Claude Desktop 的配置文件。

json
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/path/to/weather_server.py"]
    }
  }
}

“就改个 JSON?”老李难以置信。

“就改个 JSON。Claude Desktop 启动时会读取这个配置,为每个 Server 启动一个子进程,通过 stdio 通信。它会自动发送 tools/listresources/listprompts/list 请求,获取你的 Server 的全部能力。”

老李重启 Claude Desktop,在对话框里输入:“北京今天天气怎么样?”

Claude 的回复里出现了工具调用的标志——它自动调用了 get_weather("北京"),拿到结果后回复:“北京当前天气晴,气温 25°C,适合出行。”

老李靠在椅背上,盯着屏幕沉默了好一会儿。然后他缓缓拧开保温杯,枸杞的香气弥漫开来。

“28 分钟。”小王笑着按下计时器,“你还欠我一周咖啡——哦不对,是我赌赢了,你欠我什么?”

“嗯……还有点意思。”老李端起保温杯,喝了一口。“下不为例啊。对了——你刚才说 Tools 的描述要写得精准,如果我有十个工具,怎么保证 AI 不会选错?有没有什么编写描述的黄金模板?给我看看你用的提示词。”

小王把椅子拉近,打开一个笔记文档。窗外夕阳正好,老李的屏幕上那个 60 行的 weather_server.py,安安静静地躺在文件夹里,像一个刚被赋予生命的小程序,等待着 AI 的第一次召唤。

MCP Resources:把数据"喂"给 AI 的正确姿势
MCP 架构解密:那些你看不见的 Client 和 Server