1. 从一次工具注册失败说起:FastMCP 工具管理到底管什么
如果你正在把本地脚本、内部 API 或者一堆零散函数封装成 MCP 工具,多半会遇到这几个问题:函数写好了但客户端list_tools看不到、参数校验报一堆 Pydantic 警告、工具里想打日志却拿不到请求上下文、返回一个 dict 客户端解析出来结构对不上。这些都不是业务逻辑的问题,而是 FastMCP 的工具管理没配对。
FastMCP 是 MCP 生态里上手成本比较低的服务器框架,它把「函数注册成工具」这件事收敛到了@tool装饰器上,把「工具执行时能拿到什么」收敛到了Context上下文注入,把「返回什么格式」收敛到了结构化输出配置。这三块合起来就是工具管理的核心。适合谁看:已经写过一两个 MCP 工具、但工具一多就开始乱、想搞清楚注册机制和返回结构的开发者。下面我会给一套可复制的服务骨架,把@tool注册、Context注入、结构化输出三件事串起来,最后用客户端实际调用验证工具列表和返回结构。
2. 前置准备:TaoToken 接入与 FastMCP 环境
2.1 为什么这里要提 TaoToken
FastMCP 本身是服务器框架,但你在本地调试工具时,往往需要一个能稳定调用模型的入口来做端到端验证,比如让模型决定调用哪个工具。TaoToken 提供统一的 API 入口,兼容常见的模型调用方式,适合放在调试链路里。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个不加 UTM)。你需要先去控制台拿一个 API Key,后面客户端验证时会用到。
2.2 安装依赖
FastMCP 的包名是fastmcp,建议用虚拟环境隔离:
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install fastmcp pydantic装完之后确认版本,@tool的参数在不同小版本里有细微差异:
python -c "import fastmcp; print(fastmcp.__version__)"2.3 目录结构
我习惯把工具和服务入口分开,工具一多不至于全堆在一个文件里:
fastmcp-demo/ ├── server.py # FastMCP 实例与启动入口 ├── tools/ │ ├── __init__.py │ ├── math_tools.py # 示例工具 │ └── text_tools.py └── client_test.py # 客户端验证脚本3. 可复制配置:@tool 注册、Context 注入与结构化输出
3.1 服务骨架
先写server.py,把 FastMCP 实例建好,工具模块导入进来触发注册:
# server.py from fastmcp import FastMCP mcp = FastMCP( name="demo-tools", instructions="演示 FastMCP 工具管理:注册、上下文、结构化输出", ) # 导入即注册,装饰器在模块加载时执行 from tools import math_tools, text_tools # noqa: E402,F401 if __name__ == "__main__": mcp.run()这里有个容易踩的点:@tool装饰器是在模块被导入时执行的,所以工具模块必须被 import 一次,否则list_tools里什么都没有。很多人把工具写在server.py里没事,一拆文件就忘了导入。
3.2 @tool 装饰器注册
@tool支持同步和异步函数,框架通过inspect.iscoroutinefunction自动识别执行模式。常用参数对照如下:
| 参数 | 作用 | 默认值 |
|---|---|---|
name | 工具的程序化名称 | 函数名 |
title | UI 展示用的可读标题 | 无 |
description | 工具功能描述 | 函数 docstring |
annotations | 附加注解信息 | 无 |
structured_output | 是否强制结构化输出 | None(自动检测) |
写一个带完整元数据的工具:
# tools/math_tools.py from fastmcp import FastMCP from pydantic import BaseModel, Field mcp = FastMCP(name="demo-tools") class AddResult(BaseModel): """加法结果的结构化模型""" sum: int = Field(description="两数之和") expression: str = Field(description="原始表达式") @mcp.tool( name="add_numbers", title="整数加法", description="对两个整数求和,返回结构化结果", ) def add_numbers(a: int, b: int) -> AddResult: """计算 a + b 并返回结构化结果。""" return AddResult(sum=a + b, expression=f"{a} + {b}")注意装饰器必须写成@mcp.tool()带括号的形式。如果写成@mcp.tool,框架会抛TypeError,因为它期望先调用再返回装饰器。这个错误信息不算特别直观,第一次遇到容易懵。
3.3 Context 上下文注入
想让工具里能打日志、报进度、读资源,就在函数签名里加一个Context类型注解的参数。框架的find_context_parameter会用typing.get_type_hints解析签名,找到这个参数后在Tool.run里作为关键字参数注入。它支持Optional[Context]这类泛型写法。
# tools/text_tools.py import asyncio from fastmcp import FastMCP from fastmcp.server.context import Context mcp = FastMCP(name="demo-tools") @mcp.tool( name="summarize_text", title="文本摘要(模拟)", description="模拟一个耗时任务,演示进度上报与日志", ) async def summarize_text(text: str, ctx: Context) -> dict: """对文本做模拟摘要,过程中上报进度。""" await ctx.info(f"收到文本长度: {len(text)}") total = 3 for i in range(1, total + 1): await asyncio.sleep(0.2) await ctx.report_progress(progress=i, total=total, message=f"第 {i} 步") await ctx.debug("摘要流程结束") return {"summary": text[:20] + "...", "steps": total}Context提供的能力包括debug/info/warning/error日志方法、report_progress进度上报、read_resource资源访问、elicit用户交互,以及request_id、client_id属性。这些只在请求处理期间有效,别把ctx存到全局变量里跨请求用。
3.4 结构化输出配置
结构化输出由structured_output参数控制,三种模式:None按返回类型注解自动检测,True强制创建结构化工具,False无条件非结构化。返回类型到输出模型的映射规则大致是:
| 返回类型注解 | 输出模型处理方式 |
|---|---|
BaseModel子类 | 直接使用该类 |
str/int等基本类型 | 包装进含result字段的模型 |
TypedDict | 转换为 Pydantic 模型 |
list/dict等泛型 | 包装进result字段 |
上面add_numbers返回AddResult,框架会直接用它作为outputSchema。而summarize_text返回dict,会被包装成{"result": {...}}的结构。如果你希望客户端拿到扁平结构,就显式定义BaseModel返回类型,别偷懒返回裸 dict。
4. 验证请求:启动服务并检查工具列表与返回结构
4.1 启动服务
python server.py默认走 stdio 传输,日志会打到 stderr。看到类似Starting MCP server 'demo-tools'就说明起来了。
4.2 客户端验证工具列表
写一个client_test.py,用 FastMCP 自带的客户端连上去:
# client_test.py import asyncio from fastmcp import Client async def main(): async with Client("server.py") as client: tools = await client.list_tools() for t in tools: print(f"- {t.name} | {t.title} | schema={bool(t.outputSchema)}") result = await client.call_tool("add_numbers", {"a": 3, "b": 4}) print("add_numbers ->", result) result2 = await client.call_tool("summarize_text", {"text": "FastMCP 工具管理实战"}) print("summarize_text ->", result2) if __name__ == "__main__": asyncio.run(main())运行后你应该看到两个工具都出现在列表里,add_numbers的outputSchema为真,summarize_text返回的是带result字段的包装结构。如果list_tools是空的,先回去检查工具模块有没有被 import。
4.3 用模型驱动工具调用做端到端验证
工具列表对了,再验证模型能不能正确选工具。把 TaoToken 的 API Key 配到环境变量:
export TAOTOKEN_API_KEY="你的key"然后在客户端里把工具暴露给模型,让它根据用户问题决定调用哪个工具。这一步能验证的不只是工具注册,还有description和参数 schema 是否足够清晰——模型选错工具,八成是描述写得太含糊。
5. 本篇常见错排查
5.1 工具没出现在 list_tools
最常见的原因是工具模块没被导入。@tool是导入时执行的副作用,拆文件后忘了 import 就静默失败。其次是装饰器写成了@mcp.tool而不是@mcp.tool(),这种情况通常会抛TypeError,但如果你在异常处理里吞掉了就看不到。
5.2 同名工具被静默忽略
ToolManager内部维护一个_tools字典,add_tool时用get检查同名。如果warn_on_duplicate_tools为 True,会记录警告但不覆盖已有工具,优先保留先注册的。想更新工具必须先remove_tool再重新注册。这个策略保证了注册顺序的确定性,但也意味着你改了工具代码却没生效时,先怀疑是不是有同名旧工具占着位置。
5.3 参数校验报 Pydantic 警告
func_metadata会为参数创建ArgModelBase子类。如果参数名和BaseModel的属性冲突(比如叫schema、copy),框架会用别名规避,但会打警告。另外StrictJsonSchema会把 JSON Schema 生成过程中的警告直接转成异常,所以 schema 不合法时不是警告而是直接报错,看 traceback 里的 schema 字段定位。
5.4 Context 注入失败
find_context_parameter依赖类型注解解析。如果你用了from __future__ import annotations又没正确解析,或者参数注解写成了字符串但没在可解析作用域里,就找不到 Context 参数,ctx会变成必填参数导致调用报缺参。确保Context是从fastmcp.server.context正确导入的。
5.5 结构化输出结构对不上
返回裸dict会被包装成{"result": ...},客户端如果按扁平结构解析就会失败。要么在客户端按包装结构取,要么在服务端定义BaseModel返回类型。convert_result的行为还受structured_output配置影响,False时直接返回非结构化内容,True时返回非结构化加结构化的元组,调试时打印原始CallToolResult最直观。
6. 继续往下走
工具管理跑通之后,下一步通常是把它接到真实的编码或 Agent 流程里。如果你要长期跑编码类任务,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ;想先在对话里手动验证工具调用效果,用模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ;需要管理多个 Key 或查看用量,去控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ;Key 的创建和权限配置在 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ;接入细节和协议说明看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。工具注册这块,我的经验是先把description和返回类型写清楚,比事后调 schema 省事得多。