MCP协议实战:从零写个Agent工具
摘要:MCP 就是 AI 世界的 REST API------你写好服务,Claude 来调。前一篇聊了 AI Agent 的学习路线,这篇落到具体动作:用 Python 写一个 MCP Server,把自己的 API 包装成 Agent 能认识的标准工具。不讲协议规范,讲后端最关心的事------怎么让 AI 调你的服务。
每个大模型的工具调用方式都不一样。OpenAI 是 function calling,Claude 是 tool use,国产模型各有各的格式。做后端的人最烦这种事------同一个查天气的接口,接三个模型要写三套适配层。
举个例子。你用 Spring Boot 写了个天气查询接口,想让 AI 能调用。接 OpenAI,你得按它的 function calling 格式写 JSON Schema,字段名、参数类型全得按它的规范来。接 Claude,换一套 tool use 的定义格式,描述文案的写法也不一样。接国内的模型,又是一套。
最崩溃的不是第一次适配------是模型升级之后。OpenAI 改了个参数格式,你的适配层报错了。Claude 更新了 tool use 的限制,你之前能跑的工具现在调不起来了。三个模型三个迭代节奏,你一个人维护三套适配代码。
说实话,适配到想吐。
后来 Anthropic 搞了个 MCP(Model Context Protocol),把这事统一了。2024 年底提出,2025 年生态炸开,到 2026 年已经成了 Agent 工具调用的标配(来源:Anthropic 官方博客,2024年11月)。跟 HTTP 之于 Web 一样------大家都认这个协议,你写一次,所有支持 MCP 的模型都能调。
上篇聊了 AI Agent 的学习路线,这篇不聊概念,聊代码。
MCP 是什么,用后端的话说
你用 Spring Boot 写过一个 REST 接口。
bash
GET /api/weather?city=北京
→ { "temp": 26, "humidity": 60 }
前端发起 HTTP 请求,你的 Controller 处理逻辑,返回 JSON------这条路你走了十年,闭着眼睛都能写。
MCP 干的事一模一样,只是调用方从浏览器换成了 AI。
arduino
Claude 想查天气 → MCP 协议 → 你的 MCP Server → 返回天气数据
架构拆开来看:
arduino
┌──────────────────┐ ┌──────────────────┐
│ MCP Client │ JSON-RPC 2.0 │ MCP Server │
│ │ ◄──────────────────► │ │
│ Claude Desktop │ stdio / HTTP/SSE │ 你写的 Python │
│ Claude Code │ │ 服务 │
│ Cursor / Cline │ │ │
└──────────────────┘ └─────────┬────────┘
│
▼
┌─────────────────┐
│ 你的业务 API │
│ │
│ 天气接口 │
│ 数据库查询 │
│ 搜索引擎 │
└─────────────────┘
三个要点,用后端的类比就够了:
MCP Server = 你写的微服务。 用 @server.tool() 装饰器注册工具函数,跟 Spring 的 @RestController 一样------声明这个方法对外暴露,MCP 框架自动处理协议细节。
MCP Client = 调用方。 Claude Desktop、Claude Code、Cursor 这些 AI 工具都内置了 MCP 客户端。你配好 Server 地址,它们自动发现你的工具、看懂工具描述、在需要的时候调用。
通信协议 = JSON-RPC 2.0。 跟 REST 的 HTTP + JSON 一样,MCP 用 JSON-RPC 做序列化,支持 stdio(本地进程通信)和 HTTP/SSE(远程部署)。本机调试用 stdio,部署到服务器换成 SSE。
一句话:以前你写 REST API 给浏览器调,现在写 MCP Server 给 AI 调。换的是调用方,不换的是"定义接口 → 处理请求 → 返回结果"这套逻辑。
为什么要用 MCP 而不是直接把 API 丢给 AI?两个原因。第一,MCP 是标准协议。AI 客户端不用知道你的 API 地址、认证方式、参数格式------它只认 MCP 协议,你的 Server 负责把内部 API 包装成标准工具。第二,工具发现是自动的。你加了新工具,AI 自动感知;你改了工具描述,AI 下次调用就用新的描述。不用通知任何一个客户端。
动手写:天气查询 MCP Server
理论够了,写代码。
环境准备
Python 3.10+(3.10 以下跑不了,后面踩坑会说),装一个包:
bash
pip install mcp
就一个依赖。FastMCP 把协议细节全封好了,你只关心工具函数怎么写。
核心代码
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
async def get_weather(city: str) -> str:
"""查询指定城市的实时天气信息,返回温度、湿度、风向和天气概况"""
# 这里接真实的天气 API,示例用模拟数据
return f"{city}:晴,26°C,湿度 60%,北风 3 级"
if __name__ == "__main__":
mcp.run(transport="stdio")
就这么几行。拆一下:
FastMCP("weather-server")------ 创建一个 MCP Server 实例,名字随便起。@mcp.tool()------ 装饰器注册工具,跟 Spring 的@GetMapping一个意思。- 函数签名
(city: str) -> str------ 参数类型和返回值 MCP 框架会自动转成 JSON Schema,AI 客户端用它来判断什么时候调用。 - 函数的 docstring ------ 这个不是注释,是 AI 看的东西。写什么、怎么写,直接决定 AI 调不调用你的工具。后面专门说。
mcp.run(transport="stdio")------ 启动服务。stdio走标准输入输出,本机调试用;部署到服务器换成transport="sse"。
三十行不到,一个能跑的 MCP Server 就出来了。
再加一个工具:网页搜索
只有一个工具太单薄。再加一个搜索工具,让 AI 能查实时信息:
python
import httpx
@mcp.tool()
async def search_web(query: str) -> str:
"""在互联网上搜索指定关键词,返回前 3 条搜索结果摘要和链接。
适用于需要实时信息的问题,比如新闻、股价、最新动态。
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.search.example.com/v1/search",
params={"q": query, "limit": 3},
)
data = resp.json()
return "\n".join(
f"- {r['title']}: {r['snippet']} ({r['url']})" for r in data["results"]
)
两个工具注册完,你的 MCP Server 就有了两样本事:查天气、搜网页。AI 拿到这两个工具,碰到"今天北京热不热"会调天气工具,碰到"最近 AI 圈有什么新闻"会调搜索工具。
先自己测一下
接 Claude 之前,先确认 Server 本身能跑。FastMCP 内置了调试模式:
bash
python server.py --debug
或者在代码里用 mcp dev 命令启动一个本地测试界面,直接在浏览器里手动调工具、看返回结果。这个调试界面还显示了 AI 看到的工具 Schema------你会看到你的 docstring 是怎么被转成 JSON Schema 的,参数类型、返回值描述一目了然。这时候如果发现 Schema 不对,回去改 docstring 就行。
测通了再接 Claude,省得配完配置才发现 Server 起不来,两头排查更费劲。
接 Claude:让 AI 真正用起来
服务写好了,接下来让 Claude 认识它。
Claude Desktop 配置
macOS 上配置文件在这里:
javascript
~/Library/Application Support/Claude/claude_desktop_config.json
打开加上这一段:
json
{
"mcpServers": {
"weather-server": {
"command": "python",
"args": ["/Users/tangyuewei/mcp-server/server.py"]
}
}
}
command 是启动命令,args 是脚本路径。保存,重启 Claude Desktop。
重启后,Claude 对话框左下角会出现一个锤子图标------点开能看到你的两个工具:get_weather 和 search_web,描述就是你写的 docstring。这时候你问 Claude "北京今天天气怎么样",它会自动调你的 get_weather。
Claude Code 配置
如果你用的是 Claude Code(终端里的 Claude),配法更简单。在项目根目录加一个 .mcp.json:
json
{
"mcpServers": {
"weather-server": {
"command": "python",
"args": ["server.py"]
}
}
}
然后在 Claude Code 里 /mcp 就能看到连接状态。绿点代表连上了,可以调。
HTTP 部署
如果你想把服务部署到服务器上,让团队共用,把 stdio 换成 SSE:
python
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8080)
客户端配置对应改成 URL:
json
{
"mcpServers": {
"weather-server": {
"url": "http://your-server:8080/sse"
}
}
}
跟 REST 服务的部署套路一样------本机开发用 stdio,上线换成 HTTP。
踩过的坑
写了几个 MCP Server 之后,我把踩过的坑说三个。
坑一:工具描述写模糊了,AI 不调用
这个是最大的坑。@mcp.tool() 的 docstring,AI 是当"工具说明书"来读的。你说得越清楚,AI 越知道什么时候调。
我做过对比。描述写"查询天气信息"------调用率 70% 左右。改成了"查询指定城市的实时天气信息,返回温度、湿度、风向和天气概况"------调用率跳到 95% 以上。加了"适用于查询天气相关问题"这句使用场景描述后,误调用也少了。
为什么差这么多?因为 LLM 靠语义匹配决定调哪个工具。你写"查询天气",它只在用户明确说"天气"的时候调;你写了"温度、湿度、风向",用户说"今天热不热"它也能匹配上。
经验:docstring 写三行------第一行说功能,第二行说输入输出,第三行说适用场景。别省这个功夫,这是你跟 AI 之间唯一的接口文档。
还有一个容易被忽略的点:工具名也是有语义的。get_weather 比 fetch_wx 好,因为 AI 能直接从名字推断功能。如果你用缩写或项目内部代号当工具名,AI 看不懂,调用率就低。工具名 + 描述一起决定了 AI 的调用意愿。
坑二:Python 版本
MCP 官方依赖要求 Python 3.10+。macOS 自带的 Python 3.9 跑不了。我第一次装了之后启动报 TypeError,查了半小时才反应过来是版本问题。
装个 pyenv 切到 3.12 解决:
bash
pyenv install 3.12
pyenv local 3.12
pip install mcp
坑三:配置文件路径
Claude Desktop 的配置路径 macOS 和 Windows 不一样。macOS 是 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在 %APPDATA%\Claude\ 下面。网上很多教程只贴 mac 路径,用 Windows 的同事照着找半天找不到。
还有一个点:配置文件里的 args 必须是绝对路径,写相对路径 Claude Desktop 找不到。用 pwd 看完整路径再填进去。
后端看 MCP
写了一圈,回到一个朴素的问题:MCP 对后端工程师到底意味着什么。
它不是什么新东西。本质上跟你写了十年的 REST API 一样------定义接口、处理请求、返回结果。MCP 不过是把请求方从浏览器换成了 AI。
但换个角度想,这意味着你的代码能力直接延伸到了 AI 的世界。以前你写的接口只有前端能调------用户点按钮 → 前端发请求 → 后端处理。现在你写的接口 AI 也能调------用户说句话 → AI 理解意图 → AI 调你的工具 → 返回结果。这条链路上,你的代码是最后一公里------它连接着 AI 的理解能力和真实世界的业务数据。
回头看那个类比:MCP 就是 AI 世界的 REST API。 十年前后端学 REST,现在后端学 MCP------逻辑没变,能力范围变了。
有一件事值得多想一步。REST API 的时代,你的接口能力边界是前端决定的------前端能调什么,你的系统就能做什么。MCP 时代,你的接口能力边界是 AI 的理解能力决定的------AI 能理解用户的意图、能组合调用你的工具、能在多个工具之间做决策。同样是你的后端代码,在 REST 时代是被动等调用,在 MCP 时代是主动参与 AI 的推理链。
这不只是"换个调用方"的事。它意味着你写的每一个工具,都可能被 AI 以一种你没想到的方式组合使用------今天它拿天气工具查天气,明天可能用天气工具 + 搜索工具 + 数据库工具自动生成一份出差报告。工具的排列组合效应,是 MCP 区别于 REST 最本质的地方。
上篇说关注 MCP 和 A2A 两个协议,这篇算是把 MCP 从概念到代码走了一遍。A2A(Agent-to-Agent)是 Agent 之间的通信协议,场景不一样,后面再写。
tangyuewei,从后端出发,用 AI 拓展到全栈的工程师。