首页 › tutorials › FastMCP 造首个 MCP 工具

概述

本教程带你用 Python FastMCP 从零构建一个可被 Claude Code 直接调用的 MCP 服务器,把一个 SQLite 知识库变成"可对话工具"——模型不仅能搜索词条,还能读取结构化资源、写入新词条。全程零额外依赖(除 fastmcp 外只用 Python 标准库),适合已写过 Python、想让 Claude Code 接入私有数据或内部 API 的工程师。学完你将掌握:用装饰器声明工具/资源、用 Context 注入协议能力、用 stdio 传输跑通服务、以及用 claude mcp 命令把服务器接入会话并调试。

前置条件

为什么选 FastMCP?它现在是 MCP 生态的事实标准——FastMCP 1.0 已在 2024 年并入官方 Python SDK,独立维护的 FastMCP(当前 v3,由 Prefect 团队维护)据官方 README 称每天下载量达百万级,"某种版本的 FastMCP 驱动着跨语言 70% 的 MCP 服务器"。它的核心承诺是:你只写普通 Python 函数,schema、校验、文档、传输协商全部自动生成。

一、MCP 三件套:工具、资源、提示词

在写代码前,先理清 MCP 暴露给模型的三大原语,这决定了你如何切分功能:

原语 语义 何时用 何时不用
Tool(工具) 模型主动调用的"动作",有副作用、可写、可计算 搜索、写入、查询、调外部 API 纯静态大文件(模型每次都拉一遍浪费上下文)
Resource(资源) 模型可"读取"的数据,URI 寻址,可订阅 文档、配置、列表、只读数据集 需要参数化决策的逻辑(那是工具的活)
Prompt(提示词) 可复用的参数化模板 固定工作流入口 一次性问答

一条经验法则:"读静态数据"用资源,"做决策/有副作用"用工具。本教程聚焦前两者——这也是绝大多数 MCP 服务器的核心。

二、五分钟跑通"Hello Tool"

步骤 1:初始化项目

mkdir kb-mcp && cd kb-mcp
uv init --no-readme
uv add fastmcp

uv add fastmcp 会创建虚拟环境并写入 pyproject.toml。完成后验证:

uv run python -c "import fastmcp; print(fastmcp.__version__)"

预期输出形如 3.x.x。

步骤 2:写最小服务器

新建 server.py:

from fastmcp import FastMCP

mcp = FastMCP("Demo 🚀")

@mcp.tool
def add(a: int, b: int) -> int:
    """两数相加"""
    return a + b

if __name__ == "__main__":
    mcp.run()   # 默认 stdio 传输

注意三点:① 装饰器是 @mcp.tool(v3 支持无括号写法,带括号 @mcp.tool() 等价);② 函数的类型注解会被自动转成 JSON Schema——没有注解就没有参数定义;③ docstring 会成为工具描述,模型据此决定是否调用,所以务必写清楚。

步骤 3:用 Inspector 调试

FastMCP 自带一个开发命令,直接打开 MCP Inspector 图形界面:

uv run fastmcp dev server.py

浏览器会弹出 Inspector,你能逐个调用 add 工具、查看返回的 JSON。这是脱离 Claude Code、快速验证逻辑的最佳手段——先在 Inspector 里跑通,再接入 Claude Code。

常见问题: - command not found: fastmcp → 用 uv run fastmcp 而非裸 fastmcp,确保走的是项目虚拟环境。 - Inspector 启动后无工具 → 检查 @mcp.tool 是否漏写、函数是否有 docstring。

三、实战:SQLite 知识库 MCP 服务器

现在构建一个真正有用的服务器。场景:你有一张术语表(这里用 5G/通信术语填充,贴合本机用户的领域),希望 Claude Code 能"查、读、写"。我们用 Python 标准库 sqlite3,零额外依赖。

步骤 4:建库与种子数据

新建 db.py,负责初始化和连接:

import sqlite3
from pathlib import Path

DB_PATH = Path(__file__).parent / "kb.sqlite3"

def get_conn() -> sqlite3.Connection:
    """每次调用返回一个新连接(MCP 工具可能并发,避免共享连接)"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row          # 让结果像字典一样取值
    return conn

def init_db() -> None:
    conn = get_conn()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS terms (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            term TEXT NOT NULL UNIQUE,
            definition TEXT NOT NULL
        )
    """)
    # 仅在空表时写入种子数据
    if conn.execute("SELECT COUNT(*) FROM terms").fetchone()[0] == 0:
        seeds = [
            ("AMF", "Access and Mobility Management Function,5G 核心网接入与移动性管理网元。"),
            ("SMF", "Session Management Function,会话管理功能,负责 PDU 会话的建立与释放。"),
            ("UPF", "User Plane Function,用户面功能,负责数据面转发与 QoS 执行。"),
            ("gNB", "5G 基站,连接 5G 核心网 NG 接口与空口 NR。"),
            ("PDU Session", "终端与数据网络之间的会话,承载用户数据。"),
        ]
        conn.executemany("INSERT INTO terms(term, definition) VALUES(?, ?)", seeds)
        conn.commit()
    conn.close()

为什么每个工具都新建连接? MCP 工具可能被模型并发调用,sqlite3.Connection 不是线程安全的。复用单连接会随机抛 SQLite objects created in a thread can only be used in that same thread。新建连接成本极低(本地文件),换来的是正确性。

步骤 5:声明工具与资源

把核心逻辑写进 server.py:

from fastmcp import FastMCP, Context
from db import get_conn, init_db

init_db()  # 进程启动时确保表与种子数据就绪

mcp = FastMCP("5G 知识库")

# ---------- 工具:模型主动调用的"动作" ----------

@mcp.tool
def search_terms(query: str, limit: int = 10) -> list[dict]:
    """按关键词模糊搜索术语。query 为空时返回全部。

    Args:
        query: 搜索关键词(匹配 term 或 definition)
        limit: 返回条数上限,默认 10
    """
    conn = get_conn()
    pattern = f"%{query}%"
    rows = conn.execute(
        "SELECT id, term, definition FROM terms "
        "WHERE term LIKE ? OR definition LIKE ? LIMIT ?",
        (pattern, pattern, limit),
    ).fetchall()
    conn.close()
    return [dict(r) for r in rows]

@mcp.tool
def add_term(term: str, definition: str, ctx: Context) -> dict:
    """向知识库新增一个术语。

    Args:
        term: 术语名(唯一)
        definition: 该术语的释义
    """
    ctx.info(f"准备写入术语: {term}")            # 进度/日志,会回传给客户端
    conn = get_conn()
    try:
        cur = conn.execute(
            "INSERT INTO terms(term, definition) VALUES(?, ?)",
            (term, definition),
        )
        conn.commit()
        ctx.info(f"写入成功,新 id = {cur.lastrowid}")
        return {"id": cur.lastrowid, "term": term, "ok": True}
    except sqlite3.IntegrityError:
        return {"ok": False, "error": f"术语 '{term}' 已存在"}
    finally:
        conn.close()

# ---------- 资源:模型可"读取"的数据 ----------

@mcp.resource("kb://terms")
def all_terms() -> list[dict]:
    """返回知识库全部术语列表(只读快照)"""
    conn = get_conn()
    rows = conn.execute("SELECT id, term FROM terms ORDER BY term").fetchall()
    conn.close()
    return [dict(r) for r in rows]

# 资源模板:用 URI 参数动态寻址,模型可按 id 读取单条
@mcp.resource("kb://term/{term_id}")
def term_by_id(term_id: int) -> dict:
    """按 id 读取单条术语的完整释义"""
    conn = get_conn()
    row = conn.execute(
        "SELECT id, term, definition FROM terms WHERE id = ?", (term_id,)
    ).fetchone()
    conn.close()
    if row is None:
        return {"error": "not found"}
    return dict(row)

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

逐行解读关键点:

  1. Context 依赖注入:add_term 的最后一个参数声明为 ctx: Context,FastMCP 检测到类型后自动注入协议上下文——ctx.info(...) 会作为日志回传客户端,ctx.report_progress(current, total) 可上报进度条。这是 FastMCP 区别于裸 SDK 的核心便利:你不用关心协议帧,只写 ctx。
  2. 资源 vs 资源模板:kb://terms 是静态 URI(无参数),kb://term/{term_id} 是模板(花括号占位)。模板让模型能像访问 REST 端点一样按 id 取数。
  3. 类型即契约:list[dict]、int 这些注解直接生成 JSON Schema,模型因此"知道"该传什么、能拿到什么。返回值也会被 schema 校验,所以保持返回结构稳定很重要。

步骤 6:再次用 Inspector 自测

uv run fastmcp dev server.py

在 Inspector 里依次:调用 search_terms(query="AMF")、读取资源 kb://term/1、调用 add_term(...) 看日志是否回显。全部通过后再进入下一步——这是规避"Claude Code 里报错却不知是哪一层"的最有效习惯。

四、接入 Claude Code

MCP 服务器本质是被客户端拉起的子进程(stdio 传输)。Claude Code 通过 claude mcp 命令管理这些注册项。

步骤 7:注册服务器

在项目根目录执行:

claude mcp add kb-server -- uv run --directory $(pwd) server.py

要点: - kb-server 是你给服务器起的名字,后续用它引用。 - -- 是分隔符:其左侧是 Claude Code 的参数,右侧是真正启动服务器的命令。Claude Code 会把右侧整条命令作为子进程拉起。 - uv run --directory $(pwd) server.py 确保无论从哪个目录启动,都能定位到项目的虚拟环境。Windows PowerShell 下把 $(pwd) 换成绝对路径。

步骤 8:验证注册

claude mcp list            # 列出所有已注册服务器及连接状态
claude mcp get kb-server   # 查看单个服务器的命令、参数、环境变量

claude mcp list 会显示 kb-server ... ✓ connected。若显示连接失败,多半是命令路径或环境问题(见下方排错)。

步骤 9:在会话中调用

启动会话:claude。然后自然语言提问:

帮我查一下知识库里关于 UPF 的定义,并新增一条术语:SBA,定义为"服务化架构,5G 核心网基于服务化接口的架构风格"。

模型会自行决定:调用 search_terms 查询、读取 kb://terms 浏览目录、调用 add_term 写入。你可以输入 /mcp 查看当前会话里各服务器的工具与连接健康度。

三种作用域,按需选择

作用域 标志 存储位置 适用
local(默认) -s local 用户配置,绑定当前项目 个人开发,不进版本库
project -s project 项目根 .mcp.json 团队共享,应提交到 git
user -s user 用户全局配置 跨所有项目复用

团队共享时,把 .mcp.json 直接写在项目根:

{
  "mcpServers": {
    "kb-server": {
      "command": "uv",
      "args": ["run", "--directory", ".", "server.py"]
    }
  }
}

队友 clone 后首次启动会话,Claude Code 检测到 .mcp.json 会提示是否信任并加载——这是把内部 MCP 工具分发给团队的标准姿势。

五、典型排错清单

现象 根因 解决
claude mcp list 显示未连接 启动命令在 Claude Code 的 shell 里找不到 用绝对路径;确认 uv 在 PATH;改用 claude mcp add kb -- /abs/venv/bin/python /abs/server.py
工具列表里没有你的工具 漏写 @mcp.tool 或函数无 docstring 每个工具必须有装饰器 + docstring,否则不暴露给模型
模型传参类型不对 注解缺失或用了无 schema 的类型 参数必须有 int/str/bool/... 注解;避免用 Any、裸 dict 作入参
sqlite3 线程报错 共享了连接 每个工具内部新建连接(见步骤 4)
改了代码模型却"没生效" 旧子进程仍在运行 /mcp 里 reconnect,或重启会话;stdio 服务器在启动时加载代码
资源读不到 URI 拼写或模板参数不匹配 在 Inspector 的 Resources 面板核对 URI 与模板花括号

六、整体工作流

flowchart TD
    A[uv add fastmcp<br/>初始化项目] --> B[写 server.py<br/>@mcp.tool / @mcp.resource]
    B --> C[fastmcp dev server.py<br/>Inspector 自测]
    C --> D{自测通过?}
    D -- 否 --> B
    D -- 是 --> E[claude mcp add kb -- uv run server.py]
    E --> F[claude mcp list 验证连接]
    F --> G[claude 会话中自然语言调用]
    G --> H[/mcp 查看健康度与工具]
    H --> I{需要团队共享?}
    I -- 是 --> J[claude mcp add -s project<br/>生成 .mcp.json 并提交]
    I -- 否 --> K[完成]
    J --> K

延伸阅读

训练营适用性评估

参考来源

  1. FastMCP — GitHub (PrefectHQ/fastmcp) — 官方仓库与 README,确认 v3 安装方式 uv pip install fastmcp、@mcp.tool / mcp.run() 基础用法,以及 FastMCP 在生态中的地位。
  2. Quickstart — FastMCP 官方文档 — 服务器创建、stdio 传输、三大原语入门。
  3. Tools / Resources & Templates — FastMCP 官方文档 — @mcp.tool、@mcp.resource、资源模板、Context 依赖注入的 API 细节。
  4. Connect Claude Code to tools via MCP — Claude Code Docs — claude mcp add/list/get/remove、-- 分隔符、local/project/user 三作用域与 .mcp.json 配置。
  5. Run Python MCP Servers With uvx — RapidDev — 用 uv/uvx 跑 Python MCP 服务器的实践,对应本文步骤 7 的启动命令选型。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入