<!-- title: 造PDF智能处理MCP让AI读懂任意文档 -->

本教程手把手带你用 Python + FastMCP 从零构建一个 PDF 智能处理 MCP 服务器，让 Claude Code 能够提取文本、读取元数据、全文搜索、拆分与合并 PDF。适合已了解 MCP 基本概念、想做出第一个真正"有用且可复用"自定义工具的开发者。

## 一、前置条件

在开始之前，请确认你的环境满足以下条件：

- **Python 3.10+**（推荐 3.11 或 3.12）。MCP Python SDK 官方最低要求 3.10。
- **pip 或 uv** 包管理器。本教程使用 `uv`，它是目前 MCP 生态推荐的速度更快的替代方案；用 `pip` 同样可以，命令等效。
- **Claude Code** 已安装并能正常运行（`claude --version` 有输出）。
- 一份用于测试的 PDF 文件。可以是任意 PDF——产品手册、论文、合同、发票均可。本教程的示例 PDF 假设为一份多页技术报告。
- **基础 Python 知识**：会写函数、懂类型注解（`def foo(x: int) -> str`）、知道虚拟环境是什么。

你**不需要**事先精通 MCP 协议细节——本教程会按需讲解。但如果你完全没听过 MCP，建议先读一遍官方的 "Build an MCP server" 入门文档再回来。

## 二、为什么用 MCP 而不是直接让 Claude 跑脚本

你可能会想：让 Claude 直接写一段 `pypdf` 脚本处理 PDF 不就行了，为什么要专门造一个 MCP 服务器？

核心区别在于**稳定性与复用性**。

- **临时脚本**：每次都要让模型重新写、重新读、重新调试。模型可能选错库（`pypdf` vs `PyMuPDF`），可能把页码搞混，可能在多文件场景下手忙脚乱。更关键的是，这些"能力"无法跨会话沉淀。
- **MCP 服务器**：把 PDF 能力封装成一组**命名清晰、参数明确、有文档说明**的工具。Claude 一旦接入，就像多了一双永远在线、行为可预测的手。同一个服务器可以被 Claude Code、Claude Desktop、任何兼容 MCP 的客户端复用。

一句话总结：**MCP 把"一次性魔法"变成"可复用的基础设施"**。这正是本教程要教的核心思维——不是写脚本，而是造工具。

## 三、MCP 三大原语：Tool / Resource / Prompt 速览

在动手前，用 30 秒厘清 MCP 的三个核心概念，它们决定了你的服务器长什么样：

| 原语 | 作用 | 触发方式 | 本教程中的例子 |
|------|------|----------|----------------|
| **Tool（工具）** | 模型可以"调用执行"的函数，有副作用，返回结果 | 模型主动决定调用 | `extract_text`、`search_pdf` |
| **Resource（资源）** | 模型可以"读取"的数据，类似文件，只读为主 | 客户端/用户引用 URI | `pdf://report/meta` |
| **Prompt（提示模板）** | 预置的提示词模板，带参数 | 用户从菜单选择 | "总结这份 PDF" |

一个设计良好的 MCP 服务器通常三者结合：**Tool 干活，Resource 暴露数据，Prompt 引导用法**。本教程会三个都覆盖，让你做出"完整"而非"半成品"的服务器。

## 四、MCP Python SDK 现状（2026 年 7 月重要提醒）

在安装前，必须了解 SDK 当前状态，避免踩坑：

- **v1.x 是唯一稳定的生产线**，使用 `FastMCP` 类，`pip install "mcp[cli]"` 默认装到的就是这个。本教程基于 v1.x，因为它最稳定、文档最全。
- **v2 正在预发布**（`2.0.0bN`），为支持 2026-07-28 的 MCP 规范发布做重大重构，类名改为 `MCPServer`，**每个预发布版本之间可能有破坏性变更**，官方明确说"不要用于生产"。
- 关键建议：如果你的 `pyproject.toml` 依赖 `mcp`，请加上上限约束 `mcp>=1.27,<2`，避免将来 stable v2 发布时自动升级把你的代码搞崩。

本教程使用 v1.x 的 FastMCP API。等 v2 stable 后迁移成本很低，迁移指南官方会提供。

## 五、步骤一：搭建项目骨架

先建一个干净的项目目录。打开终端：

```bash
# 1. 创建项目目录
mkdir pdf-mcp && cd pdf-mcp

# 2. 用 uv 初始化（没有 uv 可跳过，手动建文件）
uv init --python 3.11

# 3. 安装依赖
uv add "mcp[cli]>=1.27,<2" pypdf
```

**为什么选 `pypdf`？** 它是纯 Python 实现、无系统级二进制依赖、跨平台、Apache 协议，最适合做教学和通用工具。生产环境若需更高性能或图片提取，可换 `PyMuPDF`（`pip install pymupdf`），接口稍异但思路一致。

安装完成后，确认版本：

```bash
uv run python -c "import mcp, pypdf; print('mcp', mcp.__version__); print('pypdf', pypdf.__version__)"
```

**预期结果**：打印出 `mcp 1.x.x` 和 `pypdf 5.x.x` 之类的版本号，无报错即安装成功。

**常见问题**：

- **`uv: command not found`**：用 `pip install uv` 或参考 uv 官方安装脚本。也可以全程用 `pip` + `venv` 替代。
- **安装 pypdf 报错**：升级 pip（`pip install --upgrade pip`）后重试；pypdf 是纯 Python，几乎不会装不上。

## 六、步骤二：实现第一个工具 —— 提取文本

在项目根目录创建 `server.py`，从最简单的"提取 PDF 文本"开始：

```python
# server.py
from pathlib import Path
from pypdf import PdfReader
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("pdf-tools")


@mcp.tool()
def extract_text(pdf_path: str, max_pages: int = 0) -> str:
    """提取 PDF 的文本内容。

    Args:
        pdf_path: PDF 文件的绝对路径或相对路径。
        max_pages: 最多提取的页数。0 表示提取全部。
                   大文件建议传 5~20，避免一次性返回过多文本。
    """
    path = Path(pdf_path).expanduser()
    if not path.exists():
        return f"错误：文件不存在 {path}"

    reader = PdfReader(str(path))
    pages = reader.pages[:max_pages] if max_pages > 0 else reader.pages
    chunks = []
    for i, page in enumerate(pages, start=1):
        text = page.extract_text() or ""
        chunks.append(f"--- 第 {i} 页 ---\n{text.strip()}")
    return "\n\n".join(chunks)


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

**关键点解析**：

1. `FastMCP("pdf-tools")` 创建服务器实例，`"pdf-tools"` 是服务器名，会显示在客户端里。
2. `@mcp.tool()` 把普通 Python 函数变成 MCP 工具。**类型注解就是参数 schema，docstring 就是工具说明**——你不需要手写 JSON Schema，FastMCP 自动从注解和文档字符串生成。
3. `mcp.run(transport="stdio")` 用 stdio 传输启动，这是 Claude Code 接入本地 MCP 服务器的标准方式。
4. 错误处理返回字符串而非抛异常——这样模型能"读懂"错误并自我修正，而不是整个调用崩溃。

**预期结果**：先别急着接 Claude，我们用 MCP Inspector 单测。

## 七、步骤三：用 Inspector 独立测试（强烈推荐）

MCP Inspector 是官方提供的可视化调试工具，能在不接 Claude 的情况下手动调用你的工具，是开发期最省时间的利器。

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

这会启动一个本地 Web 界面（通常是 `http://localhost:5173`），自动加载你的 `server.py`。

**操作步骤**：

1. 在左侧看到 `pdf-tools` 服务器已连接，工具列表里出现 `extract_text`。
2. 点击 `extract_text`，右侧出现参数表单（自动从类型注解生成：`pdf_path` 文本框 + `max_pages` 数字框）。
3. 填入测试 PDF 路径，点 **Run Tool**。
4. 下方显示返回的文本。

**常见问题**：

- **页面打不开**：检查端口是否被占用，Inspector 偶尔会用 6274 等端口，看终端提示。
- **工具没出现**：99% 是 `server.py` 有语法错误或导入失败。回到终端看 Inspector 的日志输出，通常会直接打印 Python 报错。
- **返回空文本**：某些 PDF（尤其扫描件）没有文字层，`extract_text` 返回空。这是 PDF 本身的问题，需要 OCR 才能解决，后面延伸阅读会提到。

**这条调试链路务必养成习惯**：写一个工具 → Inspector 单测 → 再写下一个。比"写完一堆代码再接 Claude 排查"高效十倍。

## 八、步骤四：补齐核心工具集

现在一次性补上元数据、全文搜索、拆分、合并四个工具，构成一个完整的能力闭环。在 `server.py` 中 `extract_text` 之后追加：

```python
@mcp.tool()
def get_metadata(pdf_path: str) -> str:
    """读取 PDF 元数据（标题、作者、页数、创建时间等）。"""
    path = Path(pdf_path).expanduser()
    if not path.exists():
        return f"错误：文件不存在 {path}"

    reader = PdfReader(str(path))
    meta = reader.metadata or {}
    fields = {
        "页数": len(reader.pages),
        "标题": meta.get("/Title", "（无）"),
        "作者": meta.get("/Author", "（无）"),
        "主题": meta.get("/Subject", "（无）"),
        "创建时间": meta.get("/CreationDate", "（无）"),
        "是否加密": reader.is_encrypted,
    }
    return "\n".join(f"{k}: {v}" for k, v in fields.items())


@mcp.tool()
def search_pdf(pdf_path: str, keyword: str) -> str:
    """在 PDF 中全文搜索关键词，返回所在页码和上下文片段。"""
    path = Path(pdf_path).expanduser()
    if not path.exists():
        return f"错误：文件不存在 {path}"

    reader = PdfReader(str(path))
    hits = []
    for i, page in enumerate(reader.pages, start=1):
        text = page.extract_text() or ""
        low = text.lower()
        kw = keyword.lower()
        start = 0
        while True:
            idx = low.find(kw, start)
            if idx == -1:
                break
            snippet = text[max(0, idx - 30): idx + len(keyword) + 30]
            hits.append(f"第 {i} 页: ...{snippet}...")
            start = idx + len(keyword)
        if len(hits) >= 50:  # 防止超大输出
            hits.append("（命中过多，仅显示前 50 条）")
            break
    return "\n".join(hits) if hits else f"未找到关键词「{keyword}」"


@mcp.tool()
def split_pdf(pdf_path: str, start_page: int, end_page: int, output_path: str) -> str:
    """把 PDF 的指定页码范围拆分成一个新文件。

    Args:
        start_page: 起始页码，从 1 开始。
        end_page: 结束页码（含）。
        output_path: 输出 PDF 路径。
    """
    path = Path(pdf_path).expanduser()
    if not path.exists():
        return f"错误：源文件不存在 {path}"

    from pypdf import PdfWriter
    reader = PdfReader(str(path))
    total = len(reader.pages)
    if start_page < 1 or end_page > total or start_page > end_page:
        return f"错误：页码范围无效（共 {total} 页）"

    writer = PdfWriter()
    for p in range(start_page - 1, end_page):
        writer.add_page(reader.pages[p])
    out = Path(output_path).expanduser()
    out.parent.mkdir(parents=True, exist_ok=True)
    with open(out, "wb") as f:
        writer.write(f)
    return f"已生成 {out}（第 {start_page}-{end_page} 页，共 {end_page - start_page + 1} 页）"


@mcp.tool()
def merge_pdfs(pdf_paths: list[str], output_path: str) -> str:
    """把多个 PDF 按顺序合并为一个文件。

    Args:
        pdf_paths: 源 PDF 路径列表，按合并顺序排列。
        output_path: 合并后的输出路径。
    """
    from pypdf import PdfWriter
    writer = PdfWriter()
    resolved = [Path(p).expanduser() for p in pdf_paths]
    for p in resolved:
        if not p.exists():
            return f"错误：文件不存在 {p}"
    for p in resolved:
        writer.append(str(p))
    out = Path(output_path).expanduser()
    out.parent.mkdir(parents=True, exist_ok=True)
    with open(out, "wb") as f:
        writer.write(f)
    return f"已合并 {len(resolved)} 个文件到 {out}"
```

**设计要点**：

- `list[str]` 这种容器类型注解也能被 FastMCP 正确识别为"字符串数组"参数，模型会传入 JSON 数组。
- 文件写操作（split/merge）都做了 `mkdir(parents=True, exist_ok=True)`，避免目录不存在报错。
- `search_pdf` 限制最多返回 50 条命中并给出提示——这是 MCP 工具设计的"防过载"最佳实践，避免一次性把几十页命中结果灌进模型上下文。

写完后回 Inspector 把这四个工具都点一遍，确认行为符合预期再继续。

## 九、步骤五：暴露 Resource（资源）

工具是"干活"，资源是"摆数据"。我们把 PDF 的元数据暴露成一个可被引用的资源，让客户端能像读文件一样读取它，而不必每次调用工具：

```python
# 在 server.py 中，记录"当前工作目录"作为可被操作 PDF 的根
WORKDIR = Path.home() / "pdf-workspace"
WORKDIR.mkdir(parents=True, exist_ok=True)


@mcp.resource("pdf://list")
def list_workspace_pdfs() -> str:
    """列出工作区中所有 PDF 文件及其页数。"""
    lines = []
    for pdf in sorted(WORKDIR.glob("*.pdf")):
        try:
            n = len(PdfReader(str(pdf)).pages)
        except Exception:
            n = "?"
        lines.append(f"- {pdf.name}（{n} 页）")
    return "\n".join(lines) if lines else "工作区暂无 PDF"


@mcp.resource("pdf://{filename}/meta")
def get_pdf_resource_meta(filename: str) -> str:
    """通过 URI 读取指定 PDF 的元数据，例如 pdf://report.pdf/meta。"""
    target = (WORKDIR / filename).resolve()
    # 防路径穿越：必须落在工作区内
    if not str(target).startswith(str(WORKDIR)):
        return "错误：越权访问"
    return get_metadata(str(target))
```

**这里有两个进阶知识点**：

1. **URI 模板**（`pdf://{filename}/meta`）：花括号表示动态参数，客户端可以传入 `pdf://report.pdf/meta` 来读取"report.pdf 的元数据"。这是 MCP 资源的高级用法——一个函数服务一族资源。
2. **路径穿越防护**：`{filename}` 理论上可能被传入 `../../etc/passwd`。代码里用 `.resolve()` 后检查是否仍在 `WORKDIR` 下，是所有"文件型 MCP 服务器"必须做的安全闸。

## 十、步骤六：加一个 Prompt（提示模板）

最后补一个提示模板，让用户能一键触发"总结这份 PDF"的标准流程：

```python
@mcp.prompt()
def summarize_pdf(pdf_path: str) -> str:
    """生成"总结一份 PDF"的提示词，引导模型先取元数据再分页提取。"""
    return f"""请帮我总结这份 PDF：{pdf_path}

建议流程：
1. 先调用 get_metadata 了解页数和基本信息；
2. 调用 extract_text 分批提取文本（每次不超过 10 页）；
3. 如果是长文档，用 search_pdf 定位关键章节；
4. 最后给出结构化摘要：核心结论、关键数据、章节大纲。"""
```

Prompt 的价值在于**固化最佳实践**：不依赖用户记得"先看元数据再分页"，模板直接把流程写进去，模型会照着执行。

## 十一、步骤七：接入 Claude Code

服务器开发完成，现在把它接到 Claude Code。在你的 `~/.claude.json`（或项目级 `.mcp.json`）中添加服务器配置。推荐用命令行一键添加：

```bash
claude mcp add pdf-tools -- uv --directory /绝对路径/pdf-mcp run server.py
```

注意几个要点：

- `--directory` 后面必须用**绝对路径**，否则 Claude Code 找不到你的项目目录。
- 用 `uv run` 而非 `python server.py`，确保用对虚拟环境和依赖。
- `--` 分隔符之后是真正的启动命令。

**验证接入成功**：

```bash
claude mcp list
```

应看到 `pdf-tools` 出现在列表中且状态为 connected。然后在 Claude Code 对话里输入：

> /mcp

能看到 `pdf-tools` 下的 5 个工具、2 个资源、1 个提示词。

**常见问题**：

- **状态显示 failed**：多半是路径问题或依赖没装在 `uv` 管理的环境里。手动跑一遍 `uv --directory /路径/pdf-mcp run server.py`，看是否有 Python 报错。
- **工具出现了但调用报错**：检查 PDF 路径权限、是否扫描件（无文字层）。
- **Windows 下的 `~` 路径**：用 `%USERPROFILE%` 或绝对路径，不要用波浪线。

## 十二、实战案例：用 MCP 读完一份 80 页技术报告

现在用一个端到端的真实场景，体验这套工具的威力。假设 `~/pdf-workspace/annual-report.pdf` 是一份 80 页的年度技术报告，你想快速了解它讲了什么。

**第一步：自然语言提问**

在 Claude Code 中直接说：

> "帮我了解 pdf-workspace 里的 annual-report.pdf 大概讲了什么，重点找 5G 核心网相关内容。"

**第二步：观察 Claude 自主编排**

Claude 会自动规划调用序列（这就是 MCP 的魅力——模型自己决定怎么组合工具）：

1. 先读资源 `pdf://list` 确认文件存在和页数。
2. 调 `get_metadata` 看基本信息。
3. 调 `search_pdf` 搜 "5G"、"核心网" 等关键词，定位相关页码。
4. 调 `extract_text` 精读命中的那几页（`max_pages` 控制范围）。
5. 综合输出结构化摘要。

**第三步：扩展操作**

你继续追问：

> "把第 12 到 18 页（核心网架构图所在）单独拆出来发我，再和另外那份 nfv.pdf 合并成一份参考资料。"

Claude 会调 `split_pdf` + `merge_pdfs` 完成文件操作，并告诉你输出路径。整个过程你**没有写一行 Python**，全部由 MCP 工具完成——而这些能力从此永久存在于你的 Claude Code 里，下次任何 PDF 都能直接处理。

对比一下：如果没有 MCP，你要么手动用软件操作，要么让 Claude 现写脚本、现调库、现试错。前者慢，后者不可复现。MCP 把它变成了一个"开箱即用、行为稳定的能力"。

## 十三、整体工作流架构

下面这张图展示了从开发到使用的完整链路：

```mermaid
flowchart LR
    subgraph 开发期
        A[server.py<br/>FastMCP] -->|@mcp.tool<br/>@mcp.resource<br/>@mcp.prompt| B[5工具+2资源+1提示]
        B --> C[MCP Inspector<br/>可视化单测]
        C -->|通过| A
    end
    subgraph 接入期
        B --> D[claude mcp add]
        D --> E["~/.claude.json<br/>注册配置"]
    end
    subgraph 使用期
        F[用户自然语言提问] --> G[Claude Code]
        E --> G
        G -->|自动编排调用| B
        B -->|操作| H[(本地 PDF 文件)]
        H --> B
        B -->|结构化结果| G
        G --> F
    end
```

## 十四、进阶优化方向

把基础版跑通后，可以从这几个方向迭代：

1. **换 PyMuPDF 提升能力**：`pypdf` 对复杂排版、表格提取较弱。换 `import fitz`（PyMuPDF）能拿到更干净的文本、支持图片提取，速度也快几倍。接口封装在 `extract_text` 内部，对外工具签名不变。
2. **加 OCR 兜底扫描件**：对 `extract_text` 返回空的页，自动调 `pytesseract` 做图像 OCR。这是扫描合同、老档案的刚需。
3. **加 Streamable HTTP 远程化**：把 `mcp.run(transport="stdio")` 改成 HTTP 传输，部署到内网，让团队所有人共享同一个 PDF 工具服务器，而非各自本地装。注意加鉴权。
4. **加缓存**：对同一个 PDF 的 `extract_text` 结果按文件 hash 缓存，避免重复解析几百页的大文件。

## 十五、延伸阅读

- **MCP 官方入门**：[Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) — 官方手把手教程，概念权威来源。
- **MCP Python SDK**：[github.com/modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) — 看 README 了解 v1.x 与 v2 的差异，迁移指南在此。
- **FastMCP 文档**：[gofastmcp.com](https://gofastmcp.com/) — FastMCP 框架的完整文档，Tools/Resources/Prompts 三个原语的深度用法。
- **MCP Inspector**：[github.com/modelcontextprotocol/inspector](https://github.com/modelcontextprotocol/inspector) — 开发期调试神器，本教程步骤三的核心工具。
- **参考服务器集**：[github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) — 官方维护的参考实现（filesystem、fetch 等），读源码学架构的最佳范例。

## 十六、训练营适用性评估

- **适合训练营类型**：MCP 开发训练营、AI 工具链工程师训练营、Python 进阶训练营。定位为"第二个 MCP 项目"——学员应已做过至少一个 Hello World 级 MCP。
- **教学时长**：建议 4 课时。课时 1 讲三原语与 SDK 现状；课时 2 实现 extract_text + Inspector 单测；课时 3 补齐工具集与 Resource/Prompt；课时 4 接入 Claude Code 做端到端实战。
- **考核方式**：学员需独立交付一个能处理真实 PDF、通过 Inspector 全工具验证、并成功接入 Claude Code 完成一次"搜索+拆分"任务的 MCP 服务器。
- **难度系数**：中（3/5）。难点不在 PDF 处理，而在理解 Tool/Resource/Prompt 的分工、URI 模板、以及路径安全防护。

## 参考来源

1. [Build an MCP server — Model Context Protocol](https://modelcontextprotocol.io/docs/develop/build-server) — MCP 官方服务器构建入门文档
2. [MCP Python SDK — GitHub](https://github.com/modelcontextprotocol/python-sdk) — 官方 Python SDK 源码与 v1.x/v2 版本说明
3. [FastMCP — PyPI](https://pypi.org/project/fastmcp/) — FastMCP 框架 PyPI 主页，工具/资源/提示词装饰器 API
4. [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/) — 含 Docker 容器化部署的完整实战
5. [FastMCP in Python: Build a Real MCP Server (2026 Guide)](https://www.danilchenko.dev/posts/fastmcp-mcp-server/) — FastMCP 3.x 生产级服务器构建指南

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