跳转至

MCP构建Tools

Tools 是 MCP 中最常用的原语,让模型能够主动调用函数完成任务。这一篇深入讲怎么写好用的 Tool。

1. 基础用法

最简单的 Tool 定义:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("tools-demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b

FastMCP 会自动:

  • 用函数名作为 Tool 名称
  • 用 docstring 作为 Tool 描述
  • 用类型注解生成 input schema
  • 用返回值类型推断输出

2. Tool 描述很重要

模型靠 description 判断什么时候调用这个 Tool。描述写得好坏直接决定模型是否能正确使用。

❌ 不好的写法:

@mcp.tool()
def search(q: str) -> str:
    """搜索"""
    ...

✅ 好的写法:

@mcp.tool()
def search_documents(query: str) -> str:
    """在公司知识库中搜索文档。

    适用场景:用户询问公司内部规章、产品手册、技术文档时使用。
    输入:自然语言搜索词,如 "请假流程"、"API 鉴权方式"。
    输出:相关文档的标题和摘要列表。
    """
    ...

好的描述应该包含:

  • Tool 的核心功能(做什么)
  • 适用场景(什么时候用)
  • 输入输出说明(用什么参数、返回什么)

3. 参数类型与 Schema

FastMCP 支持丰富的 Python 类型,自动转换为 JSON Schema:

from typing import Literal
from pydantic import Field

@mcp.tool()
def search(
    query: str,
    limit: int = 10,
    sort_by: Literal["date", "relevance"] = "relevance",
    tags: list[str] | None = None,
) -> dict:
    """高级搜索接口"""
    return {"results": [...]}

用 Pydantic Field 增强

复杂参数可以用 pydantic.Field 加描述和约束:

from pydantic import Field
from typing import Annotated

@mcp.tool()
def book_flight(
    from_city: Annotated[str, Field(description="出发城市,如 '北京'")],
    to_city: Annotated[str, Field(description="到达城市")],
    date: Annotated[str, Field(description="出发日期,格式 YYYY-MM-DD", pattern=r"^\d{4}-\d{2}-\d{2}$")],
    passengers: Annotated[int, Field(description="乘客数", ge=1, le=9)] = 1,
) -> str:
    """预订机票"""
    return f"已预订 {from_city}{to_city}{date}{passengers} 人"

每个参数都会生成完整 schema,模型能精确理解每个字段。

4. 返回结构化数据

简单情况下可以返回 strintdictlist,FastMCP 自动处理。

更复杂的场景,用 Pydantic 模型定义返回结构:

from pydantic import BaseModel

class WeatherInfo(BaseModel):
    city: str
    temperature: float
    condition: str
    humidity: int

@mcp.tool()
def get_weather(city: str) -> WeatherInfo:
    """查询城市天气"""
    return WeatherInfo(
        city=city,
        temperature=22.5,
        condition="晴",
        humidity=65,
    )

5. 返回多种内容类型

MCP 支持工具返回文本、图片、嵌入式资源等多种类型:

from mcp.server.fastmcp import Image
from mcp.types import TextContent, ImageContent

@mcp.tool()
def generate_chart(data: list[float]) -> Image:
    """生成图表"""
    import matplotlib.pyplot as plt
    import io

    plt.plot(data)
    buf = io.BytesIO()
    plt.savefig(buf, format="png")
    plt.close()

    return Image(data=buf.getvalue(), format="png")

6. 异步 Tool

如果工具需要 I/O(HTTP 请求、数据库查询),用 async def

import httpx

@mcp.tool()
async def fetch_url(url: str) -> str:
    """获取 URL 的内容"""
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.text[:5000]  # 限制返回长度

FastMCP 会自动识别异步函数并正确调度。

7. Context:访问会话上下文

Tool 可以接收一个 Context 参数,访问当前会话的能力:

from mcp.server.fastmcp import Context

@mcp.tool()
async def long_task(items: list[str], ctx: Context) -> str:
    """处理大量数据,汇报进度"""
    total = len(items)

    for i, item in enumerate(items):
        # 上报进度(客户端可以显示进度条)
        await ctx.report_progress(i + 1, total)

        # 输出日志(客户端可以显示)
        await ctx.info(f"正在处理: {item}")

        # ... 实际处理 ...

    return f"处理完成,共 {total} 项"

Context 提供了:

  • ctx.info()ctx.warning()ctx.error():发送日志
  • ctx.report_progress(current, total):上报进度
  • ctx.read_resource(uri):在 Tool 中读取资源
  • ctx.session:底层会话对象,高级用法

8. 错误处理

Tool 抛出异常会被 MCP 捕获并以错误形式返回给客户端:

@mcp.tool()
def divide(a: float, b: float) -> float:
    """两数相除"""
    if b == 0:
        raise ValueError("除数不能为 0")
    return a / b

模型看到错误后通常会重试或换方法,所以错误信息要清晰:

@mcp.tool()
def get_user(user_id: int) -> dict:
    """根据 ID 查询用户"""
    user = db.find_user(user_id)
    if not user:
        raise ValueError(f"用户 {user_id} 不存在,请确认 ID 是否正确")
    return user

如果错误很常见(如未授权),可以选择返回结构化错误而不是抛异常:

@mcp.tool()
def access_file(path: str) -> dict:
    """读取文件"""
    if not is_allowed(path):
        return {
            "success": False,
            "error": "permission_denied",
            "message": f"无权访问 {path}",
        }
    # ... 正常逻辑 ...

模型能直接看到结构化错误并在回复中处理。

9. 调用真实 API 的完整示例

下面是一个调用真实 HTTP API 的工具示例:

import httpx
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("github-mcp-demo")

GITHUB_API = "https://api.github.com"

@mcp.tool()
async def get_repo_info(owner: str, repo: str, ctx: Context) -> dict:
    """获取 GitHub 仓库的基本信息。

    适用场景:用户询问某个 GitHub 仓库的星数、描述、主要语言等元数据。
    """
    await ctx.info(f"正在获取 {owner}/{repo}")

    async with httpx.AsyncClient() as client:
        response = await client.get(f"{GITHUB_API}/repos/{owner}/{repo}")

        if response.status_code == 404:
            raise ValueError(f"仓库 {owner}/{repo} 不存在")
        if response.status_code != 200:
            raise RuntimeError(f"GitHub API 请求失败: {response.status_code}")

        data = response.json()
        return {
            "name": data["full_name"],
            "description": data.get("description", ""),
            "stars": data["stargazers_count"],
            "forks": data["forks_count"],
            "language": data.get("language", "未知"),
            "url": data["html_url"],
        }

@mcp.tool()
async def search_repos(keyword: str, limit: int = 5) -> list[dict]:
    """按关键词搜索 GitHub 仓库,返回前 N 个最受欢迎的结果"""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{GITHUB_API}/search/repositories",
            params={"q": keyword, "sort": "stars", "per_page": limit},
        )
        items = response.json().get("items", [])

        return [{
            "full_name": item["full_name"],
            "description": item.get("description"),
            "stars": item["stargazers_count"],
        } for item in items]

if __name__ == "__main__":
    mcp.run()

10. Tool 设计的最佳实践

1. 单一职责

每个 Tool 只做一件事。get_userupdate_user 分开,不要 manage_user(action="get/update")

2. 描述清晰

描述要让模型一眼看懂什么时候用这个 Tool。可以加适用场景示例。

3. 命名规范

用动词开头:get_*search_*create_*delete_*。模型对常见命名模式更敏感。

4. 限制返回大小

避免返回超长内容把上下文撑爆。可以分页或截断:

@mcp.tool()
def list_files(path: str, max_count: int = 50) -> list[str]:
    """列出目录下的文件,最多返回 max_count 个"""
    files = os.listdir(path)
    return files[:max_count]

5. 提供有用的错误信息

错误消息要让模型知道接下来怎么做

raise ValueError("city 参数必须是中文城市名(如 '北京'),不要用拼音")

6. 警惕副作用

写入、删除、发送类操作要慎重。考虑加入 confirm 参数让模型显式确认:

@mcp.tool()
def delete_file(path: str, confirm: bool = False) -> str:
    """删除文件。confirm 必须为 True 才会真正删除。"""
    if not confirm:
        return "请将 confirm 设为 True 来确认删除"
    os.remove(path)
    return f"已删除 {path}"

总结

  • @mcp.tool() 装饰器 + 类型注解 + docstring 即可定义工具
  • Tool 描述决定模型能否正确使用,要包含功能、场景、输入输出
  • 用 Pydantic Field 增强参数描述和约束
  • 异步 Tool 用 async def,I/O 任务务必异步
  • Context 参数提供日志、进度上报、资源读取能力
  • 异常会传给客户端,错误信息要让模型知道下一步怎么办
  • 设计原则:单一职责、命名规范、限制大小、警惕副作用

评论