在MCP中,Tools是一个函数,它可以被用于执行动作或者访问外部系统。
一、如何创建一个Tools?
最简单的方法就是用@mcp.tool注解一个python的函数。
服务端:
fromfastmcpimportFastMCP mcp=FastMCP(name="CalculatorServer")@mcp.tooldefadd(a:int,b:int)->int:"""Adds two integer numbers together."""returna+bif__name__=="__main__":mcp.run(transport="http",# 使用HTTP传输host="0.0.0.0",# 允许外部访问port=8000# 端口)带有 * args 或 **kwargs 的函数不支持作为工具。存在这一限制是因为 FastMCP 需要为 MCP 协议生成完整的参数 schema,而这对于可变参数列表来说是无法实现的。
客户端
importasynciofromfastmcpimportClient client=Client("http://localhost:8000/mcp")asyncdefcall_tool(name:str):asyncwithclient:result=awaitclient.call_tool(name,{"a":2,"b":3})print(result)asyncio.run(call_tool("add"))装饰器参数说明
- name str | None 设置通过 MCP 暴露的明确工具名称。如果未提供,则使用函数名称
- description str | None 提供通过 MCP 公开的描述。如果设置了此描述,那么函数的文档字符串将为此目的而被忽略。
- tags set[str] | None 一组用于对工具进行分类的字符串。服务器以及在某些情况下的客户端可以使用这些字符串来筛选或分组可用的工具。
- enabled bool default:“True” 一个用于启用或禁用该工具的布尔值。
二、同步与异步
FastMCP 是一个优先支持异步的框架,它能无缝支持异步(async def)和同步(def)函数作为工具。对于 I/O 密集型操作,异步工具是更优选择,可保持服务器的响应性。
虽然同步工具在 FastMCP 中能无缝运行,但在执行过程中可能会阻塞事件循环。对于 CPU 密集型或可能存在阻塞的同步操作,可考虑其他策略。一种方法是使用 anyio(FastMCP 内部已在使用)将它们包装为异步函数。
同步示例在第一章节已经有了,这里举一个异步的例子
服务端
importasyncioimportanyiofromfastmcpimportFastMCP mcp=FastMCP()defcpu_intensive_task(data:str)->str:# Some heavy computation that could block the event loopreturndata@mcp.toolasyncdefwrapped_cpu_task(data:str)->str:"""CPU-intensive task wrapped to prevent blocking."""returnawaitanyio.to_thread.run_sync(cpu_intensive_task,data)# 服务启动入口asyncdefmain():# 方式1:启动 FastMCP 本地服务(默认基于 HTTP 或 WebSocket,取决于 FastMCP 版本)# 如需自定义端口/地址,可传入参数,例如:host="0.0.0.0", port=8000awaitmcp.run_http_async(host="127.0.0.1",# 绑定本地地址,外部访问可改为 0.0.0.0port=8000,# 服务端口)if__name__=="__main__":asyncio.run(main())客户端
importasynciofromfastmcpimportClient client=Client("http://localhost:8000/mcp")asyncdefcall_tool(name:str):asyncwithclient:result=awaitclient.call_tool("wrapped_cpu_task",{"data":name})print(result)if__name__=="__main__":asyncio.run(call_tool("apple"))三、类型声明
FastMCP 支持多种类型注解,包括所有的 Pydantic 类型, 在定义Tool函数的时候,建议添加上类型:
比如:name: str, [str] 就是类型声明。
| Type Annotation | Example | Description |
|---|---|---|
| Basic types | int, float, str, bool | Simple scalar values |
| Binary data | bytes | Binary content (raw strings, not auto-decoded base64) |
| Date and Time | datetime, date, timedelta | Date and time objects (ISO format strings) |
| Collection types | list[str], dict[str, int], set[int] | Collections of items |
| Optional types | float | None, Optional[float] |
| Union types | str | int, Union[str, int] |
| Constrained types | Literal[“A”, “B”], Enum | Parameters with specific allowed values |
| Paths | Path | File system paths (auto-converted from strings) |
| UUIDs | UUID | Universally unique identifiers (auto-converted from strings) |
| Pydantic models | UserData | Complex structured data with validation |
四、验证模式
在LLM调用Tools的时候,需要传参,默认情况下,FastMCP 采用 Pydantic 的灵活验证机制,会将兼容的输入强制转换为与类型注解匹配的形式。这提高了与大型语言模型客户端的兼容性,这些客户端可能会发送值的字符串表示形式(例如,对于整数参数发送 “10”)。
如果需要更严格的验证以拒绝任何类型不匹配的情况,您可以启用严格输入验证。严格模式使用 MCP SDK 内置的 JSON 模式验证,在将输入传递给函数之前,根据精确的模式对其进行验证:
4.1 宽松的验证
Tools定义的类型是int, 在调用的时候,传str类型,MCP自动转换int类型。
服务端
fromfastmcpimportFastMCP mcp=FastMCP("StrictServer",strict_input_validation=False)@mcp.tooldefadd_numbers(a:int,b:int)->int:"""Add two numbers."""returna+bif__name__=="__main__":mcp.run(transport="http",# 使用HTTP传输host="0.0.0.0",# 允许外部访问port=8000# 端口)客户端
importasynciofromfastmcpimportClient client=Client("http://localhost:8000/mcp")asyncdefcall_tool(name:str):asyncwithclient:result=awaitclient.call_tool(name,{"a":"2","b":"3"})print(result)asyncio.run(call_tool("add_numbers"))4.2 严格的验证
服务端
fromfastmcpimportFastMCP mcp=FastMCP("StrictServer",strict_input_validation=True)@mcp.tooldefadd_numbers(a:int,b:int)->int:"""Add two numbers."""returna+bif__name__=="__main__":mcp.run(transport="http",# 使用HTTP传输host="0.0.0.0",# 允许外部访问port=8000# 端口)客户端
importasynciofromfastmcpimportClient client=Client("http://localhost:8000/mcp")asyncdefcall_tool(name:str):asyncwithclient:result=awaitclient.call_tool(name,{"a":"2","b":"3"})print(result)asyncio.run(call_tool("add_numbers"))调用的时候,将会报异常:
Traceback(most recent call last):File"D:\code\mcp-demo\tools\flexible_client.py",line17,in<module>asyncio.run(call_tool("add_numbers"))~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^File"D:\Python\Python313\Lib\asyncio\runners.py",line195,inrunreturnrunner.run(main)~~~~~~~~~~^^^^^^File"D:\Python\Python313\Lib\asyncio\runners.py",line118,inrunreturnself._loop.run_until_complete(task)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^File"D:\Python\Python313\Lib\asyncio\base_events.py",line725,inrun_until_completereturnfuture.result()~~~~~~~~~~~~~^^File"D:\code\mcp-demo\tools\flexible_client.py",line14,incall_tool result=awaitclient.call_tool(name,{"a":"2","b":"3"})^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^File"D:\code\mcp-demo\.venv\Lib\site-packages\fastmcp\client\client.py",line969,incall_toolraiseToolError(msg)fastmcp.exceptions.ToolError:Input validation error:'3'isnotoftype'integer'五、参数元数据
可以通过多种方式提供有关参数的额外元数据:
5.1 简单字符串描述
fromtypingimportAnnotated@mcp.tooldefprocess_image(image_url:Annotated[str,"URL of the image to process"],resize:Annotated[bool,"Whether to resize the image"]=False,width:Annotated[int,"Target width in pixels"]=800,format:Annotated[str,"Output image format"]="jpeg")->dict:"""Process an image with optional resizing."""# Implementation...5.2 使用Field进行描述
方式一: 在Annotated中使用Field
fromtypingimportAnnotatedfrompydanticimportField@mcp.tooldefprocess_image(image_url:Annotated[str,Field(description="URL of the image to process")],resize:Annotated[bool,Field(description="Whether to resize the image")]=False,width:Annotated[int,Field(description="Target width in pixels",ge=1,le=2000)]=800,format:Annotated[Literal["jpeg","png","webp"],Field(description="Output image format")]="jpeg")->dict:"""Process an image with optional resizing."""# Implementation...方式二:将 Field 用作默认值,不过更推荐使用 Annotated 方法:
@mcp.tooldefsearch_database(query:str=Field(description="Search query string"),limit:int=Field(10,description="Maximum number of results",ge=1,le=100))->list:"""Search the database with the provided query."""# Implementation...5.3 对LLM隐藏参数
要在运行时注入值而不将其暴露给 LLM(例如用户 ID、凭据或数据库连接),请使用带有 Depends () 的依赖注入。使用 Depends () 的参数会自动从工具架构中排除:
fromfastmcpimportFastMCPfromfastmcp.dependenciesimportDepends mcp=FastMCP()defget_user_id()->str:return"user_123"# Injected at runtime@mcp.tooldefget_user_details(user_id:str=Depends(get_user_id))->str:# user_id is injected by the server, not provided by the LLMreturnf"Details for{user_id}"六、返回值
FastMCP 工具可以以两种互补的格式返回数据:传统内容块(如文本和图像)和结构化输出(机器可读取的 JSON)。当你添加返回类型注释时,FastMCP 会自动生成输出模式来验证结构化数据,并使客户端能够将结果反序列化为 Python 对象。
6.1 Content
FastMCP 会自动将工具返回值转换为适当的 MCP 内容块:
| 返回类型 | 转换后的MCP内容块 |
|---|---|
| str | Sent as TextContent |
| bytes | Base64 encoded and sent as BlobResourceContents (within an EmbeddedResource) |
| fastmcp.utilities.types.Image | Sent as ImageContent |
| fastmcp.utilities.types.Audio | Sent as AudioContent |
| fastmcp.utilities.types.File | Sent as base64-encoded EmbeddedResource |
示例
fromfastmcp.utilities.typesimportImage,Audio,File@mcp.tooldefget_chart()->Image:"""Generate a chart image."""returnImage(path="chart.png")@mcp.tooldefget_multiple_charts()->list[Image]:"""Return multiple charts."""return[Image(path="chart1.png"),Image(path="chart2.png")]注意事项(以上类型转换的条件)
- 直接返回
- 作为List的一部分返回
- 其他情况需要手动转换
# ✅ Automatic conversionreturnImage(path="chart.png")return[Image(path="chart1.png"),"text content"]# ❌ Will not be automatically convertedreturn{"image":Image(path="chart.png")}# ✅ Manual conversion for nested usereturn{"image":Image(path="chart.png").to_image_content()}6.2 结构化输出
当你的工具返回具有 JSON 对象表示形式的数据时,FastMCP 会自动创建与传统内容并存的结构化输出。这提供了机器可读取的 JSON 数据,客户端可以将其反序列化为 Python 对象。
自动结构化内容规则:
- 类对象结果(dict、Pydantic 模型、dataclasses)→ 始终成为结构化内容(即使没有输出模式)
- 非对象结果(int、str、list)→ 只有存在用于验证 / 序列化它们的输出模式时,才会成为结构化内容
- 所有结果 → 为了向后兼容,始终成为传统内容块
6.2.1 返回类对象(dict、Pydantic 模型、dataclasses)
当你的工具返回字典、数据类或 Pydantic 模型时,FastMCP 会自动从中创建结构化内容。这种结构化内容包含实际的对象数据,便于客户端反序列化为原生对象。
返回
@mcp.tooldefget_user_data(user_id:str)->dict:"""Get user data."""return{"name":"Alice","age":30,"active":True}结构化返回
{"content":[{"type":"text","text":"{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}"}],"structuredContent":{"name":"Alice","age":30,"active":true}}6.2.2返回非对象结果(int、str、list)
6.2.2.1 不带类型注解(没有说明函数返回的类型)
返回
@mcp.tooldefcalculate_sum(a:int,b:int):"""Calculate sum without return annotation."""returna+b# Returns 8实际结果
CallToolResult(content=[TextContent(type='text',text='5',annotations=None,meta=None)],structured_content=None,meta=None,data=None,is_error=False)6.2.2.2 带类型注解(没有说明函数返回的类型)
返回
@mcp.tooldefcalculate_sum(a:int,b:int)->int:"""Calculate sum without return annotation."""returna+b# Returns 8实际结果
CallToolResult(content=[TextContent(type='text',text='5',annotations=None,meta=None)],structured_content={'result':5},meta=None,data=5,is_error=False)