FastMCP源码深度解析:一个@tool()装饰器背后到底发生了什么?
关键词:FastMCP、MCP、Python、@tool、装饰器、JSON Schema、AI Agent、Model Context Protocol、源码解析
在前面的文章中,我们已经学会了使用FastMCP开发自己的MCP Server。
例如,只需要几行代码:
from mcp.server.fastmcp import FastMCP app = FastMCP("Demo") @app.tool() def weather(city: str): """查询天气""" return f"{city}:晴天,28℃" app.run()Claude、Cursor就能够自动发现这个工具。
很多开发者都会产生疑问:
为什么加了一个
@tool(),AI就知道这个函数可以调用了?
今天,我们不再停留在"会用"的层面,而是深入FastMCP的实现原理,从源码设计角度理解@tool()背后发生的一切。
一、先理解Python装饰器
很多人第一次接触:
@app.tool()觉得十分神奇。
其实它本质上还是Python装饰器。
例如:
def logger(func): def wrapper(*args, **kwargs): print("开始执行") result = func(*args, **kwargs) print("执行结束") return result return wrapper然后:
@logger def hello(): print("Hello")真正执行的是:
hello = logger(hello)FastMCP采用的是同样的思想。
二、@tool()并不是执行工具,而是注册工具
很多人误以为:
@app.tool()会立即执行函数。
实际上:
不会。
它真正做的是:
把这个函数注册到MCP Server。
可以理解成:
weather() ↓ 注册 ↓ Tool Registry(工具注册表)例如:
FastMCP内部通常会维护一个工具集合(不同版本实现细节可能略有不同):
self.tools = {}每注册一个Tool:
@app.tool() def weather():最终都会进入:
Tool Registry ↓ weather calculator database github以后。
客户端获取工具列表。
实际上读取的就是:
Tool Registry三、源码第一步:接收Python函数
装饰器拿到的是:
整个Python函数对象。
例如:
@app.tool() def weather(city: str): return "Sunny"实际上:
weather本身就是:
Python对象。
因此。
FastMCP可以直接获得:
function.__name__得到:
weather再获得:
function.__doc__得到:
查询天气所以:
根本不用开发者:
重复写配置。
四、第二步:解析函数签名
接下来。
FastMCP会读取:
Python类型提示。
例如:
def weather( city: str, days: int ):利用:
inspect.signature()可以得到:
city ↓ str days ↓ int最终得到:
{ "city":"string", "days":"integer" }所以:
MCP协议里面:
参数。
其实来自:
Python类型。
而不是:
开发者手写JSON。
五、第三步:自动生成JSON Schema
MCP协议要求:
每个Tool。
必须提供:
{ "inputSchema": {} }FastMCP根据函数签名自动构建Schema。
例如:
def add( a:int, b:int )生成:
{ "type":"object", "properties":{ "a":{ "type":"integer" }, "b":{ "type":"integer" } }, "required":[ "a", "b" ] }这一步非常关键。
因为:
LLM真正理解工具。
依赖的是:
Schema。
不是Python代码。
六、第四步:封装成Tool对象
接下来。
FastMCP会把所有信息:
封装。
例如:
Tool ├── name ├── description ├── schema ├── function └── metadata以后。
真正执行的时候。
实际上调用的是:
tool.function(...)所以:
Tool。
只是:
函数。
加上一层:
描述信息。
七、第五步:注册到Server
然后:
加入:
FastMCP ↓ tools ↓ Dictionary例如:
{ "weather":Tool(), "query_db":Tool(), "email":Tool() }以后:
客户端请求:
tools/listFastMCP直接:
遍历。
整个Dictionary。
返回即可。
八、AI为什么能够看到这些Tool?
很多人认为:
AI会扫描Python代码。
实际上:
不会。
真正流程:
Cursor ↓ 连接MCP ↓ tools/list ↓ 返回JSON ↓ LLM阅读JSON ↓ 决定调用哪个Tool例如:
客户端收到:
[ { "name":"weather", "description":"查询天气" }, { "name":"mysql" } ]LLM看到:
weather就知道:
可以查询天气。
所以:
真正看的:
一直都是:
JSON。
九、真正调用时发生了什么?
例如:
用户:
北京今天多少度?
LLM:
输出:
{ "tool":"weather", "arguments":{ "city":"北京" } }然后:
FastMCP:
收到:
weather ↓ 找到Tool对象 ↓ 找到function ↓ 执行 ↓ weather("北京")得到:
晴天返回:
LLM。
整个调用流程如下:
用户输入 ↓ LLM分析 ↓ 生成Tool Call ↓ FastMCP路由 ↓ Python函数执行 ↓ 返回结果 ↓ LLM组织语言整个过程中。
LLM从来没有:
直接执行Python。
十、为什么FastMCP开发效率这么高?
因为:
大量重复工作:
SDK都帮我们完成了。
例如:
以前。
开发Tool。
需要:
写JSON ↓ 写Schema ↓ 写RPC ↓ 注册Tool ↓ 处理协议 ↓ 解析参数现在:
开发者:
只写:
@app.tool() def weather(): ...剩下:
FastMCP全部自动完成。
包括:
Tool注册
参数解析
Schema生成
协议封装
请求分发
返回结果格式化
开发者可以把精力集中在业务逻辑,而不是协议细节上。
十一、企业项目中的最佳实践
在企业开发中,@tool()不仅仅是"把函数暴露给AI"这么简单,还应遵循一些设计原则。
1. 一个Tool只做一件事
推荐:
@app.tool() def query_order(order_id: str): ...不推荐:
@app.tool() def system_manager(action: str): ...职责越单一,模型越容易正确选择。
2. 提供清晰的Docstring
例如:
@app.tool() def query_inventory(product_id: str): """查询指定商品的库存数量"""比:
"""查询"""效果更好。
3. 做好参数校验
不要假设模型一定会传入正确参数。
建议:
检查参数是否为空
校验数据类型
验证业务规则
返回规范错误信息
这样能提高Tool的稳定性。
4. 不要在Tool中混杂复杂业务
推荐:
@app.tool() def query_customer(customer_id: str): return customer_service.query(customer_id)把真正的业务逻辑放到Service层,Tool仅作为AI调用入口。
总结
一个简单的:
@app.tool()背后其实完成了一整套自动化流程:
Python函数 ↓ 装饰器接管 ↓ 读取函数信息 ↓ 解析参数类型 ↓ 生成JSON Schema ↓ 创建Tool对象 ↓ 注册到Tool Registry ↓ 响应tools/list ↓ AI发现工具 ↓ LLM生成Tool Call ↓ Python函数执行 ↓ 返回结果这也是FastMCP最吸引人的地方:
它把MCP协议中繁琐的注册、Schema生成、协议封装和请求分发全部隐藏起来,让开发者可以像写普通Python函数一样开发AI工具。
理解了@tool()背后的工作机制后,你会发现:FastMCP并没有"魔法",它只是巧妙地利用了Python装饰器、类型注解、反射机制和MCP协议,将普通函数转换成了AI可以理解和调用的标准化工具。
本文知识点
✅ Python装饰器原理
✅ FastMCP Tool注册机制
✅inspect.signature()参数解析
✅ JSON Schema自动生成
✅ Tool Registry设计思想
✅ MCP工具发现流程
✅ 企业级Tool设计最佳实践
