周五下午四点,老李把 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 模式,不需要装额外的。”
老李敲下命令:
pip install mcp“就一个包?”老李有点不敢相信。
“就一个包。MCP 的 Python SDK 把 JSON-RPC 协议、stdio 通信、能力协商全封装了。你只需要从 mcp.server 里继承一个类,写你自己的业务逻辑。”
安装完毕。老李看了看表——才过了三分钟。
第 5-12 分钟:Hello MCP——第一个 Tool
“先写一个最简单的 Server,让它提供一个 hello 工具。”小王把代码模板投影到屏幕上。
老李新建一个文件,跟着敲起来:
# 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 基础上扩展,添加了第二个工具:
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。各加一个,凑齐三件套。”
老李在同一个文件里继续添加:
# 添加一个静态 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 的配置文件。
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/weather_server.py"]
}
}
}“就改个 JSON?”老李难以置信。
“就改个 JSON。Claude Desktop 启动时会读取这个配置,为每个 Server 启动一个子进程,通过 stdio 通信。它会自动发送 tools/list、resources/list、prompts/list 请求,获取你的 Server 的全部能力。”
老李重启 Claude Desktop,在对话框里输入:“北京今天天气怎么样?”
Claude 的回复里出现了工具调用的标志——它自动调用了 get_weather("北京"),拿到结果后回复:“北京当前天气晴,气温 25°C,适合出行。”
老李靠在椅背上,盯着屏幕沉默了好一会儿。然后他缓缓拧开保温杯,枸杞的香气弥漫开来。
“28 分钟。”小王笑着按下计时器,“你还欠我一周咖啡——哦不对,是我赌赢了,你欠我什么?”
“嗯……还有点意思。”老李端起保温杯,喝了一口。“下不为例啊。对了——你刚才说 Tools 的描述要写得精准,如果我有十个工具,怎么保证 AI 不会选错?有没有什么编写描述的黄金模板?给我看看你用的提示词。”
小王把椅子拉近,打开一个笔记文档。窗外夕阳正好,老李的屏幕上那个 60 行的 weather_server.py,安安静静地躺在文件夹里,像一个刚被赋予生命的小程序,等待着 AI 的第一次召唤。

