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 接口。

GET /api/weather?city=北京
→ { "temp": 26, "humidity": 60 }

前端发起 HTTP 请求,你的 Controller 处理逻辑,返回 JSON——这条路你走了十年,闭着眼睛都能写。

MCP 干的事一模一样,只是调用方从浏览器换成了 AI。

Claude 想查天气 → MCP 协议 → 你的 MCP Server → 返回天气数据

架构拆开来看:

┌──────────────────┐                      ┌──────────────────┐
│   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 以下跑不了,后面踩坑会说),装一个包:

pip install mcp

就一个依赖。FastMCP 把协议细节全封好了,你只关心工具函数怎么写。

核心代码

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")

就这么几行。拆一下:

  1. FastMCP("weather-server") —— 创建一个 MCP Server 实例,名字随便起。
  2. @mcp.tool() —— 装饰器注册工具,跟 Spring 的 @GetMapping 一个意思。
  3. 函数签名 (city: str) -> str —— 参数类型和返回值 MCP 框架会自动转成 JSON Schema,AI 客户端用它来判断什么时候调用。
  4. 函数的 docstring —— 这个不是注释,是 AI 看的东西。写什么、怎么写,直接决定 AI 调不调用你的工具。后面专门说。
  5. mcp.run(transport="stdio") —— 启动服务。stdio 走标准输入输出,本机调试用;部署到服务器换成 transport="sse"

三十行不到,一个能跑的 MCP Server 就出来了。

再加一个工具:网页搜索

只有一个工具太单薄。再加一个搜索工具,让 AI 能查实时信息:

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 内置了调试模式:

python server.py --debug

或者在代码里用 mcp dev 命令启动一个本地测试界面,直接在浏览器里手动调工具、看返回结果。这个调试界面还显示了 AI 看到的工具 Schema——你会看到你的 docstring 是怎么被转成 JSON Schema 的,参数类型、返回值描述一目了然。这时候如果发现 Schema 不对,回去改 docstring 就行。

测通了再接 Claude,省得配完配置才发现 Server 起不来,两头排查更费劲。


接 Claude:让 AI 真正用起来

服务写好了,接下来让 Claude 认识它。

Claude Desktop 配置

macOS 上配置文件在这里:

~/Library/Application Support/Claude/claude_desktop_config.json

打开加上这一段:

{
  "mcpServers": {
    "weather-server": {
      "command": "python",
      "args": ["/Users/tangyuewei/mcp-server/server.py"]
    }
  }
}

command 是启动命令,args 是脚本路径。保存,重启 Claude Desktop。

重启后,Claude 对话框左下角会出现一个锤子图标——点开能看到你的两个工具:get_weathersearch_web,描述就是你写的 docstring。这时候你问 Claude “北京今天天气怎么样”,它会自动调你的 get_weather

Claude Code 配置

如果你用的是 Claude Code(终端里的 Claude),配法更简单。在项目根目录加一个 .mcp.json

{
  "mcpServers": {
    "weather-server": {
      "command": "python",
      "args": ["server.py"]
    }
  }
}

然后在 Claude Code 里 /mcp 就能看到连接状态。绿点代表连上了,可以调。

HTTP 部署

如果你想把服务部署到服务器上,让团队共用,把 stdio 换成 SSE:

if __name__ == "__main__":
    mcp.run(transport="sse", host="0.0.0.0", port=8080)

客户端配置对应改成 URL:

{
  "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_weatherfetch_wx 好,因为 AI 能直接从名字推断功能。如果你用缩写或项目内部代号当工具名,AI 看不懂,调用率就低。工具名 + 描述一起决定了 AI 的调用意愿。

坑二:Python 版本

MCP 官方依赖要求 Python 3.10+。macOS 自带的 Python 3.9 跑不了。我第一次装了之后启动报 TypeError,查了半小时才反应过来是版本问题。

装个 pyenv 切到 3.12 解决:

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 拓展到全栈的工程师。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐