1. 为什么我决定把 REST API 封装成 MCP 工具
先说个场景。我的个人知识库里一直有个小工具,通过 REST API 拉取天气数据,配合定时任务做天气播报。原本这套东西跑得好好的,直到我把它接入 AI 助手后,问题来了——模型根本不知道该怎么调这个 API。
你可能会说,把 API 文档喂给模型不就行了?试过,效果很差。模型面对一堆 URL、请求头、参数说明,经常构造出错误的请求,要么少了必填参数,要么把经纬度格式写错,更别提处理错误响应了。这时候我注意到 MCP(Model Context Protocol)——它正好解决"模型如何规范地调用外部工具"这个问题。
后来我又连着封装了几个内部 REST API,像查工单状态、读数据库表结构的接口也都用同样的方式接进了 MCP。跑通之后最大的感受是:这不是一个"锦上添花"的优化,而是"雪中送炭"的刚需。所以这篇文章就用天气服务当例子,把从 REST API 到 MCP Server 的完整封装思路掰开揉碎讲清楚。适合正在做 AI 应用接入的开发者,也适合想把自己手头 API 接进 Claude、Codex 等工具链的朋友。
1.1 MCP 到底解决了什么问题
MCP 本质上是一个标准化协议,规定了模型如何发现工具、如何调用工具、如何接收结果。它不是某个公司的私有方案,而是一套开放规范。你可以把它理解成 AI 世界的"USB 接口"——过去每个设备都要自己接一堆线,MCP 统一之后即插即用。
对比一下传统方式:
| 接入方式 | 模型侧体验 | 开发者侧工作 |
|---|---|---|
| 纯Prompt描述API | 每次都要重新理解文档,容易出错 | 文档要写得极其详细,仍难覆盖边界 |
| 自建Function Calling框架 | 需要自己处理工具注册、参数校验、结果解析 | 换一个模型就要适配一套框架,重复劳动 |
| MCP工具 | 模型通过统一协议自动发现并调用工具 | 写一次工具定义,所有支持MCP的客户端都能复用 |
我踩过坑才明白,Function Calling 虽然解决了"模型能调用函数"的问题,但各家实现不互通。MCP 相当于把"函数定义"和"传输方式"也标准化了,不同客户端之间可以共享同一个 Server。
1.2 为什么不直接给模型看 OpenAPI 文档
有人会问:现在的模型不是能读 JSON Schema 吗?直接把 OpenAPI 规范丢给它不就行了?
理论上可以,实际很痛苦。OpenAPI 文档动辄几千行,模型在上下文中"理解"这些内容会消耗大量 token,而且容易遗漏细节。天气服务这种还算简单的,要是内部系统有几十个接口,模型根本记不住。
封装成 MCP 工具后,工具的描述、参数结构都是由代码生成的 Schema,模型只需要按照工具名和参数槽来调用,完全不用关心底层的 HTTP 细节。上下文里只暴露"有用的部分",效率和准确率都上来了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先想清楚:天气服务 API 的选型与结构拆解
封装的第一步不是写代码,而是把你要封装的 REST API 彻底吃透。我用的是 Open-Meteo 作为示例,因为它免费、无需 API Key、接口设计也规范,非常适合演示。如果你用的是和风天气、高德天气或者其他私有 API,思路完全一致,只需要替换请求逻辑。
2.1 天气 API 的核心要素
以 Open-Meteo 为例,它的核心接口是:
bash复制GET https://api.open-meteo.com/v1/forecast
必填参数就两个:latitude 和 longitude,可选参数有 current_weather、hourly、daily 等,按需拼进 Query String。
实际返回的 JSON 长这样(节选):
json复制{
"latitude": 39.9,
"longitude": 116.4,
"current_weather": {
"temperature": 21.3,
"windspeed": 11.2,
"weathercode": 2,
"time": "2025-06-08T10:00"
}
}
这里有几个封装时要特别注意的点:
- **天气代码(weathercode)**是数字,0 表示晴天,1 表示基本晴,2 表示多云,3 表示阴天,61、63、65 表示不同程度的降雨。不转换的话,模型拿到的就是干巴巴的数字,没法自然回答用户"今天天气怎么样"。
- 单位不统一,温度默认是摄氏度,风速默认是 km/h,不同 API 可能不一样,封装时要明确告诉模型单位,或者在工具内部做转换。
我在第一次封装时没做 weathercode 的转换,结果模型回答"当前天气代码为2"这种鬼话,后来加了字典映射才正常。这类"看起来小但影响体验"的细节,在封装时尤其值得花精力处理。
2.2 确定 MCP 工具的最小粒度
封装粒度怎么定?我的原则是:一个工具对应一个用户心智中的"能力",而不是对应一个 REST 端点。
比如天气服务的 REST API 本身只有一个 /forecast 端点,但用户问"现在热不热"和"明天会不会下雨",心智上是两个不同的能力。如果只暴露一个"查询天气"工具,模型要自己从返回的一大坨数据里提取答案,容易出错。
所以我拆成了两个工具:
| 工具名 | 功能描述 | 对应REST请求 |
|---|---|---|
get_current_weather |
查询某地当前天气(温度、天气状况、风速) | ?latitude=..&longitude=..¤t_weather=true |
get_weather_forecast |
查询未来N天天气预报(最高/最低温、降水概率) | ?latitude=..&longitude=..&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max&forecast_days=N |
灵"粒度太粗模型用不好,太细则工具数量爆炸"的两难,我倾向于拆成动词 + 场景的组合。比如这里是"当前"和"预报"两个场景,以后你封装电商 API 时,可以拆成"查订单状态""创建订单""取消订单"等粒度,每个都对应一个用户可理解的动作。
3. 用 Python 构建 MCP Server 的完整步骤
现在进入正题。我用的技术栈是 Python + fastmcp 库,相比官方 mcp 库,它封装了更多便捷的装饰器语法,编写效率高不少。如果你是第一次接触,这个库绝对值得一试。
3.1 环境准备
bash复制mkdir weather-mcp
cd weather-mcp
python -m venv .venv
source .venv/bin/activate
pip install fastmcp httpx
我习惯用 httpx 而不是 requests,因为 httpx 原生支持异步,MCP Server 并发请求时会舒服很多。fastmcp 的底层支持 httpx 的异步客户端,搭配使用非常顺手。
3.2 实现天气工具
创建 weather_server.py:
python复制from fastmcp import FastMCP
import httpx
from typing import Literal
mcp = FastMCP("Weather Server")
# 天气代码解释映射
WEATHER_CODE_MAP = {
0: "晴朗",
1: "基本晴朗",
2: "多云",
3: "阴天",
45: "雾",
48: "雾凇",
51: "毛毛雨",
53: "小毛毛雨",
55: "大毛毛雨",
61: "小雨",
63: "中雨",
65: "大雨",
71: "小雪",
73: "中雪",
75: "大雪",
80: "小阵雨",
81: "中阵雨",
82: "大阵雨",
95: "雷阵雨",
}
def _parse_weather_code(code: int) -> str:
return WEATHER_CODE_MAP.get(code, f"未知天气代码({code})")
def _call_weather_api(params: dict) -> dict:
"""调用 Open-Meteo 并返回原始 JSON,附错误处理。"""
base_url = "https://api.open-meteo.com/v1/forecast"
params = {k: v for k, v in params.items() if v is not None}
resp = httpx.get(base_url, params=params, timeout=10.0)
resp.raise_for_status()
return resp.json()
@mcp.tool()
def get_current_weather(
latitude: float,
longitude: float,
) -> str:
"""获取指定纬度、经度的当前天气信息。
参数直接使用 WGS84 坐标系的十进制度数,纬度范围 -90 到 90,经度范围 -180 到 180。
返回温度(摄氏度)、天气状况、风速(km/h)、风向等信息。
"""
data = _call_weather_api({
"latitude": latitude,
"longitude": longitude,
"current_weather": "true",
})
current = data.get("current_weather", {})
code = current.get("weathercode", -1)
desc = _parse_weather_code(code)
return (
f"位置坐标: {latitude}, {longitude}\n"
f"温度: {current.get('temperature', 'N/A')}°C\n"
f"天气状况: {desc}\n"
f"风速: {current.get('windspeed', 'N/A')} km/h\n"
f"风向: {current.get('winddirection', 'N/A')}°\n"
f"观测时间(UTC): {current.get('time', 'N/A')}"
)
@mcp.tool()
def get_weather_forecast(
latitude: float,
longitude: float,
forecast_days: int = 3,
) -> str:
"""获取指定纬度、经度的未来天气预报。
forecast_days 范围为 1 到 7,默认返回未来 3 天。
返回每天的最高温、最低温、天气状况、最大降水概率。
"""
days = min(max(forecast_days, 1), 7)
data = _call_weather_api({
"latitude": latitude,
"longitude": longitude,
"daily": ",".join([
"weathercode",
"temperature_2m_max",
"temperature_2m_min",
"precipitation_probability_max",
]),
"forecast_days": str(days),
"timezone": "auto",
})
daily = data.get("daily", {})
output = f"位置坐标: {latitude}, {longitude},未来 {days} 天预报:\n"
for i in range(len(daily.get("time", []))):
code = daily["weathercode"][i]
desc = _parse_weather_code(code)
output += (
f"- {daily['time'][i]}: "
f"最高 {daily['temperature_2m_max'][i]}°C, "
f"最低 {daily['temperature_2m_min'][i]}°C, "
f"{desc}, 降水概率 {daily['precipitation_probability_max'][i]}%\n"
)
return output
if __name__ == "__main__":
mcp.run()
这段代码的核心思路,是把 REST API 的"查询-解析-格式化" 三段式逻辑全部封装在工具函数内部。模型只传递语义化的参数(比如经纬度),而返回的是一段人类可读的文本,模型可以直接引用回答用户,不需要再做二次解析。
3.3 参数 Schema 自动生成的关键细节
很多文章会在这一步强调"写工具描述",但我想多说一层:fastmcp 会根据函数签名自动生成 JSON Schema,函数名、参数名、类型注解、docstring 都会反映到 Schema 中。所以:
- 函数名要动词开头,比如
get_current_weather,不要用weather这种模糊的名字。 - 参数名要直观,
latitude比lat好,因为模型的常识里更熟悉后者这个说法,而且很多 API 也用latitude,保持一致能降低模型的误解。 - docstring 里要写明参数格式、范围、单位。不要写"获取天气"这种废话,要写"纬度的范围是-90到90"这类模型无法自行推断的信息。
- 返回值尽量是格式化好的字符串。虽然 MCP 也支持返回 JSON,但让模型直接引用自然语言结论,比让它读原始结构再组织语言更稳定。
3.4 运行与接入客户端
脚本运行有两种模式:
bash复制# 方式一:Stdio 模式,适合本地接入 Claude Desktop、Windsurf 等
python weather_server.py
# 方式二:HTTP + SSE 模式,适合远程部署,让多个客户端共享同一个 Server
python weather_server.py --transport sse
实际使用中,如果我开发机上有 claude CLI,可以直接在配置里指向这个脚本。如果用的是 SSE 模式,客户端配置里填 http://localhost:8000/sse 就行。我在本地测试时更喜欢 Stdio 模式,启动快,日志也直接打到标准输出,排查问题非常直观。
4. 让模型记住你封装了什么:命名、描述与返回格式的调优
封装本身不难,难在让模型在正确的时候调用正确的工具,并且调用得有模有样。这一步极其依赖工具定义的质量。我这部分内容是经验之谈,试过很多错误写法,才总结出几条真正生效的原则。
4.1 工具描述的措辞决定模型能不能理解
写工具描述有一个朴素的准则:像给一个没有见过该 API 的同事写交接说明。不要写"查询天气数据",要写清楚:
- 这个工具能解决什么问题("获取某个经纬度坐标点当前的温度、天气状况、风速")
- 哪些场景适合用它("用户询问此刻的天气时使用")
- 哪些场景不适合("查询未来天气预报请使用 get_weather_forecast")
我在 docstring 里就会把这些信息全部写上。fastmcp 会把描述与函数源码一并传给客户端侧的模型,描述写得具体时,模型判断"该用哪个工具"的准确率明显更高。
4.2 返回格式要"向着用户说话"
另一个容易忽视的是返回格式的设计。我一开始图省事,直接返回原始 JSON,结果模型经常对着 JSON 结构愣半天,或者把字段名字串在回答里。后来我把返回改成自然语言模板,模型几乎一句废话没有,直接就能说"北京当前 21.3°C,多云,风速 11.2 km/h"。
这里建议做一行"返回格式说明"放在 docstring 里,比如:
code复制返回一段完整的自然语言描述,包含温度、天气状况、风速,不要输出 JSON。
这样会把"模型如何转述"这件事的概率收敛很多。
4.3 多工具之间的歧义处理
如果你同时封装了 get_current_weather 和 get_weather_forecast,模型可能混淆。我的经验是,在各自的 docstring 开头加一句"仅用于当前天气"或"仅用于未来多天预报",并在描述中强调参数 forecast_days 的含义。
更进一步,可以在工具内部做防御性校验。比如 forecast_days 超出范围时,不是报错,而是钳制到合法区间并返回"已自动调整为 N 天"。模型拿到结果后会自己明白,比硬报错更有利于对话继续。
4.4 参数校验:宁可钳制,不可抛错
REST API 对非法参数往往是返回 4xx,但 MCP 工具的最佳实践是尽量不抛异常。因为模型在对话历史上看到多次报错,会倾向于不再调用工具,甚至给出错误提示,这对用户体验伤害很大。
我在代码里写的 days = min(max(forecast_days, 1), 7) 就是钳制策略。如果 API 对 latitude 严格要求范围,我也会先校验再给一个友好提示。这类"让模型一次调用就成功"的设计,比你在调用侧做无数兜底都管用。
5. 完整 RCP 工具封装流程中踩过的坑与排查记录
顺着封装过程,我把实际调试中遇到的高频问题和排查思路整理成一份速查表,每一条都是我或身边朋友真实跑过的场景,而不是文档里的理论。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 客户端显示"工具未找到" | Server 未成功启动,或传输模式不匹配 | 先命令行运行脚本,确认无报错;检查客户端配置的启动命令路径是否绝对路径 |
| 模型调用工具后返回近似的空结果 | 请求超时或目标 API 偶尔 5xx | 在工具内部加超时时间,并对 HTTP 异常做重试(最多2次) |
| 参数总是传错,比如经纬度反了 | docstring 里没说明坐标格式和范围 | 在 docstring 第一行用人类语言写明"纬度在前,经度在后,范围分别是..." |
| 返回的中文乱码 | Stdio 模式下编码不一致 | 确保 Python 脚本以 UTF-8 输出,某些 Windows 环境需 sys.stdout.reconfigure(encoding='utf-8') |
| 多个客户端连不上同一个 Server | SSE 模式未配置 CORS,或被反代拦了 | 确认 MCP 的 SSE 端口可以被访问,必要时用 Nginx 反代并配置好 Cross-Origin |
其中编码问题在 Windows 上特别常见。有一次我在 Windows 上调试,天气描述全是乱码,查了半小时才发现是 PowerShell 的默认代码页问题,把输出重定向编码设置成 UTF-8 就解决了。这类平台问题不建议硬啃,直接参考社区里成熟的修复脚本即可。
5.2 调试 MCP Server 的三个实用技巧
第一,用 MCP Inspector 做可视化调试。运行 fastmcp run weather_server.py 后,可以在本地启动一个调试面板,手动调用工具、查看完整请求和返回,比在聊天窗口里等模型调用高效太多。我在写新工具的每个阶段都会先打开 Inspector,确认工具输出符合预期,再接入真实客户端。
第二,在 Server 端打个日志钩子。复杂项目里,我经常在 _call_weather_api 里插一行 print(f"[DEBUG] calling API with params: {params}")。Stdio 模式下这行日志会输出到启动脚本的终端,能看到每次模型调用工具时实际请求的是什么,排查"参数被模型理解错"这类问题非常关键。
第三,构造"既合法又刁钻"的测试用例。封装完后列几个特殊输入,比如城市不在经典坐标点、经纬度为 0,0、forecast_days=100。这类边角参数的调用成功与否,最能验证你的钳制与容错逻辑是否真的到位。
5.3 网络环境与依赖库版本
httpx.get 默认跟随重定向、自动解压缩,但要注意目标 API 的可用性。在国内网络环境下,访问 Open-Meteo 这类境外服务可能会有偶尔的延迟或连接不稳定。实用做法是:
- 在 Server 外层设置合理的请求超时(我通常用 10 秒)。
- 内部做一层指数退避重试,比如失败后等 0.5 秒、再等 1 秒,最多 3 次。
- 接口返回异常时,工具统一返回"暂时无法获取天气数据,请稍后再试",不要让模型把这个错误当成真实天气。
另外,fastmcp 库更新比较勤快,如果你发现某些装饰器行为异常,先去 GitHub 看 release note。我有一次被 mcp.run() 的参数变化坑过,后来直接锁定版本号写进了 requirements.txt,才彻底消停。
6. 从天气服务推广开:万物皆可 MCP 的做事心法
封装完天气服务后,我心里最大的改变是:任何外部系统的能力,都可以用一种"面向模型"的方式重新暴露一遍。这也是我标题里"万物皆可 MCP"想表达的意思。
6.1 哪些场景最适合接 MCP
我归纳了四类最有即战力价值的场景:
- 查询型API:查天气、查汇率、查股票、查订单状态。这类数据获取逻辑简单,封装成本低,回报率最高。
- 企业系统内部的工具:查工单、查客户信息、生成报表。MCP 能让 AI 直接帮你操作内部系统,省去一堆 RPA 脚本。
- 运维和开发工具链:查服务器状态、读日志、执行脚本。这类工具接入后,可以让 AI 承担一部分重复巡检工作。
- 知识库或多源数据读取:比如查数据库表结构、读取指定文件。MCP 工具的输入输出足够规范,模型用起来非常顺手。
反之,不适合封装的是那些需要长时间运行、重度人工确认、或者存在复杂状态机的流程。比如"审批流程""多步交易"这类,强行封装成单个工具,反而让模型难以把控边界。
6.2 封装的通用流程模板
我在多个项目里复用了一套固定流程,拿任何 API 都能快速落地,现在分享给你:
- 梳理端点清单:把要封装的 API 全部列出来,记录方法、路径、必填/可选参数、返回结构。
- 确定工具粒度:按"用户心智能力"拆分,而不是按端点拆分。
- 写工具函数骨架:每个工具内部做"请求-解析-格式化-异常兜底"四件事。
- 写人类可读的 docstring:包含场景、参数范围、单位、返回格式约定。
- 用 Inspector 调试:手动调用每个工具,验证输出。
- 接入真实客户端测试:跑一段真实对话,让模型自然调用工具,观察效果。
- 补充防御性校验:根据测试中暴露的问题,加上钳制、重试、超时控制。
6.3 安全和权限方面的几个提醒
MCP 工具能让模型直接触达后端服务,安全边界必须想清楚。
- 工具最小权限原则:不要一个工具函数既读数据又写数据,拆开。
- 敏感信息过滤:返回内容里不要带 API Key、内部 IP、数据库连接串等。
- 访问控制:如果 Server 以 SSE 模式暴露,一定要设置鉴权或只监听内网地址,别裸奔在公网上。
- 审计日志:在关键工具内部记录调用时间、入参来源、返回状态。出了问题能追责。
我最初封装的内部工单 API 就是没做鉴权,后来被同事开玩笑"谁都可以让 AI 查任意订单",虽然只是内部实验环境,但也算给我敲了警钟。
6.4 后续扩展思路
如果你感兴趣,这个天气 Server 还能继续往三个方向扩展:
- 增加地理编码工具:让模型先根据城市名查出经纬度,再调用天气工具,就能让模型处理"北京明天天气"这种非坐标提问。
- 接入消息推送:把天气工具的异常结果,通过 Webhook 发到钉钉或飞书,做一个天气异常提醒的自动化流程。
- 多 Server 聚合:在客户端侧配置多个 MCP Server,一个管天气、一个管日历、一个管邮件,让模型自由跨工具组合调用,实现"帮我看看明天天气适不适合去郊游,顺便在主日歷上建个提醒"这类复合任务。
我实际跑过聚合多个 Server 的方案,模型会在不同工具之间自动切换。那种体验,已经挺接近科幻片里"和个人助理对话"的感觉了。
7. 封装 MCP 工具时的心态与工作方式
讲完技术,最后聊点更感性但同样重要的东西。我封装过十几个工具后最大的体会是:写 MCP Server 并不难,难的是站在"一个什么都不知道的模型"的视角思考。
模型的常识和人类不一样,它可能理解"北京"但不懂"39.9, 116.4"这两个数字怎么来;它可能认识"weathercode=2"但不知道这代表多云。你要做的,就是把这些"人类默认懂、模型却不知道"的隐性信息,全部显式地写进工具的描述和返回值里。
这一点和写优秀函数注释的准则很像:写"为什么"而不是写"是什么"。工具描述里说清楚"什么场景会用到这个工具,参数代表什么业务含义,返回值应该被如何引用"——它执行的效果就会跟那些只写了两句话的工具有天壤之别。
我自己在新建工具前有个固定习惯:先拿一页纸,把"用户会问什么、这个工具如何回答、回答里要包含哪些必要的字段"画出来,再去写代码。代码只是这个思考过程的外化,这样设计出来的工具,模型几乎每次都能准确命中。
这篇文章从头到尾的代码和思路,来自我其中一个周末的折腾记录。如果你也正在封装自己的第一个 MCP Server,建议从小而具体的 API 开始,跑通全流程后再追求复杂场景。我始终觉得,MCP 最有魅力的地方,不是某个库的某个特性,而是它让"AI 真正理解并使用你手头的 API"这件事,变成了一个标准化、可复用、人人都能上手的过程。你的任何 Rest API,都值得被这样优雅地唤醒。
