首页 › tutorials › 造PDF智能处理MCP让AI读懂任意文档

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

一、前置条件

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

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

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

你可能会想:让 Claude 直接写一段 pypdf 脚本处理 PDF 不就行了,为什么要专门造一个 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 API。等 v2 stable 后迁移成本很低,迁移指南官方会提供。

五、步骤一:搭建项目骨架

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

# 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),接口稍异但思路一致。

安装完成后,确认版本:

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

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

常见问题:

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

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

# 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 的情况下手动调用你的工具,是开发期最省时间的利器。

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 单测 → 再写下一个。比"写完一堆代码再接 Claude 排查"高效十倍。

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

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

@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}"

设计要点:

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

九、步骤五:暴露 Resource(资源)

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

# 在 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"的标准流程:

@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)中添加服务器配置。推荐用命令行一键添加:

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

注意几个要点:

验证接入成功:

claude mcp list

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

/mcp

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

常见问题:

十二、实战案例:用 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 把它变成了一个"开箱即用、行为稳定的能力"。

十三、整体工作流架构

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

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 缓存,避免重复解析几百页的大文件。

十五、延伸阅读

十六、训练营适用性评估

参考来源

  1. Build an MCP server — Model Context Protocol — MCP 官方服务器构建入门文档
  2. MCP Python SDK — GitHub — 官方 Python SDK 源码与 v1.x/v2 版本说明
  3. FastMCP — PyPI — FastMCP 框架 PyPI 主页,工具/资源/提示词装饰器 API
  4. How to Build an MCP Server with Python, Docker, and Claude Code — freeCodeCamp — 含 Docker 容器化部署的完整实战
  5. FastMCP in Python: Build a Real MCP Server (2026 Guide) — 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学通信知识星球海报-扫码加入