<!-- title: Python FastMCP打造代码片段管家 -->

## 概述

本教程带你用 **Python + FastMCP** 从零构建一个可被 Claude Code 直接调用的本地 MCP 服务器——「代码片段管家（Snippet Vault）」。学完你将掌握：用装饰器把普通 Python 函数变成 MCP 工具、只读资源、提示模板；用 `Context` 向客户端回报进度与日志；用 `uv`/`uvx` 打包并通过 `claude mcp add` 接入 Claude Code；用 `mcp dev` 全链路调试。适合写过 Python、想给自己（或团队）的 Claude Code 加一套私有工具的工程师。前置已覆盖 Go/Rust 构建 MCP 的路径，本篇补齐最主流的 **Python FastMCP** 这条官方推荐路线。

## 前置条件

- Python 3.11 或以上（建议 3.12）
- 已安装 `uv`（`curl -LsSf https://astral.sh/uv/install.sh | sh`），用于依赖管理与 `uvx` 运行
- 已安装 Claude Code 并完成登录（`claude` 可正常启动）
- 一台能访问 GitHub/PyPI 的开发机；全程**无需**任何 API Key（纯本地 stdio 通信）
- 基础：会读 Python 类型注解、用过虚拟环境

## 为什么是 FastMCP

手写 MCP 意味着自己实现 JSON-RPC 2.0、能力协商、stdio 帧解析、工具 schema 校验……而 FastMCP 把这套底层全部封装，让你**只写业务函数**：

| 原语 | 装饰器 | 语义 | 在 Snippet Vault 中的角色 |
|------|--------|------|--------------------------|
| 工具 Tool | `@mcp.tool` | LLM **可调用**、有副作用 | 增删改查片段 |
| 资源 Resource | `@mcp.resource` | 只读数据，LLM 按需读取 | 片段索引、单个片段内容 |
| 提示 Prompt | `@mcp.prompt` | 可复用的消息模板 | “审查这段代码”模板 |

FastMCP 会根据函数签名与类型注解**自动生成 JSON Schema**，Claude 据此决定何时调用、传什么参数。它的开发者体验接近 FastAPI，是目前 Anthropic 官方在 Python 侧首推的构建方式（FastMCP 已与官方 `mcp` Python SDK 合流，2026 年稳定在 3.x）。

## 第一步：脚手架与依赖

创建项目目录并用 `uv` 初始化：

```bash
mkdir snippet-vault && cd snippet-vault
uv init --name snippet-vault
uv add fastmcp
```

`uv init` 会生成 `pyproject.toml` 和一个示例 `main.py`。把 `main.py` 删掉，我们从头写。确认 FastMCP 版本在 2.x/3.x：

```bash
uv run python -c "import fastmcp; print(fastmcp.__version__)"
# 预期输出类似：3.2.x 或 2.x
```

**常见问题**

- `uv: command not found`：没装 `uv`，回到前置条件重装。
- `fastmcp.__version__` 报 `AttributeError`：版本过老，`uv add fastmcp@latest` 升级。

## 第二步：最小可运行服务器

先写一个只有两个工具的最小版本，跑通“装饰器 → schema → Claude 调用”闭环。新建 `server.py`：

```python
from fastmcp import FastMCP

mcp = FastMCP("Snippet Vault")

@mcp.tool
def ping(message: str = "hello") -> str:
    """健康检查：原样回显消息，证明链路畅通。"""
    return f"pong: {message}"

if __name__ == "__main__":
    mcp.run()          # 默认 stdio 传输，无需任何参数
```

**预期结果**：`mcp.run()` 会让进程进入 stdio 监听，**不会**打印任何东西到终端——stdio 协议要求 stdout 只能是 JSON-RPC 帧，任何 `print` 都会破坏协议。这是新手最大的坑。

**常见问题**

- 调试时想打印日志：**不要用 `print`**，改用第三步介绍的 `ctx.info(...)` 或 `logging` 输出到 stderr。
- 启动后立刻退出：检查是否漏了 `mcp.run()`，或被 shell 的管道提前关闭了 stdin。

## 第三步：搭建 Snippet Vault 数据层

我们用一个 JSON 文件作为持久化（教学够用，生产可换 SQLite/Postgres）。新建 `store.py`：

```python
from __future__ import annotations
import json, uuid, threading
from pathlib import Path
from typing import TypedDict

class Snippet(TypedDict):
    id: str
    title: str
    language: str
    code: str
    tags: list[str]

_PATH = Path.home() / ".snippet-vault.json"
_LOCK = threading.Lock()

def _load() -> list[Snippet]:
    if not _PATH.exists():
        return []
    return json.loads(_PATH.read_text("utf-8"))

def _save(data: list[Snippet]) -> None:
    _PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")

def add(title: str, language: str, code: str, tags: list[str]) -> Snippet:
    with _LOCK:
        data = _load()
        item: Snippet = {"id": uuid.uuid4().hex[:8], "title": title,
                         "language": language, "code": code, "tags": tags or []}
        data.append(item)
        _save(data)
        return item

def all_snippets() -> list[Snippet]:
    return _load()

def get(sid: str) -> Snippet | None:
    return next((s for s in _load() if s["id"] == sid), None)

def delete(sid: str) -> bool:
    with _LOCK:
        data = _load()
        before = len(data)
        data = [s for s in data if s["id"] != sid]
        _save(data)
        return len(data) < before
```

**预期结果**：首次写入会在 `~/.snippet-vault.json` 生成文件。`_LOCK` 保证并发安全。

## 第四步：把能力暴露成 MCP 工具

回到 `server.py`，接入数据层，并写**带类型注解 + docstring** 的工具——这两样直接决定 Claude 调用的准确率：

```python
from fastmcp import FastMCP, Context
import store

mcp = FastMCP("Snippet Vault")

@mcp.tool
def save_snippet(title: str, language: str, code: str, tags: list[str] | None = None) -> dict:
    """保存一段代码片段。

    Args:
        title: 片段标题，简短描述用途。
        language: 语言标签，如 python、sql、bash。
        code: 代码本体。
        tags: 可选标签列表，便于检索。
    """
    item = store.add(title, language, code, tags or [])
    return {"id": item["id"], "title": item["title"], "saved": True}

@mcp.tool
def search_snippets(query: str, ctx: Context) -> list[dict]:
    """按关键词在标题、标签、代码中模糊检索片段。"""
    q = query.lower()
    hits: list[dict] = []
    all_items = store.all_snippets()
    for i, s in enumerate(all_items, 1):
        ctx.report_progress(i, len(all_items))          # 向客户端推送进度
        blob = f"{s['title']} {' '.join(s['tags'])} {s['code']}".lower()
        if q in blob:
            hits.append({"id": s["id"], "title": s["title"], "language": s["language"]})
    ctx.info(f"检索完成，命中 {len(hits)} 条")              # 写入客户端日志
    return hits

@mcp.tool
def get_snippet(snippet_id: str) -> dict:
    """根据 ID 取片段完整内容。"""
    s = store.get(snippet_id)
    if s is None:
        raise ValueError(f"未找到片段: {snippet_id}")
    return s

@mcp.tool
def delete_snippet(snippet_id: str) -> dict:
    """删除指定 ID 的片段。"""
    deleted = store.delete(snippet_id)
    return {"id": snippet_id, "deleted": deleted}

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

**为什么这样写**：

- 函数名、参数名、docstring 全部清晰且动词化（`save_snippet` 而非 `handle_data`）。Claude 是“读 schema 决定是否调用”，描述越准，误触发越少。
- `ctx: Context` 参数**不会**出现在工具 schema 里——FastMCP 识别出它是上下文对象，自动注入，对 LLM 不可见。用它做 `report_progress`/`info`/`debug`，既不污染协议，又能让用户在 Claude Code 里看到进度。
- 抛 `ValueError` 而非返回 `None`：FastMCP 会把它转成结构化错误，Claude 能据此重试或换参数。

**预期结果**：四个工具就绪。Claude 在收到“帮我存一段快速排序的 Python 片段”时，应能自主选择 `save_snippet` 并填好参数。

**常见问题**

- Claude 不调用工具：90% 是 docstring 模糊。把“处理数据”改成“保存代码片段到本地库”，重试。
- 参数类型不被支持：FastMCP 支持 `str/int/float/bool/list/dict/Optional/Union` 及 Pydantic 模型；自定义类需要先转成 `TypedDict` 或 `BaseModel`。
- 报 `missing required argument: ctx`：你把 `ctx` 放在了**有默认值**的参数之后，或没从 `fastmcp` 导入 `Context`。

## 第五步：用资源和提示补齐“三原语”

工具负责“动作”，资源负责“可读数据”，提示负责“任务模板”。补上后两类：

```python
from fastmcp import FastMCP

# —— 静态资源：片段目录 ——
@mcp.resource("snippets://index")
def snippet_index() -> list[dict]:
    """所有片段的精简目录，便于 LLM 一览全貌。"""
    return [{"id": s["id"], "title": s["title"], "language": s["language"]}
            for s in store.all_snippets()]

# —— 资源模板：按 ID 取单个片段（URI 含占位符，动态解析）——
@mcp.resource("snippet://{snippet_id}")
def snippet_by_id(snippet_id: str) -> str:
    s = store.get(snippet_id)
    return f"# {s['title']}\n```{s['language']}\n{s['code']}\n```" if s else "（片段不存在）"

# —— 提示模板：固定一套审查流程 ——
@mcp.prompt
def review_snippet(snippet_id: str, focus: str = "正确性与边界") -> str:
    """生成一段“代码审查”提示，聚焦指定维度。"""
    s = store.get(snippet_id)
    if not s:
        return f"未找到片段 {snippet_id}，请先用 search_snippets 查找。"
    return (
        f"请审查以下 {s['language']} 片段，重点：{focus}。\n"
        f"标题：{s['title']}\n代码：\n{s['code']}\n"
        "输出：1) 潜在 bug；2) 改进建议；3) 重写后的版本。"
    )
```

把上面三段追加到 `server.py`（`mcp = FastMCP(...)` 之后、`mcp.run()` 之前）。至此工具/资源/提示**三原语齐备**，与 Claude Code 的对接面完整。

**资源 vs 工具的取舍**：只读、想让 LLM 自主“翻阅”的用资源；需要副作用、精确参数校验的用工具。`snippets://index` 给 Claude 一个总览入口，避免它一上来就盲目调用 `search_snippets`。

## 第六步：打包成可执行入口（给 uvx 用）

要让 Claude Code 用 `uvx` 一键拉起服务器，得声明一个命令行入口。编辑 `pyproject.toml`：

```toml
[project]
name = "snippet-vault"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastmcp"]

[project.scripts]
snippet-vault = "server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["."]
```

再在 `server.py` 末尾把启动逻辑包成函数：

```python
def main() -> None:
    mcp.run()
```

然后本地自测入口：

```bash
uv run snippet-vault --help   # 进程能起来、不报错即通过（stdio 模式会等待输入，Ctrl+C 退出）
```

**常见问题**

- `uvx: package not found`：还没发布。本地开发用 `claude mcp add ... -- uv run snippet-vault`；想用 `uvx` 直接跑，先 `uv build` 再 `uv tool install ./dist/*.whl`。
- `hatchling` 报找不到包：确认 `packages = ["."]` 与 `server.py` 同级。

## 第七步：接入 Claude Code

Claude Code 支持三种传输：本地 stdio、HTTP、远程 SSE。本地工具用 **stdio** 最省事。两种接入方式任选其一。

**方式 A：`claude mcp add`（命令行）**

```bash
# 从项目目录运行；-- 之后的参数就是启动命令
claude mcp add snippet-vault -- uv run --directory $(pwd) snippet-vault
```

`--directory` 很关键——否则 Claude Code 可能在别的工作目录找不到 `pyproject.toml`。

**方式 B：`.mcp.json`（项目级配置，可提交到仓库共享）**

在项目根写 `.mcp.json`：

```json
{
  "mcpServers": {
    "snippet-vault": {
      "command": "uv",
      "args": ["run", "--directory", "/绝对路径/snippet-vault", "snippet-vault"]
    }
  }
}
```

放进仓库后，团队成员 `claude` 一启动就会自动加载（首次会提示授权）。范围控制：`claude mcp add --scope user` 全局可用，`--scope project` 写入 `.mcp.json`，`--scope local` 仅本人本机。

**验证连接**：

```bash
claude mcp list                 # 应看到 snippet-vault ✓ connected
claude mcp get snippet-vault    # 列出它暴露的 tools/resources/prompts
```

进 Claude Code 交互界面，输入：*“帮我存一段二分查找的 Python 实现，标签加上算法和查找，然后告诉我现在库里都有哪些片段。”* Claude 应依次调用 `save_snippet` → `snippets://index`（或 `search_snippets`）。

**常见问题**

- `claude mcp list` 显示 `✗ failed to connect`：进入项目目录跑 `uv run snippet-vault` 看真实报错；99% 是缺依赖或路径不对。
- 工具列表里没有新加的工具：重启 Claude Code 会话（MCP 服务器在会话启动时加载一次）。
- 权限弹窗太频繁：在 `.claude/settings.json` 把 `mcp__snippet-vault__*` 加入 `permissions.allow`。

## 第八步：用 Inspector 全链路调试

写 MCP 最大的痛点是“它跑在 stdio 黑盒里，出错难定位”。官方调试器 `mcp dev` 能可视化地手动触发每个工具/资源/提示，并看到完整的 JSON-RPC 往返：

```bash
uv run mcp dev server.py
```

执行后终端会打印一个本地 Web 地址（默认 `http://localhost:5173`），浏览器打开即可：

1. 左侧 **Tools** 列表点 `save_snippet` → 右侧填表单 → **Run**，下方看返回值。
2. **Resources** 里能看到 `snippets://index` 与模板 `snippet://{snippet_id}`。
3. **Prompts** 里执行 `review_snippet`，预览生成的消息。
4. 底部 **Notifications** 区域会实时显示 `ctx.report_progress` 推送的进度——这是验证 Context 是否工作的最直观方式。

**调试心法**：

- 先在 Inspector 跑通每个原语，再去 Claude Code 里测自然语言触发。把“协议正确性”和“LLM 触发准确性”两层问题分开排错。
- 服务器内部想打日志，统一 `import logging; logging.basicConfig(stream=sys.stderr)`，**绝不** `print` 到 stdout。
- 想看 Claude 到底发了什么参数，开 `claude --debug`，或在 Inspector 里观察入站请求。

## 实战案例：把 Snippet Vault 变成“团队代码风格自检”闭环

把前面八步拼成一个真实场景：新人提交 PR 前，让 Claude Code 调用你的服务器自检是否符合团队片段库的既有范式。

1. 往库里灌入团队约定片段（命名规范、错误处理范式、SQL 模板等）。
2. 在仓库根放 `.mcp.json`，把 `snippet-vault` 设为 `--scope project`。
3. 用 `review_snippet` 提示 + `search_snippets` 工具组合：让 Claude 先按“错误处理”检索现有范式，再据此审查当前改动。
4. 配一个 hook：`Stop` 事件触发时若改动文件含 `# TODO`，自动调用 `search_snippets("TODO 处理范式")` 把规范片段贴进对话。

这样一个“私有知识 + 工具调用”的闭环就建好了，且**全程零外部 API、零密钥**。

## 工作流程图

```mermaid
flowchart TD
    A[Python 函数 + 装饰器] -->|FastMCP 自动生成| B[JSON Schema]
    B --> C[stdio / uvx 启动 server.py]
    C --> D{调试通道}
    D -->|mcp dev Inspector| E[可视化手动触发]
D --> |claude mcp add . mcp.json| F[Claude Code 加载]
    F --> G[自然语言触发]
    G --> H[Claude 按 schema 选工具]
    H --> I[store.py 读写 JSON]
    I --> J[Context 回报进度/日志]
    J --> G
```

## 三原语取舍决策图

```mermaid
flowchart LR
    Q{这份能力是?}
    Q -->|有副作用/需参数校验| T[@mcp.tool<br/>工具]
    Q -->|只读数据/可被浏览| R[@mcp.resource<br/>资源]
    Q -->|固定任务模板| P[@mcp.prompt<br/>提示]
    T --> CT[命名动词化 + 详尽 docstring]
    R --> CR[URI 清晰 + 静态/模板二选一]
    P --> CP[参数少 + 产出可预期]
```

## 延伸阅读

- **FastMCP 官方文档**（`gofastmcp.com`）：Context 进阶、认证、部署到 Cloudflare 的最权威出处。
- **Model Context Protocol 规范**（`modelcontextprotocol.io`）：传输层、生命周期、能力协商的底层定义，想从“会用”到“会改协议”必读。
- **Claude Code MCP 文档**（`code.claude.com/docs/en/mcp`）：`claude mcp add/get/list`、三种 scope、`.mcp.json` 的官方说明。
- **MCP Python SDK**（GitHub `modelcontextprotocol/python-sdk`）：FastMCP 已与其合流，底层 API 与示例库。
- **Anthropic MCP 示例集**：filesystem、sqlite、fetch 等参考服务器，是拆解“生产级服务器怎么写”的最佳样本。

## 训练营适用性评估

- **适合**：Python 后端 / AI 工程化 / 企业内 AI 平台方向的训练营；5–7 天封闭营可设为“工具化”模块的第 2–3 天项目。
- **教学时长**：讲授 + 跟做约 **3 小时**（环境 15min、最小服务器 30min、三原语 75min、接入与调试 60min）；学员独立完成实战闭环另需 1.5 小时。
- **分组建议**：2 人一组，一人写工具、一人写资源/提示，最后合并联调，能最大化暴露“schema 描述不清导致 Claude 不触发”这类真实协作问题。
- **前置衔接**：建议放在“Skill 开发”之后——Skill 解决“提示词复用”，MCP 解决“能力扩展”，两者组合即是企业内 Claude Code 平台的两大支柱。
- **难度锚点**：⭐⭐⭐（满分 5）。难点不在 Python，而在“用 LLM 视角写 schema 与 docstring”的范式转换，以及 stdio 调试习惯的建立。

## 参考来源

1. [Connect Claude Code to tools via MCP — Claude Code Docs](https://code.claude.com/docs/en/mcp) — 官方 `claude mcp add/list/get`、三种 scope 与 `.mcp.json` 格式权威说明。
2. [How to Build a Python MCP Server with FastMCP — Pyrastra](https://pyrastra.com/posts/python-mcp-server-fastmcp-tutorial/) — 工具/资源/提示三原语 + Streamable HTTP 部署的完整 2026 实操。
3. [FastMCP in Python: Build a Real MCP Server (2026 Guide) — danilchenko.dev](https://www.danilchenko.dev/posts/fastmcp-mcp-server/) — FastMCP 3.2 的新特性、Context 用法与 GitHub OAuth 集成示例。
4. [Tools — FastMCP 官方文档](https://fastmcp.wiki/en/servers/tools) / [Prompts — FastMCP](https://gofastmcp.com/servers/prompts) — 装饰器签名、Context 对象、资源模板语法的官方定义。
5. [How to Build an MCP Server with Python, Docker, and Claude Code — freeCodeCamp](https://www.freecodecamp.org/news/how-to-build-an-mcp-server-with-python-docker-and-claude-code/) — 从本地 stdio 到 Docker 化部署的端到端参考。

> [!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)
