<!-- title: FastMCP 造首个 MCP 工具 -->

## 概述

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

## 前置条件

- **Python 3.10+**（FastMCP 要求 3.10 及以上）
- **uv**（推荐，Astral 出品的 Python 包/运行管理器）：`curl -LsSf https://astral.sh/uv/install.sh | sh`
- 已安装并登录的 **Claude Code** CLI（`claude` 命令可用）
- 基本的 Python 函数、类型注解、SQL 常识
- 终端环境（macOS / Linux / WSL 均可）

为什么选 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：初始化项目

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

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

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

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

### 步骤 2：写最小服务器

新建 `server.py`：

```python
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 图形界面：

```bash
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`，负责初始化和连接：

```python
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`：

```python
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 自测

```bash
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：注册服务器

在项目根目录执行：

```bash
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：验证注册

```bash
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` 直接写在项目根：

```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 与模板花括号 |

## 六、整体工作流

```text
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
```

## 延伸阅读

- **FastMCP 官方文档**（gofastmcp.com）：Quickstart、Tools、Resources、Context 章节，是权威 API 参考，并提供 `llms.txt` 供模型直接消费。
- **Claude Code MCP 文档**（code.claude.com/docs/en/mcp）：`claude mcp` 命令全集、作用域、`--` 分隔符、HTTP/SSE 传输。
- **MCP 官方规范**（modelcontextprotocol.io）：协议层原语定义，理解 Tools/Calls/Resources/Prompts 的 JSON-RPC 帧细节。
- **从 stdio 到 HTTP**：服务器稳定后，把 `mcp.run()` 换成 Streamable HTTP 部署到云端，即可跨机器共享（本系列已有"Streamable HTTP 远程部署"专题，可作为下一篇）。
- **Context 高级用法**：`ctx.report_progress`、`await ctx.read_resource(...)` 让工具间互相读资源、`ctx.session` 访问低层会话——这是构建复杂服务器的进阶入口。

## 训练营适用性评估

- **适合类型**：后端/全栈工程师训练营、AI 应用开发营、企业内部"AI 提效"工作坊。要求学员有 Python 基础。
- **教学时长**：讲师演示 + 学员跟练约 **2.5–3 小时**（含建库、调试、注册、排错）。若压缩到 90 分钟，可省略资源模板与团队共享部分，只保留工具 + 注册主线。
- **难度定位**：⭐⭐⭐（中等）。难点不在 Python，而在"理解模型如何选择工具/资源"以及"调试子进程连接"——建议把 Inspector 自测作为强制检查点。
- **可扩展作业**：① 给 `search_terms` 加分页；② 把 SQLite 换成对外 REST API 封装；③ 新增 `@mcp.prompt` 暴露一个"术语复盘"模板。每个作业都能独立验收。

## 参考来源

1. [FastMCP — GitHub (PrefectHQ/fastmcp)](https://github.com/PrefectHQ/fastmcp) — 官方仓库与 README，确认 v3 安装方式 `uv pip install fastmcp`、`@mcp.tool` / `mcp.run()` 基础用法，以及 FastMCP 在生态中的地位。
2. [Quickstart — FastMCP 官方文档](https://gofastmcp.com/getting-started/quickstart) — 服务器创建、stdio 传输、三大原语入门。
3. [Tools / Resources & Templates — FastMCP 官方文档](https://gofastmcp.com/servers/tools) — `@mcp.tool`、`@mcp.resource`、资源模板、`Context` 依赖注入的 API 细节。
4. [Connect Claude Code to tools via MCP — Claude Code Docs](https://code.claude.com/docs/en/mcp) — `claude mcp add/list/get/remove`、`--` 分隔符、local/project/user 三作用域与 `.mcp.json` 配置。
5. [Run Python MCP Servers With uvx — RapidDev](https://www.rapidevelopers.com/mcp-tutorial/how-to-use-uvx-to-run-python-mcp-servers) — 用 `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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
