工具是核心构建模块,使您的LLM能够与外部系统交互、执行代码并访问其训练数据之外的数据。在FastMCP中,工具是通过MCP协议向LLM公开的Python函数。

什么是工具?

FastMCP中的工具将常规Python函数转换为LLM在对话中可以调用的能力。当LLM决定使用工具时:

  1. 它根据工具的架构发送带有参数的请求。
  2. FastMCP会针对您函数的签名验证这些参数。
  3. 您的函数将使用经过验证的输入执行。
  4. 结果返回给LLM,它可以在其响应中使用该结果。

这使得LLM能够执行诸如查询数据库、调用API、进行计算或访问文件等任务,从而将其能力扩展到训练数据之外。

工具

@tool 装饰器

创建一个工具就像用@mcp.tool()装饰一个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

当这个工具注册后,FastMCP会自动:

  • 使用函数名称(add)作为工具名称。
  • 使用函数的文档字符串(Adds two integer numbers...)作为工具描述。
  • 根据函数的参数和类型注解生成输入模式。
  • 处理参数验证和错误报告。

定义Python函数的方式决定了该工具在LLM客户端中的显示和行为方式。

不支持将带有*args**kwargs参数的函数作为工具使用。这一限制是因为FastMCP需要为MCP协议生成完整的参数模式,而可变参数列表无法实现这一点。

参数

注释

参数的类型注释对于工具的正常功能至关重要。它们:

  1. 向LLM告知每个参数的预期数据类型
  2. 启用FastMCP来验证来自客户端的输入数据
  3. 为MCP协议生成准确的JSON模式

为参数使用标准的Python类型注解:

@mcp.tool()
def analyze_text(
    text: str,
    max_tokens: int = 100,
    language: str | None = None
) -> dict:
    """Analyze the provided text."""
    # Implementation...

参数元数据

您可以使用Pydantic的Field类结合Annotated来提供关于参数的额外元数据。这种方法更受推荐,因为它更为现代,并且能将类型提示与验证规则分开:

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注解方式:

@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...

Field 提供了多种验证和文档功能:

  • description: 参数的人类可读解释(展示给大语言模型)
  • ge/gt/le/lt: 大于/小于(或等于)约束
  • min_length/max_length: 字符串或集合的长度约束
  • pattern: 用于字符串验证的正则表达式模式
  • default: 如果省略参数时的默认值

支持的类型

FastMCP 支持广泛的类型注解,包括所有 Pydantic 类型:

类型注解示例描述
Basic typesint, float, str, boolSimple scalar values - see Built-in Types
Binary databytesBinary content - see Binary Data
Date and Timedatetime, date, timedeltaDate and time objects - see Date and Time Types
Collection typeslist[str], dict[str, int], set[int]Collections of items - see Collection Types
Optional typesfloat | None, Optional[float]Parameters that may be null/omitted - see Union and Optional Types
Union typesstr | int, Union[str, int]Parameters accepting multiple types - see Union and Optional Types
Constrained typesLiteral["A", "B"], EnumParameters with specific allowed values - see Constrained Types
PathsPathFile system paths - see Paths
UUIDsUUIDUniversally unique identifiers - see UUIDs
Pydantic modelsUserDataComplex structured data - see Pydantic Models

如需查看此处未列出的其他类型注解,请参阅下方的参数类型部分以获取更详细的信息和示例。

可选参数

FastMCP遵循Python的标准函数参数约定。没有默认值的参数是必填的,而有默认值的参数则是可选的。

@mcp.tool()
def search_products(
    query: str,                   # Required - no default value
    max_results: int = 10,        # Optional - has default value
    sort_by: str = "relevance",   # Optional - has default value
    category: str | None = None   # Optional - can be None
) -> list[dict]:
    """Search the product catalog."""
    # Implementation...

在这个示例中,LLM必须提供一个query参数,而如果没有明确提供max_resultssort_bycategory参数,则会使用它们的默认值。

元数据

虽然FastMCP会从您的函数中推断名称和描述,但您可以通过@mcp.tool装饰器的参数来覆盖这些内容并添加标签:

@mcp.tool(
    name="find_products",           # Custom tool name for the LLM
    description="Search the product catalog with optional category filtering.", # Custom description
    tags={"catalog", "search"}      # Optional tags for organization/filtering
)
def search_products_implementation(query: str, category: str | None = None) -> list[dict]:
    """Internal function description (ignored if description is provided above)."""
    # Implementation...
    print(f"Searching for '{query}' in category '{category}'")
    return [{"id": 2, "name": "Another Product"}]
  • name: 设置通过MCP公开的显式工具名称。
  • description: 提供通过MCP暴露的描述信息。如果设置此项,将忽略函数的文档字符串用于此目的。
  • tags: 用于对工具进行分类的一组字符串。客户端可能会使用标签来筛选或分组可用工具。

异步工具

FastMCP无缝支持标准(def)和异步(async def)函数作为工具。

# Synchronous tool (suitable for CPU-bound or quick tasks)
@mcp.tool()
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Calculate the distance between two coordinates."""
    # Implementation...
    return 42.5

# Asynchronous tool (ideal for I/O-bound operations)
@mcp.tool()
async def fetch_weather(city: str) -> dict:
    """Retrieve current weather conditions for a city."""
    # Use 'async def' for operations involving network calls, file I/O, etc.
    # This prevents blocking the server while waiting for external operations.
    async with aiohttp.ClientSession() as session:
        async with session.get(f"https://api.example.com/weather/{city}") as response:
            # Check response status before returning
            response.raise_for_status()
            return await response.json()

当您的工具需要执行可能等待外部系统(网络请求、数据库查询、文件访问)的操作时,使用async def来保持服务器的响应性。

返回值

FastMCP会自动将您的函数返回的值转换为适合客户端的MCP内容格式:

  • str: 作为TextContent发送。
  • dict, list, Pydantic BaseModel: 序列化为JSON字符串并以TextContent形式发送。
  • bytes: 经过Base64编码后作为BlobResourceContents发送(通常包含在EmbeddedResource中)。
  • fastmcp.Image: 一个用于轻松返回图像数据的辅助类。以ImageContent形式发送。
  • None: 返回空响应(不向客户端发送任何内容)。

FastMCP会尝试将其他类型序列化为字符串(如果可能)。

目前,FastMCP仅响应您工具返回的,而非返回的注解

from fastmcp import FastMCP, Image
import io
try:
    from PIL import Image as PILImage
except ImportError:
    raise ImportError("Please install the `pillow` library to run this example.")

mcp = FastMCP("Image Demo")

@mcp.tool()
def generate_image(width: int, height: int, color: str) -> Image:
    """Generates a solid color image."""
    # Create image using Pillow
    img = PILImage.new("RGB", (width, height), color=color)

    # Save to a bytes buffer
    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    img_bytes = buffer.getvalue()

    # Return using FastMCP's Image helper
    return Image(data=img_bytes, format="png")

@mcp.tool()
def do_nothing() -> None:
    """This tool performs an action but returns no data."""
    print("Performing a side effect...")
    return None

错误处理

New in version: 2.3.4

如果你的工具遇到错误,可以抛出标准的Python异常(ValueErrorTypeErrorFileNotFoundError、自定义异常等)或FastMCP的ToolError

在所有情况下,异常都会被记录并转换为MCP错误响应返回给客户端LLM。出于安全考虑,默认情况下错误消息不会包含在响应中。但是,如果您抛出ToolError,则异常内容包含在响应中。这允许您选择性地向客户端LLM提供有意义的错误信息,帮助LLM理解故障并做出适当反应。

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

@mcp.tool()
def divide(a: float, b: float) -> float:
    """Divide a by b."""

    # Python exceptions raise errors but the contents are not sent to clients
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("Both arguments must be numbers.")

    if b == 0:
        # ToolError contents are sent back to clients
        raise ToolError("Division by zero is not allowed.")
    return a / b

注释

New in version: 2.2.7

FastMCP允许您通过注解为工具添加专门的元数据。这些注解向客户端应用程序传达工具的行为方式,而无需在LLM提示中消耗令牌上下文。

注释在客户端应用程序中有多种用途:

  • 添加用户友好的标题用于显示
  • 指示工具是否会修改数据或系统
  • 描述工具的安全特性(破坏性 vs 非破坏性)
  • 标记工具是否与外部系统交互

你可以通过在@mcp.tool()装饰器中使用annotations参数来为工具添加注释:

@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

FastMCP支持以下标准注解:

注解类型默认值用途
titlestring-Display name for user interfaces
readOnlyHintbooleanfalseIndicates if the tool only reads without making changes
destructiveHintbooleantrueFor non-readonly tools, signals if changes are destructive
idempotentHintbooleanfalseIndicates if repeated identical calls have the same effect as a single call
openWorldHintbooleantrueSpecifies if the tool interacts with external systems

请记住,注释有助于提升用户体验,但应视为建议性提示。它们能帮助客户端应用程序呈现适当的UI元素和安全控制,但本身不会强制执行安全边界。始终专注于让您的注释准确反映fastmcp工具的实际功能。

MCP上下文

工具可以通过Context对象访问MCP功能,如日志记录、读取资源或报告进度。要使用它,请在你的工具函数中添加一个类型提示为Context的参数。

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

有关Context对象及其所有功能的完整文档,请参阅Context文档

参数类型

FastMCP支持多种参数类型,让您在设计工具时拥有更大的灵活性。

FastMCP 通常支持 Pydantic 支持的所有类型作为字段,包括所有 Pydantic 自定义类型。这意味着您可以在工具参数中使用任何能被 Pydantic 验证和解析的类型。

FastMCP在可能的情况下支持类型强制转换。这意味着如果客户端发送的数据与预期类型不匹配,FastMCP会尝试将其转换为适当类型。例如,如果客户端为标注为int的参数发送字符串,FastMCP会尝试将其转换为整数。如果转换不可行,FastMCP将返回验证错误。

内置类型

最常见的参数类型是Python的内置标量类型:

@mcp.tool()
def process_values(
    name: str,             # Text data
    count: int,            # Integer numbers
    amount: float,         # Floating point numbers
    enabled: bool          # Boolean values (True/False)
):
    """Process various value types."""
    # Implementation...

这些类型为LLM提供了关于可接受值的明确预期,并允许FastMCP正确验证输入。即使客户端提供像"42"这样的字符串,对于标注为int的参数,它也会被强制转换为整数。

日期与时间类型

FastMCP 支持来自 datetime 模块的各种日期和时间类型:

from datetime import datetime, date, timedelta

@mcp.tool()
def process_date_time(
    event_date: date,             # ISO format date string or date object
    event_time: datetime,         # ISO format datetime string or datetime object
    duration: timedelta = timedelta(hours=1)  # Integer seconds or timedelta
) -> str:
    """Process date and time information."""
    # Types are automatically converted from strings
    assert isinstance(event_date, date)  
    assert isinstance(event_time, datetime)
    assert isinstance(duration, timedelta)
    
    return f"Event on {event_date} at {event_time} for {duration}"
  • datetime - 接受ISO格式字符串(例如:"2023-04-15T14:30:00")
  • date - 接受ISO格式的日期字符串(例如:"2023-04-15")
  • timedelta - 接受整数秒或timedelta对象

集合类型

FastMCP 支持所有标准的 Python 集合类型:

@mcp.tool()
def analyze_data(
    values: list[float],           # List of numbers
    properties: dict[str, str],    # Dictionary with string keys and values
    unique_ids: set[int],          # Set of unique integers
    coordinates: tuple[float, float],  # Tuple with fixed structure
    mixed_data: dict[str, list[int]] # Nested collections
):
    """Analyze collections of data."""
    # Implementation...

所有集合类型都可以用作参数注解:

  • list[T] - 有序的项目序列
  • dict[K, V] - 键值映射
  • set[T] - 无序的唯一项集合
  • tuple[T1, T2, ...] - 固定长度的序列,可能包含不同类型

集合类型可以嵌套和组合以表示复杂的数据结构。与预期结构匹配的JSON字符串将被自动解析并转换为适当的Python集合类型。

联合类型与可选类型

对于可以接受多种类型或可能被省略的参数:

@mcp.tool()
def flexible_search(
    query: str | int,              # Can be either string or integer
    filters: dict[str, str] | None = None,  # Optional dictionary
    sort_field: str | None = None  # Optional string
):
    """Search with flexible parameter types."""
    # Implementation...

现代Python语法(str | int)比旧式的Union[str, int]形式更受推荐。同样地,str | None也比Optional[str]更受青睐。

约束类型

当一个参数必须是一组预定义值中的一个时,您可以使用字面量类型或枚举类型:

字面量

字面量将参数限制为特定的值集合:

from typing import Literal

@mcp.tool()
def sort_data(
    data: list[float],
    order: Literal["ascending", "descending"] = "ascending",
    algorithm: Literal["quicksort", "mergesort", "heapsort"] = "quicksort"
):
    """Sort data using specific options."""
    # Implementation...

字面量类型:

  • 直接在类型注解中指定允许的确切值
  • 帮助LLMs准确理解哪些数值是可接受的
  • 提供输入验证(对无效值报错)
  • 为客户创建清晰的模式

枚举类型

对于更结构化的约束值集合,请使用Python的Enum类:

from enum import Enum

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@mcp.tool()
def process_image(
    image_path: str, 
    color_filter: Color = Color.RED
):
    """Process an image with a color filter."""
    # Implementation...
    # color_filter will be a Color enum member

使用枚举类型时:

  • 客户端应提供枚举的值(例如"red"),而不是枚举成员名称(例如"RED")
  • FastMCP 会自动将字符串值转换为相应的枚举对象
  • 您的函数接收实际的枚举成员(例如 Color.RED
  • 对于不在枚举中的值会引发验证错误

二进制数据

处理工具参数中的二进制数据有两种方法:

字节

@mcp.tool()
def process_binary(data: bytes):
    """Process binary data directly.
    
    The client can send a binary string, which will be 
    converted directly to bytes.
    """
    # Implementation using binary data
    data_length = len(data)
    # ...

当您将参数标注为bytes时,FastMCP会:

  • 将原始字符串直接转换为字节
  • 验证输入可以正确表示为字节

fastmcp不会自动解码base64编码的字符串作为bytes参数。如果您需要接受base64编码的数据,应该按照如下方式手动处理解码。

Base64编码字符串

from typing import Annotated
from pydantic import Field

@mcp.tool()
def process_image_data(
    image_data: Annotated[str, Field(description="Base64-encoded image data")]
):
    """Process an image from base64-encoded string.
    
    The client is expected to provide base64-encoded data as a string.
    You'll need to decode it manually.
    """
    # Manual base64 decoding
    import base64
    binary_data = base64.b64decode(image_data)
    # Process binary_data...

当您预期会从客户端接收到base64编码的二进制数据时,推荐采用这种方法。

路径

Path 类型来自 pathlib 模块,可用于文件系统路径:

from pathlib import Path

@mcp.tool()
def process_file(path: Path) -> str:
    """Process a file at the given path."""
    assert isinstance(path, Path)  # Path is properly converted
    return f"Processing file at {path}"

当客户端发送一个字符串路径时,FastMCP会自动将其转换为Path对象。

UUID

UUID 类型来自 uuid 模块,可用于生成唯一标识符:

import uuid

@mcp.tool()
def process_item(
    item_id: uuid.UUID  # String UUID or UUID object
) -> str:
    """Process an item with the given UUID."""
    assert isinstance(item_id, uuid.UUID)  # Properly converted to UUID
    return f"Processing item {item_id}"

当客户端发送一个字符串形式的UUID(例如"123e4567-e89b-12d3-a456-426614174000")时,FastMCP会自动将其转换为UUID对象。

Pydantic 模型

对于具有嵌套字段和验证的复杂结构化数据,使用Pydantic模型:

from pydantic import BaseModel, Field
from typing import Optional

class User(BaseModel):
    username: str
    email: str = Field(description="User's email address")
    age: int | None = None
    is_active: bool = True

@mcp.tool()
def create_user(user: User):
    """Create a new user in the system."""
    # The input is automatically validated against the User model
    # Even if provided as a JSON string or dict
    # Implementation...

使用Pydantic模型提供以下优势:

  • 为复杂输入提供清晰、自文档化的结构
  • 内置数据验证
  • 为LLM自动生成详细的JSON模式
  • 支持从字典/JSON输入自动转换

客户端可以为Pydantic模型参数提供以下两种形式的数据:

  • 一个JSON对象(字符串)
  • 一个具有适当结构的字典
  • 以适当格式嵌套的参数

Pydantic字段

FastMCP通过Pydantic的Field类支持强大的参数验证功能。这对于确保输入值不仅满足类型要求,还符合特定条件特别有用。

请注意,字段可以在Pydantic模型之外使用,以提供元数据和验证约束。推荐的方法是使用AnnotatedField结合:

from typing import Annotated
from pydantic import Field

@mcp.tool()
def analyze_metrics(
    # Numbers with range constraints
    count: Annotated[int, Field(ge=0, le=100)],         # 0 <= count <= 100
    ratio: Annotated[float, Field(gt=0, lt=1.0)],       # 0 < ratio < 1.0
    
    # String with pattern and length constraints
    user_id: Annotated[str, Field(
        pattern=r"^[A-Z]{2}\d{4}$",                     # Must match regex pattern
        description="User ID in format XX0000"
    )],
    
    # String with length constraints
    comment: Annotated[str, Field(min_length=3, max_length=500)] = "",
    
    # Numeric constraints
    factor: Annotated[int, Field(multiple_of=5)] = 10,  # Must be multiple of 5
):
    """Analyze metrics with validated parameters."""
    # Implementation...

你也可以使用Field作为默认值,不过更推荐Annotated这种方式:

@mcp.tool()
def validate_data(
    # Value constraints
    age: int = Field(ge=0, lt=120),                     # 0 <= age < 120
    
    # String constraints
    email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$"),  # Email pattern
    
    # Collection constraints
    tags: list[str] = Field(min_length=1, max_length=10)  # 1-10 tags
):
    """Process data with field validations."""
    # Implementation...

常见的验证选项包括:

验证类型描述
ge, gtNumberGreater than (or equal) constraint
le, ltNumberLess than (or equal) constraint
multiple_ofNumberValue must be a multiple of this number
min_length, max_lengthString, List, etc.Length constraints
patternStringRegular expression pattern constraint
descriptionAnyHuman-readable description (appears in schema)

当客户端发送无效数据时,FastMCP会返回一个验证错误,说明参数验证失败的原因。

服务器行为

重复工具

New in version: 2.1.0

当您尝试注册多个同名工具时,可以控制FastMCP服务器的行为方式。这通过在创建FastMCP实例时使用on_duplicate_tools参数进行配置。

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"

重复行为选项包括:

  • "warn" (默认值): 记录警告日志并用新工具替换旧工具。
  • "error": 抛出ValueError错误,阻止重复注册。
  • "replace": 静默地用新工具替换现有工具。
  • "ignore": 保留原始工具并忽略新的注册尝试。

移除工具

New in version: 2.3.4

您可以使用remove_tool方法动态地从服务器移除工具:

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")

传统JSON解析

New in version: 2.2.10

FastMCP 1.0 和 2.2.10 之前的版本依赖一个临时解决方案,该方案试图通过自动解析工具参数中的字符串化JSON(例如将"[1,2,3]"转换为[1,2,3])来规避LLM的限制。从FastMCP 2.2.10开始,此行为默认被禁用,因为它绕过了类型验证并可能导致意外的类型强制转换问题(例如将"true"解析为布尔值,而尝试调用期望字符串的工具,这将无法通过类型验证)。

大多数现代LLM能够正确格式化JSON,但如果使用的模型会不必要地将JSON字符串化(如2024年末的Claude Desktop版本),您可以通过在服务器上设置环境变量FASTMCP_TOOL_ATTEMPT_PARSE_JSON_ARGS=1来重新启用此功能。

除非必要,我们强烈建议保持此选项禁用。