<!-- title: 用FastMCP构建首个Python MCP服务器 -->

## 概述

本教程面向想为 Claude Code（或任意支持 MCP 的客户端）打造**自定义工具服务器**的开发者，目标是带你用 Python 的 **FastMCP** 框架，从零写出一个可被 Claude Code 直接调用的 MCP 服务器。学完后你将掌握：用装饰器把普通 Python 函数变成工具（Tool）、资源（Resource）与提示词（Prompt）；用 `Context` 对象实现日志上报、进度通知与会话状态；用 `claude mcp` 命令把服务器以三种作用域接入 Claude Code；并完成一个可运行的 Markdown 笔记库检索服务器实战项目。适合已具备 Python 基础、想扩展 AI 编程助手能力边界的工程师。

## 前置条件

- **Python 3.10+**（FastMCP 大量使用现代类型注解与异步特性）。
- 包管理工具：推荐 **uv**（一条命令拉起隔离环境），也可用 `pip` + `venv`。
- 已安装 **Claude Code** CLI（`claude` 命令可用），用于最后一步接入验证。
- 基础知识：会写函数与类型注解，了解 `async/await` 的基本含义即可，**不需要**了解 JSON-RPC 协议细节——这正是 FastMCP 要帮你屏蔽的部分。

## 一、为什么是 FastMCP：写 Python，而不是协议样板

MCP（Model Context Protocol）是一个开放协议，规范了 AI 模型如何从外部服务获取**工具、资源与提示词**。如果你手工实现，需要处理大量样板：JSON-RPC 报文收发、会话状态管理、请求与响应的格式化、能力协商……这些和你的业务逻辑毫无关系。

**FastMCP** 把这些都封装掉了。它的核心哲学一句话概括：**写 Python，而不是协议样板**。你只需要做三件事：

1. 创建一个 `FastMCP` 实例作为容器；
2. 用 `@mcp.tool`、`@mcp.resource`、`@mcp.prompt` 装饰普通函数；
3. 调用 `mcp.run()` 启动。

FastMCP 会自动根据函数名、文档字符串（docstring）和类型注解，生成工具的 JSON Schema，让客户端的模型知道这个工具叫什么、做什么、需要什么参数。这种"装饰器即接口"的设计，让一个工具的定义可以短到三行。

## 二、第一步：搭建项目骨架

创建项目目录并安装依赖：

```bash
mkdir notes-mcp && cd notes-mcp
# 用 uv（推荐，自动建虚拟环境）
uv init && uv add fastmcp
# 或用 pip
python -m venv .venv && source .venv/bin/activate
pip install fastmcp
```

新建 `server.py`，写入最小可运行骨架：

```python
from fastmcp import FastMCP

# 1. 创建服务器实例，起一个描述性名字（会展示给客户端）
mcp = FastMCP(name="Notes MCP Server")

if __name__ == "__main__":
    mcp.run()   # 默认使用 STDIO 传输
```

运行 `python server.py`，它会阻塞等待客户端通过标准输入输出与之通信——这正是正常状态，说明服务器已就绪。按 `Ctrl+C` 退出。

**预期结果**：进程不报错地挂起，没有 traceback。如果出现 `ModuleNotFoundError: fastmcp`，说明虚拟环境没激活或依赖没装到当前环境。

## 三、第二步：用 `@mcp.tool` 把函数变成工具

工具（Tool）是模型可以**执行**的函数。写一个普通函数并加装饰器：

```python
@mcp.tool
def add(a: int, b: int) -> int:
    """两数相加并返回结果。"""
    return a + b
```

FastMCP 自动完成三件事：

- **工具名**：用函数名 `add`；
- **描述**：用 docstring 作为给模型看的说明；
- **入参 Schema**：从类型注解 `a: int, b: int` 推断出 JSON Schema。

这意味着**文档字符串和类型注解就是工具的"说明书"**——写得越清楚，模型选错工具的概率越低。三个最佳实践：

1. **docstring 要写清"做什么"与"何时用"**，不要只写"计算"。
2. **用具体类型**：`int`/`str`/`list[str]` 比 `Any` 好；必要时用 Pydantic 模型约束结构。
3. **工具要小而专注**：一个函数干一件事，避免"万能函数"。

异步工具同样支持，把 `def` 换成 `async def` 即可，适合做网络请求、文件 IO：

```python
@mcp.tool
async def fetch(url: str) -> str:
    """获取指定 URL 的纯文本内容。"""
    # 实际项目用 httpx
    return f"(假装抓取了 {url})"
```

## 四、第三步：用 `@mcp.resource` 暴露只读数据

资源（Resource）是给模型**读取**的只读数据，通过 URI 访问。资源与工具的区别：工具是"动作"，资源是"数据"。

静态资源，URI 固定：

```python
@mcp.resource("resource://config")
def get_config() -> dict:
    """返回应用配置。"""
    return {"version": "1.0", "max_notes": 1000}
```

**资源模板**用于按参数动态生成内容，在 URI 中用 `{占位符}`：

```python
@mcp.resource("notes://{title}")
def read_note_by_title(title: str) -> str:
    """按标题读取一篇笔记的正文。"""
    path = NOTES_DIR / f"{title}.md"
    return path.read_text(encoding="utf-8") if path.exists() else "未找到"
```

客户端请求 `notes://架构设计` 时，FastMCP 会把占位符映射到 `title` 参数并调用函数。资源是**懒加载**的——只有被请求时才执行，适合暴露大体积或需计算的数据。

在 Claude Code 会话里，资源可以用 `@` 引用，例如 `@notes-mcp-server:notes://架构设计`，会出现在 `@` 自动补全菜单中。

## 五、第四步：用 `@mcp.prompt` 复用提示词

提示词（Prompt）是可复用的提示模板，在 Claude Code 里会变成 `/` 斜杠命令。定义时把可变部分作为参数：

```python
@mcp.prompt
def summarize_topic(topic: str) -> str:
    """生成"总结某主题所有笔记"的提示。"""
    return (
        f"请阅读笔记库中与「{topic}」相关的所有笔记，"
        "提炼出三到五个关键结论，并标注来源标题。"
    )
```

接入后，用户输入 `/mcp__notes-mcp-server__summarize_topic 架构` 即可触发，模型会拿到渲染好的提示文本。Prompt 适合固化团队常用的分析/评审套路。

## 六、第五步：用 `Context` 打通双向通信

前面三步是"单向"的——服务器只把结果丢回去。**`Context` 对象**让你能与客户端双向交互：上报日志、推送进度、读写会话状态、甚至反向请求客户端的 LLM。这是把玩具服务器变成生产级服务器的关键。

通过依赖注入获取 Context（推荐写法）：

```python
from fastmcp import FastMCP
from fastmcp.dependencies import CurrentContext
from fastmcp.server.context import Context

mcp = FastMCP(name="Notes MCP Server")

@mcp.tool
async def search_notes(keyword: str, ctx: Context = CurrentContext()) -> list[str]:
    """在笔记库中按关键词检索，返回匹配的标题列表。"""
    await ctx.info(f"开始检索关键词：{keyword}")          # 客户端可见的日志
    matches = [p.stem for p in NOTES_DIR.glob("*.md") if keyword in p.read_text(encoding="utf-8")]
    await ctx.report_progress(progress=len(matches), total=len(matches))  # 进度通知
    return matches
```

关键点：

- **依赖参数被自动排除出 Schema**：客户端永远看不到 `ctx`，它不是工具入参。
- **Context 方法是异步的**：用了它的函数通常得是 `async def`。
- **Context 生命周期是单次请求**：一次工具调用一个新 Context，请求之间不共享。

Context 的常用能力一览：

| 能力 | 调用 | 用途 |
|------|------|------|
| 日志 | `await ctx.info/warning/error(...)` | 把执行状态回传客户端 |
| 进度 | `await ctx.report_progress(progress, total)` | 长任务进度条 |
| 读资源 | `await ctx.read_resource("notes://x")` | 工具内部读取其它资源 |
| 会话状态 | `await ctx.set_state(k,v)` / `get_state(k)` | 跨工具调用保持数据 |
| 用户输入 | `await ctx.elicit("请输入", response_type=str)` | 执行中向用户要结构化输入 |
| 反向采样 | `await ctx.sample("分析这段数据")` | 让客户端的 LLM 生成文本 |
| 传输类型 | `ctx.transport` | 区分 stdio / http，按场景调行为 |

**会话状态**尤其值得展开——它让"有状态"工具成为可能，且按客户端会话隔离：

```python
@mcp.tool
async def recent_keywords(ctx: Context) -> list[str]:
    """返回本次会话最近检索过的关键词（去重，最多 10 个）。"""
    history: list[str] = await ctx.get_state("keywords") or []
    return history[-10:]

# 在 search_notes 末尾追加：
history = await ctx.get_state("keywords") or []
if keyword not in history:
    history.append(keyword)
await ctx.set_state("keywords", history)
```

注意状态默认 1 天后过期以防内存无限增长；分布式部署可换成 Redis 后端（`FastMCP(..., session_state_store=RedisStore(...))`）。

## 七、第六步：选择传输方式并启动

FastMCP 把"传输"与"业务逻辑"彻底解耦——同一份工具定义，换传输只需改 `mcp.run()` 参数：

```python
if __name__ == "__main__":
    # STDIO（默认）：本地进程，适合 Claude Code / Claude Desktop
    mcp.run()

    # Streamable HTTP：远程/云端部署，多客户端共享
    # mcp.run(transport="http", host="127.0.0.1", port=8000)
```

| 传输 | 适用场景 | 接入方式 |
|------|----------|----------|
| **stdio** | 本机运行的私有工具，零网络开销 | `claude mcp add --transport stdio` |
| **streamable-http** | 云端托管、团队共享、需要鉴权 | `claude mcp add --transport http <url>` |
| **sse** | 旧标准，已被 http 取代，新项目别用 | 仅兼容老服务器时使用 |

本教程的实战项目用 **stdio**：本地启动、即时响应、无需鉴权，最适合个人工具。

## 八、第七步：用 `claude mcp` 三作用域接入 Claude Code

Claude Code 提供完整的 MCP 管理命令：

```bash
claude mcp add --transport stdio notes-mcp-server -- uv run --directory /abs/path/notes-mcp server.py
claude mcp list            # 查看所有已配置服务器
claude mcp get notes-mcp-server   # 查看单个详情与连接状态
claude mcp remove notes-mcp-server
```

`--` 之后是真正启动你服务器的命令。用 `uv run` 能保证在正确环境里执行；纯 pip 环境可写 `-- /abs/path/.venv/bin/python /abs/path/server.py`。**路径务必用绝对路径**，否则 Claude Code 在别的目录启动时会找不到文件。

三个**作用域**决定了配置存哪、谁能看到：

```bash
# local（默认）：只在你、当前项目生效，存 ~/.claude.json
claude mcp add --scope local  --transport stdio notes-mcp-server -- uv run ...

# project：存到项目根的 .mcp.json，可提交 git，团队共享
claude mcp add --scope project --transport stdio notes-mcp-server -- uv run ...

# user：跨所有项目对你生效
claude mcp add --scope user   --transport stdio notes-mcp-server -- uv run ...
```

`project` 作用域写入的 `.mcp.json` 长这样（也可手写）：

```json
{
  "mcpServers": {
    "notes-mcp-server": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "--directory", "/abs/path/notes-mcp", "server.py"]
    }
  }
}
```

配置文件支持 `${VAR}` 与 `${VAR:-默认值}` 环境变量展开，**密钥绝不硬编码进仓库**：

```json
"env": { "NOTES_API_KEY": "${NOTES_API_KEY}" }
```

接入后还有两个关键机制：

1. **工具命名约定**：MCP 工具名是 `mcp__<服务器名>__<工具名>`，例如 `mcp__notes-mcp-server__search_notes`。在 Claude Code 里用 `allowedTools: ["mcp__notes-mcp-server__*"]` 可一键放行该服务器全部工具，免去逐个授权。**推荐用 `allowedTools` 而非 `bypassPermissions`**——前者精确授权、其它安全提示照常生效。
2. **Tool Search（默认开启）**：会话启动时只加载工具名、按需加载工具定义，因此接几十个 MCP 服务器也不会撑爆上下文。需要精细控制时设 `ENABLE_TOOL_SEARCH=auto:5`。

验证接入：在项目里跑 `claude`，输入 `/mcp`，应看到 `notes-mcp-server` 为 connected，并列出 search_notes、add、summarize_topic 等组件。然后直接问："帮我搜索笔记库里和'架构'相关的标题"——模型会自动调用 `search_notes`。

## 实战案例：Markdown 笔记库检索 MCP 服务器

把前面所有部分串成一个完整、可直接跑的项目。功能：对一个本地 Markdown 笔记目录提供关键词检索、按标题读全文、统计、复用"总结某主题"提示词，并用 Context 记录会话检索历史。

目录结构：

```
notes-mcp/
├── server.py
├── notes/            # 你的 Markdown 笔记放这里
│   ├── 架构设计.md
│   └── 周会纪要.md
└── pyproject.toml
```

完整 `server.py`：

```python
from pathlib import Path
from collections import Counter
from fastmcp import FastMCP
from fastmcp.dependencies import CurrentContext
from fastmcp.server.context import Context

NOTES_DIR = Path(__file__).parent / "notes"
mcp = FastMCP(name="Notes MCP Server")


def _all_notes() -> dict[str, str]:
    """读取全部笔记，返回 {标题: 正文}。"""
    return {
        p.stem: p.read_text(encoding="utf-8")
        for p in NOTES_DIR.glob("*.md")
    }


# ---------- 工具 ----------

@mcp.tool
async def search_notes(keyword: str, ctx: Context = CurrentContext()) -> list[str]:
    """在笔记库中按关键词检索，返回命中的笔记标题列表。"""
    await ctx.info(f"检索关键词：{keyword}")
    hits = [t for t, body in _all_notes().items() if keyword in body]
    await ctx.report_progress(progress=len(hits), total=len(hits))

    history = await ctx.get_state("keywords") or []
    if keyword not in history:
        history.append(keyword)
    await ctx.set_state("keywords", history)
    return hits


@mcp.tool
async def note_stats() -> dict:
    """返回笔记库统计：篇数、总字数、词频 Top5。"""
    notes = _all_notes()
    words = Counter()
    for body in notes.values():
        words.update(body)
    return {
        "count": len(notes),
        "total_chars": sum(len(v) for v in notes.values()),
        "top_words": words.most_common(5),
    }


# ---------- 资源 ----------

@mcp.resource("notes://index")
def note_index() -> list[str]:
    """列出所有笔记标题。"""
    return list(_all_notes().keys())


@mcp.resource("notes://{title}")
def read_note(title: str) -> str:
    """按标题读取笔记正文。"""
    return _all_notes().get(title, "未找到该笔记")


# ---------- 提示词 ----------

@mcp.prompt
def summarize_topic(topic: str) -> str:
    """生成"总结某主题笔记"的提示。"""
    return (
        f"请先用 search_notes 检索「{topic}」，"
        "再逐篇读取命中笔记，最后输出三条关键结论并标注来源标题。"
    )


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

接入与验证：

```bash
# 1. 接入（替换为你的绝对路径）
claude mcp add --scope user --transport stdio notes-mcp-server \
  -- uv run --directory /Users/me/notes-mcp server.py

# 2. 在任意目录启动 Claude Code
claude

# 3. 会话内检查连接
/mcp

# 4. 自然语言调用
> 帮我搜笔记库里提到"架构"的标题，并总结要点
```

模型会先调 `search_notes("架构")`，再用 `notes://架构设计` 读全文，最后综合输出——整个过程你只说了一句话。

## 工作流程图

```mermaid
flowchart TD
    A[用户在 Claude Code 提问] --> B[Claude 解析意图]
    B --> C{需要外部能力?}
    C -->|是| D[查找已接入的 MCP 服务器]
    D --> E[mcp__notes-mcp-server__search_notes]
    E --> F[FastMCP 服务器收到 JSON-RPC 调用]
    F --> G[执行被装饰的 Python 函数]
    G --> H[经 Context 上报日志/进度/状态]
    H --> I[函数返回结构化结果]
    I --> J[FastMCP 序列化为 MCP 响应]
    J --> K[Claude 拿到工具结果]
    K --> L[综合生成最终回答]
    L --> M[展示给用户]
    C -->|否| L

    style F fill:#e1f5ff,stroke:#0288d1
    style G fill:#fff4e1,stroke:#f57c00
    style H fill:#f3e5f5,stroke:#7b1fa2
```

## 常见问题与排查

**1. `/mcp` 显示服务器 "not connected"。**
stdio 服务器多半是启动命令有问题。先在终端单独跑 `uv run --directory /abs/path server.py`，确认能正常挂起无报错；再核对 `claude mcp get notes-mcp-server` 里的 command/args 与绝对路径是否一致。Windows 原生（非 WSL）下 `npx` 类命令需套 `cmd /c`。

**2. 工具没出现在列表里。**
确认服务器已 connected；若 connected 却零工具，检查函数是否加了 `@mcp.tool`、是否有类型注解（无注解的参数无法生成 Schema）。会话中途新加的服务器要**重启会话**才能发现其工具。

**3. 工具被调用但报"权限拒绝"或静默失败。**
Claude Code 调用工具前要授权，一旦在会话内拒绝过就不再询问。用 `/permissions` 复核，或新开会话；想免确认就在配置里加 `allowedTools: ["mcp__notes-mcp-server__*"]`。

**4. 工具输出过大被截断。**
默认单次工具输出上限 25,000 tokens。解法有二：在服务器侧把大结果分页/裁剪后返回；或在客户端设 `export MAX_MCP_OUTPUT_TOKENS=50000`。服务器作者还可在工具上设 `_meta["anthropic/maxResultSizeChars"]`（最高 50 万字符）单独放宽。

**5. 开发期如何快速调试？**
用官方检查器可视化交互：`fastmcp dev server.py`（或 `npx @modelcontextprotocol/inspector`）。它打开一个网页面板，能手动调用每个工具、查看原始 JSON-RPC 报文，定位是 Schema 问题还是业务逻辑问题。注意：调试时 `print()` 会污染 stdio 通道，**日志务必走 `ctx.info(...)`** 而非 `print`。

## 延伸阅读

- **FastMCP 官方文档** — `gofastmcp.com`，工具/资源/提示词/Context 的完整 API 与进阶（依赖注入、中间件、挂载子服务器）。
- **MCP 协议规范** — `modelcontextprotocol.io/specification`，理解 JSON-RPC、能力协商、elicitation/sampling 等底层机制。
- **Claude Code MCP 接入文档** — `code.claude.com/docs/en/mcp`，`claude mcp` 全部子命令、作用域与 Tool Search 配置。
- **FastMCP PyPI** — `pypi.org/project/fastmcp`，版本与安装选项。

## 训练营适用性评估

- **适合类型**：AI 应用开发训练营、LLM 工程实战营、企业内部"AI 提效工具"工作坊。偏后端/工具链方向，需要学员有基础 Python 能力。
- **预计时长**：讲师演示 + 学员动手约 **3–4 小时**。其中环境与骨架 30 分钟、四类组件（工具/资源/提示词/Context）各 30 分钟、接入与调试 45 分钟、实战项目 60 分钟。
- **可拓展模块**：可追加"把 HTTP API 封装成工具""用 Redis 做会话状态""Streamable HTTP 远程部署 + OAuth 鉴权"等进阶半天课程，与本教程形成系列。
- **考核建议**：要求学员交付一个接入 Claude Code 后能用自然语言驱动的、至少含 2 个工具 + 1 个资源 + 1 个提示词的 MCP 服务器，并现场演示 `/mcp` 连接与调用链路。

## 参考来源

1. [How to Create an MCP Server in Python — FastMCP](https://gofastmcp.com/tutorials/create-mcp-server) — FastMCP 官方五步入门，本教程"搭建骨架/工具/资源/模板/运行"步骤的直接依据。
2. [MCP Context — FastMCP](https://gofastmcp.com/servers/context) — Context 对象官方文档，日志、进度、会话状态、elicit/sample 等 API 细节来源。
3. [Claude Code MCP Commands — Quick Reference](https://www.mcpbundles.com/blog/claude-code-mcp-tools) — `claude mcp add/list/get/remove`、三种作用域、`.mcp.json`、Tool Search、输出上限与排查的速查依据。
4. [Connect to external tools with MCP — Claude Code Docs](https://code.claude.com/docs/en/agent-sdk/mcp) — 传输类型（stdio/http/sse）、`mcp__<server>__<tool>` 命名约定、`allowedTools` 授权策略的官方说明。
5. [fastmcp · PyPI](https://pypi.org/project/fastmcp/) — FastMCP 安装方式与版本信息。

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