在MCP中,Tools是一个函数,它可以被用于执行动作或者访问外部系统。
一、如何创建一个Tools?
最简单的方法就是用@mcp.tool注解一个python的函数。
服务端:
python
from fastmcp import FastMCP
mcp = FastMCP(name="CalculatorServer")
@mcp.tool
def add(a: int, b: int) -> int:
"""Adds two integer numbers together."""
return a + b
if __name__ == "__main__":
mcp.run(
transport="http", # 使用HTTP传输
host="0.0.0.0", # 允许外部访问
port=8000 # 端口
)
带有 * args 或 **kwargs 的函数不支持作为工具。存在这一限制是因为 FastMCP 需要为 MCP 协议生成完整的参数 schema,而这对于可变参数列表来说是无法实现的。
客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call_tool(name: str):
async with client:
result = await client.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 内部已在使用)将它们包装为异步函数。
同步示例在第一章节已经有了,这里举一个异步的例子
服务端
python
import asyncio
import anyio
from fastmcp import FastMCP
mcp = FastMCP()
def cpu_intensive_task(data: str) -> str:
# Some heavy computation that could block the event loop
return data
@mcp.tool
async def wrapped_cpu_task(data: str) -> str:
"""CPU-intensive task wrapped to prevent blocking."""
return await anyio.to_thread.run_sync(cpu_intensive_task, data)
# 服务启动入口
async def main():
# 方式1:启动 FastMCP 本地服务(默认基于 HTTP 或 WebSocket,取决于 FastMCP 版本)
# 如需自定义端口/地址,可传入参数,例如:host="0.0.0.0", port=8000
await mcp.run_http_async(
host="127.0.0.1", # 绑定本地地址,外部访问可改为 0.0.0.0
port=8000, # 服务端口
)
if __name__ == "__main__":
asyncio.run(main())
客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call_tool(name: str):
async with client:
result = await client.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类型。
服务端
python
from fastmcp import FastMCP
mcp = FastMCP("StrictServer", strict_input_validation=False)
@mcp.tool
def add_numbers(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run(
transport="http", # 使用HTTP传输
host="0.0.0.0", # 允许外部访问
port=8000 # 端口
)
客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call_tool(name: str):
async with client:
result = await client.call_tool(name, {"a": "2", "b": "3"})
print(result)
asyncio.run(call_tool("add_numbers"))
4.2 严格的验证
服务端
python
from fastmcp import FastMCP
mcp = FastMCP("StrictServer", strict_input_validation=True)
@mcp.tool
def add_numbers(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run(
transport="http", # 使用HTTP传输
host="0.0.0.0", # 允许外部访问
port=8000 # 端口
)
客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call_tool(name: str):
async with client:
result = await client.call_tool(name, {"a": "2", "b": "3"})
print(result)
asyncio.run(call_tool("add_numbers"))
调用的时候,将会报异常:
python
Traceback (most recent call last):
File "D:\code\mcp-demo\tools\flexible_client.py", line 17, in <module>
asyncio.run(call_tool("add_numbers"))
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Python\Python313\Lib\asyncio\runners.py", line 195, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "D:\Python\Python313\Lib\asyncio\runners.py", line 118, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "D:\Python\Python313\Lib\asyncio\base_events.py", line 725, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "D:\code\mcp-demo\tools\flexible_client.py", line 14, in call_tool
result = await client.call_tool(name, {"a": "2", "b": "3"})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\code\mcp-demo\.venv\Lib\site-packages\fastmcp\client\client.py", line 969, in call_tool
raise ToolError(msg)
fastmcp.exceptions.ToolError: Input validation error: '3' is not of type 'integer'
五、参数元数据
可以通过多种方式提供有关参数的额外元数据:
5.1 简单字符串描述
python
from typing import Annotated
@mcp.tool
def process_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
python
from typing import Annotated
from pydantic import Field
@mcp.tool
def process_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 方法:
python
@mcp.tool
def search_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 () 的参数会自动从工具架构中排除:
python
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
mcp = FastMCP()
def get_user_id() -> str:
return "user_123" # Injected at runtime
@mcp.tool
def get_user_details(user_id: str = Depends(get_user_id)) -> str:
# user_id is injected by the server, not provided by the LLM
return f"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 |
示例
python
from fastmcp.utilities.types import Image, Audio, File
@mcp.tool
def get_chart() -> Image:
"""Generate a chart image."""
return Image(path="chart.png")
@mcp.tool
def get_multiple_charts() -> list[Image]:
"""Return multiple charts."""
return [Image(path="chart1.png"), Image(path="chart2.png")]
注意事项(以上类型转换的条件)
- 直接返回
- 作为List的一部分返回
- 其他情况需要手动转换
python
# ✅ Automatic conversion
return Image(path="chart.png")
return [Image(path="chart1.png"), "text content"]
# ❌ Will not be automatically converted
return {"image": Image(path="chart.png")}
# ✅ Manual conversion for nested use
return {"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 会自动从中创建结构化内容。这种结构化内容包含实际的对象数据,便于客户端反序列化为原生对象。
返回
python
@mcp.tool
def get_user_data(user_id: str) -> dict:
"""Get user data."""
return {"name": "Alice", "age": 30, "active": True}
结构化返回
python
{
"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 不带类型注解(没有说明函数返回的类型)
返回
python
@mcp.tool
def calculate_sum(a: int, b: int):
"""Calculate sum without return annotation."""
return a + b # Returns 8
实际结果
python
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 带类型注解(说明了函数返回的类型)
返回
python
@mcp.tool
def calculate_sum(a: int, b: int) -> int:
"""Calculate sum without return annotation."""
return a + b # Returns 8
实际结果
python
CallToolResult(
content=[TextContent(type='text', text='5', annotations=None, meta=None)],
structured_content={'result': 5},
meta=None,
data=5,
is_error=False)
6.2.3 带类型注解(返回类型是Dataclasses 和 Pydantic )
python
from dataclasses import dataclass
from fastmcp import FastMCP
mcp = FastMCP()
@dataclass
class Person:
name: str
age: int
email: str
@mcp.tool
def get_user_profile(user_id: str) -> Person:
"""Get a user's profile information."""
return Person(
name="Alice",
age=30,
email="alice@example.com",
)
实际结果
python
{
"content": [
{
"type": "text",
"text": "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"
}
],
"structuredContent": {
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
}
七、输出模式
输出模式是一种描述工具预期输出格式的新方式。当提供输出模式时,工具必须返回与该模式匹配的结构化输出。
7.1 基本类型包装
对于原始返回类型为int, str, bool的,FastMCP自动包装结果为:key为result的结构化输出。
原始返回
python
@mcp.tool
def calculate_sum(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
结构化输出
{
"result": 8
}
7.2 手动修改输出格式
可以提供一个自定义的output_schema覆盖自动生成的。
python
@mcp.tool(output_schema={
"type": "object",
"properties": {
"data": {"type": "string"},
"metadata": {"type": "object"}
}
})
def custom_schema_tool() -> dict:
"""Tool with custom output schema."""
return {"data": "Hello", "metadata": {"version": "1.0"}}
要注意的几个点
output_schema必须是"type": "object"- 如果提供了自定义的
output_schema, 那么Tool的结果必须返回匹配的结构化数据。 - 可以使用
ToolResult提供一个结构化的输出。
7.3 ToolResult和Metadata
若要完全掌控工具的响应,请返回一个 ToolResult 对象。这能让你对工具输出的所有方面进行明确控制:包括content、structured_content 和 meta。
python
from fastmcp.tools.tool import ToolResult
from mcp.types import TextContent
@mcp.tool
def advanced_tool() -> ToolResult:
"""Tool with full control over output."""
return ToolResult(
content=[TextContent(type="text", text="Human-readable summary")],
structured_content={"data": "value", "count": 42},
meta={"execution_time_ms": 145}
)
ToolResult接受的3个字段
content可以是一个string, (自动转换为TextContent),或者content的列表。content和structured_content至少要有一个。
python
# Simple string
ToolResult(content="Hello, world!")
# List of content blocks
ToolResult(content=[
TextContent(type="text", text="Result: 42"),
ImageContent(type="image", data="base64...", mimeType="image/png")
])
structured_content匹配工具输出的结构化字典,必须是字典格式或者None, 如果只提供了structured_content, 它将被作为content的内容(转成json字符串)。metaTool执行时的运行元数据(如:模型执行的时间,模型版本,置信度),可将其用作指标、调试信息、或者任何不属于content和structured_content的内容。
python
ToolResult(
content="Analysis complete",
structured_content={"result": "positive"},
meta={
"execution_time_ms": 145,
"model_version": "2.1",
"confidence": 0.95
}
)
八、错误处理
如果你想因此运行时的错误详情,可以在创建FastMCP的时候,指定mask_error_details=True参数。
python
mcp = FastMCP(name="SecureServer", mask_error_details=True)
或者使用ToolError明确的向clients返回错误。
python
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
@mcp.tool
def divide(a: float, b: float) -> float:
"""Divide a by b."""
if b == 0:
# Error messages from ToolError are always sent to clients,
# regardless of mask_error_details setting
raise ToolError("Division by zero is not allowed.")
# If mask_error_details=True, this message would be masked
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Both arguments must be numbers.")
return a / b
当指定mask_error_details=True时,只有来自ToolError的错误消息可以包含详细信息,其他异常将被转换为一条通用消息。
九、禁用工具
通过在装饰器中指定enabled参数来禁用
python
@mcp.tool(enabled=False)
def maintenance_tool():
"""This tool is currently under maintenance."""
return "This tool is disabled."
在创建后,手动切换工具的状态
python
@mcp.tool
def dynamic_tool():
return "I am a dynamic tool."
# Disable and re-enable the tool
dynamic_tool.disable()
dynamic_tool.enable()
十、MCP注解
FastMCP 允许你通过注解为工具添加专门的元数据。这些注解向客户端应用程序传达工具的行为方式,而不会占用大型语言模型提示中的令牌上下文。
注解对于client的目的
- 添加便于显示的用户友好型标题
- 表明工具是否会修改数据或系统
- 描述工具的安全特性(破坏性与非破坏性)
- 提示工具是否与外部系统交互
示例
python
@mcp.tool(
annotations={
"title": "Calculate Sum",
"readOnlyHint": True,
"openWorldHint": False
}
)
def calculate_sum(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
annoptations字段
| Annotation | Type | Default | Purpose |
|---|---|---|---|
| title | string | - | 用户界面的显示名称 |
| readOnlyHint | boolean | false | 指示该工具是否仅读取而不进行更改 |
| destructiveHint | boolean | true | 对于非只读工具,表明更改是否具有破坏性 |
| idempotentHint | boolean | false | 表示重复的相同调用是否与单次调用具有相同的效果 |
| openWorldHint | boolean | true | 指定该工具是否与外部系统交互 |
请记住,注释有助于提升用户体验,但应将其视为指导性提示。它们能帮助客户端应用程序呈现合适的用户界面元素和安全控制,但无法自行执行安全边界。始终要注重让你的注释准确反映你的工具实际能做什么。
十一、通知
当工具被添加、移除、启用或禁用时,FastMCP 会自动向已连接的客户端发送 notifications/tools/list_changednotifications。这使得客户端无需手动轮询更改,就能随时了解当前工具集的最新情况。
python
@mcp.tool
def example_tool() -> str:
return "Hello!"
# These operations trigger notifications:
mcp.add_tool(example_tool) # Sends tools/list_changed notification
example_tool.disable() # Sends tools/list_changed notification
example_tool.enable() # Sends tools/list_changed notification
mcp.remove_tool("example_tool") # Sends tools/list_changed notification
那么,客户端是怎么处理这个消息的呢?
python
from fastmcp import Client
async def message_handler(message):
"""Handle all MCP messages from the server."""
if hasattr(message, 'root'):
method = message.root.method
print(f"Received: {method}")
# Handle specific notifications
if method == "notifications/tools/list_changed":
print("Tools have changed - might want to refresh tool cache")
elif method == "notifications/resources/list_changed":
print("Resources have changed")
client = Client(
"my_mcp_server.py",
message_handler=message_handler,
)
十二、访问MCP Context
工具可以通过 Context 对象访问 MCP 的功能,如日志记录、读取资源或报告进度。要使用它,需在工具函数中添加一个带有 Context 类型提示的参数。
注意
- 带
ctx: Context的Tool,在调用的时候,不要指定ctx的值,也不要传这个参数,框架会自己注入 ctx的函数,大部分是异步的,所以,Tool函数也要是异步的。
python
from fastmcp import FastMCP, Context
mcp = FastMCP(name="ContextDemo")
@mcp.tool
async def process_data(data_uri: str, ctx: Context) -> dict:
"""Process data from a resource with progress reporting."""
await ctx.info(f"Processing data from {data_uri}")
# Read a resource
resource = await ctx.read_resource(data_uri)
data = resource[0].content if resource else ""
# Report progress
await ctx.report_progress(progress=50, total=100)
# Example request to the client's LLM for help
summary = await ctx.sample(f"Summarize this in 10 words: {data[:200]}")
await ctx.report_progress(progress=100, total=100)
return {
"length": len(data),
"summary": summary.text
}
Context对象提供的访问有:
- 日志记录: ctx.debug(), ctx.info(), ctx.warning(), ctx.error()
- 进度上报: ctx.report_progress(progress, total)
- 资源访问: ctx.read_resource(uri)
- LLM抽样: ctx.sample(...)
- 请求信息: ctx.request_id, ctx.client_id
示例
服务端
python
@mcp.tool
async def wrapped_cpu_task(data: str, ctx: Context) -> str:
"""CPU-intensive task wrapped to prevent blocking."""
await ctx.info(f"a plus b")
return await anyio.to_thread.run_sync(cpu_intensive_task, data)
客户端返回
python
[12/16/25 11:05:47] INFO Received INFO from server: {'msg': logging.py:44
'a plus b', 'extra': None}
##十三、服务端行为
13.1 工具重名(主要用在动态注册时的场景)
如果您尝试注册多个同名工具,您可以控制 FastMCP 服务器的行为。这是在创建 FastMCP 实例时通过 on_duplicate_tools 参数进行配置的。
python
from fastmcp import FastMCP
mcp = FastMCP(
name="StrictServer",
# Configure behavior for duplicate tool names
on_duplicate_tools="error"
)
@mcp.tool
def my_tool(): return "Version 1"
# This will now raise a ValueError because 'my_tool' already exists
# and on_duplicate_tools is set to "error".
# @mcp.tool
# def my_tool(): return "Version 2"
on_duplicate_tools的选项:
warn记录一个警告,并且新工具会替换旧工具。error引发一个 ValueError,防止重复注册replace默默地用新工具替换现有的工具。ignore保留原始工具,忽略新的注册尝试。
13.2 移除工具
你可以使用 remove_tool 方法从服务器动态移除工具:
python
from fastmcp import FastMCP
mcp = FastMCP(name="DynamicToolServer")
@mcp.tool
def calculate_sum(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
mcp.remove_tool("calculate_sum")