跳到主要内容

第 3 章:一次工具调用的完整链路

本章端到端追一次 tools/call:从客户端请求进网络,到你的函数返回。这是理解 FastMCP「运行期」的主线,也是依赖注入和错误处理的所在。

3.1 起点:低层 SDK 收到 JSON-RPC

FastMCP 不自己解析 JSON-RPC——它复用官方 MCP Python SDK 的 LowLevelServer(FastMCP 在 server/low_level.py:157 对它做了子类扩展)。启动时,_setup_handlers(server/mixins/mcp_operations.py:54)把 FastMCP 的处理函数用 SDK 装饰器接上去:

# server/mixins/mcp_operations.py:54 _setup_handlers —— 接线
self._mcp_server.list_tools()(self._list_tools_mcp)
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(self._call_tool_mcp)
self._mcp_server.read_resource()(self._read_resource_mcp)
self._mcp_server.get_prompt()(self._get_prompt_mcp)

所以当 tools/call 到达时,SDK 会调 FastMCP 的 _call_tool_mcp,后者转调公开 API call_tool

3.2 主干:call_tool

公开入口是 server/server.py:1200(async def call_tool)。它在一个 Context 上下文里跑完整条链。整体结构:

call_tool(name, arguments)


① 建立 Context(async with Context(fastmcp=self))


② 若 run_middleware=True → 进中间件洋葱链
│ 洋葱最内层再回调 call_tool(run_middleware=False)

③ get_tool(name) ← 经 providers 聚合定位工具(找不到再试 hash 名分发)


④ tool._run(arguments) ← 执行(下节)


⑤ try/except 错误分类 + 遮蔽

中间件回调的巧思(server/server.py:1263): 中间件链的最内层 call_next 又指回 call_tool,但这次带 run_middleware=False,避免无限套娃。也就是「同一个方法,第一次带中间件、第二次不带」。

3.3 中间件洋葱怎么组装

中间件链在 server/server.py:507(_run_middleware)构建,是经典的洋葱包裹:

# server/server.py:507 _run_middleware —— 反向包裹成洋葱
chain = call_next
for mw in reversed(self.middleware): # 反着遍历
next_chain = chain
async def wrapped(context, mw=mw, call_next=next_chain):
return await mw(context, call_next) # 每层包住下一层
chain = wrapped
return await chain(context)

每个中间件是 Middleware(server/middleware/middleware.py:88),按 MCP 方法分派到 on_call_tool / on_list_tools / on_read_resource 等钩子(middleware.py:168 起)。内置的有鉴权、限流、缓存、计时、日志等(server/middleware/ 目录)。第 6 章细讲。

3.4 定位工具:providers 聚合

get_tool(name) 走的是 provider 聚合逻辑(第 4 章主题):FastMCP 是个 AggregateProvider,它按顺序问每个 provider「你有这个工具吗」,先返回非 None 的赢。装饰器注册的工具在 LocalProvider 里,静态组件总是优先于动态 provider。

如果常规名字找不到,还有第二条路:哈希名分发(server/server.py:1288 起)——某些后端工具用 <hash>_<局部名> 格式暴露,靠反查哈希表定位。这是给「挂载/代理」场景准备的路由后门。

3.5 执行:校验与调用合一

找到工具后调 tool._run(arguments)(tools/base.py:372)。它先看要不要走后台任务(check_background_task),否则调 run()。对 FunctionTool 而言,run()tools/function_tool.py:385,核心在 _execute(:436):

# tools/function_tool.py:436 _execute —— 校验即执行
if exec_is_async:
result = type_adapter.validate_python(arguments) # 校验 + 得到协程
elif self.run_in_thread:
# 同步函数:丢线程池,避免阻塞事件循环
result = await call_sync_fn_in_threadpool(type_adapter.validate_python, arguments)
else:
result = type_adapter.validate_python(arguments)
...
if inspect.isawaitable(result):
result = await result # 等待异步结果

这里就是第 2 章埋的伏笔落地: type_adapter.validate_python(arguments) 一步同时做「校验入参」和「调用函数」——因为这个 adapter 是套在函数上的。校验失败抛 pydantic 错误,校验通过就等于函数已被调用。

同步函数默认丢线程池(run_in_thread=True),避免一个慢的同步工具卡死整个 async 事件循环——这是 async 框架的必修课。

3.6 依赖注入:ContextDepends

工具函数怎么拿到「与客户端对话的能力」?靠依赖注入。有两种写法,最终走同一条路径:

# 示意,非源码。两种拿 Context 的写法
@mcp.tool
async def a(x: int, ctx: Context) -> str: # 写法一:类型注解
await ctx.info("hi")
return str(x)

@mcp.tool
async def b(x: int, ctx: Context = CurrentContext()) -> str: # 写法二:显式依赖
...

统一发生在两步:

  1. 注册期 transform_context_annotations(server/dependencies.py:180)扫描签名,把「类型是 Context 但没有依赖默认值」的参数,自动补上 CurrentContext() 默认值——于是写法一被改写成写法二。这段还小心地重排了参数(带默认值的要排在无默认值之后),处理各种参数种类(dependencies.py:229 起)。
  2. 运行期 without_injected_parameters(server/dependencies.py:539)生成一个 wrapper:它的对外签名剥掉了注入参数(所以模型不用填),被调用时内部 resolve_dependenciesContextDepends() 的值解析好再喂给真函数(dependencies.py:588async def wrapper)。

为什么统一成 Depends: FastMCP 有两套 DI 传统——老的「ctx: Context 类型注解」和新的「Depends() 显式声明」。把前者在注册期改写成后者,就只需要维护一条解析路径。transform_context_annotations 的 docstring 明说了这个动机。

3.7 错误分类:不是所有异常都一样

call_tooltry/except(server/server.py:1310 起)对异常做了细致分类,这是生产级框架的讲究:

异常类型处理意图
ValidationError(参数校验失败)记 warning,原样抛这是客户端的错(传了坏参数),不是服务器 bug
FastMCPError按其 log_level 记录后抛框架已知的领域错误
httpx 429 / 超时转成友好 ToolError(「被限流,请重试」)让大模型看到可操作的提示
其它 Exception_mask_error_details 打开 → 遮蔽成通用消息默认隐藏内部细节,防信息泄露

错误类型体系在 exceptions.py:FastMCPError 下有 ValidationErrorToolErrorResourceErrorPromptErrorAuthorizationError,另有 NotFoundErrorDisabledError

默认遮蔽错误详情(_mask_error_details)是重要的安全默认:工具内部抛的原始异常消息(可能含路径、密钥、SQL)默认不回传给客户端,只回一句通用错误——除非你显式关掉遮蔽。

代码地图

主题文件路径符号名
低层 SDK 服务器fastmcp_slim/fastmcp/server/low_level.pyLowLevelServer
协议处理函数接线fastmcp_slim/fastmcp/server/mixins/mcp_operations.py_setup_handlers_call_tool_mcp
调用主干fastmcp_slim/fastmcp/server/server.pyFastMCP.call_tool
中间件洋葱fastmcp_slim/fastmcp/server/server.pyFastMCP._run_middleware
执行入口fastmcp_slim/fastmcp/tools/base.pyTool._run
校验即执行fastmcp_slim/fastmcp/tools/function_tool.pyFunctionTool._executeFunctionTool.run
Context 注解改写fastmcp_slim/fastmcp/server/dependencies.pytransform_context_annotations
依赖解析 wrapperfastmcp_slim/fastmcp/server/dependencies.pywithout_injected_parametersresolve_dependencies
错误类型体系fastmcp_slim/fastmcp/exceptions.pyFastMCPErrorToolErrorValidationErrorNotFoundError