首页 › tutorials › Python造MCP三原语服务器

一句话定位

用 Python + FastMCP 从零造一个同时暴露 Tools(工具)、Resources(资源)、Prompts(提示词)三大原语的 MCP 服务器,接入 Claude Code 让模型读你的代码库、执行扫描、套用审查模板——一次打通 MCP 全部能力,而不是只写一个 Tool 凑数。

适用人群与学习目标

如果你已经能用 Claude Code 跑通官方或第三方的 MCP 服务器,但自己写时只会堆 @mcp.tool(),那就读这篇。学完你将掌握:

前置条件

正文

步骤 0:先想清楚三大原语到底差在哪

很多人写 MCP 服务器翻车,不是代码写错,而是把所有东西都做成 Tool。MCP 之所以分成三类原语,是因为它们的控制权归属不同——这是协议设计的第一性原理:

原语 谁决定调用 类比 HTTP 有副作用吗 典型场景
Tools 模型自主决定 POST 通常有 查数据库、发请求、跑脚本
Resources 应用/用户选择 GET 无 读文件、看 schema、拉配置
Prompts 用户主动选 模板 无 代码审查、提交规范、Bug 复盘

记住这个心智模型:Tool 让模型「动手」,Resource 让模型「看见」,Prompt 给模型「剧本」。一个设计良好的 MCP 服务器应该三者分工,而不是用 Tool 模拟一切(比如用 get_file 这种 Tool 去读静态数据,就是典型的反模式——那本该是 Resource 的活)。

FastMCP 3.x 把这三类原语都收敛成装饰器:@mcp.tool()、@mcp.resource()、@mcp.prompt(),注册一个能力只需一行。

步骤 1:搭项目骨架并装依赖

mkdir repo-analyzer-mcp && cd repo-analyzer-mcp
uv init --no-readme           # 或 python -m venv .venv && source .venv/bin/activate
uv add "fastmcp>=2.3"         # pip install "fastmcp>=2.3"

建立如下目录结构:

repo-analyzer-mcp/
├── pyproject.toml
└── repo_analyzer/
    └── server.py

预期结果:uv add 成功后,pyproject.toml 会出现 fastmcp 依赖。FastMCP 是官方 Python SDK 之上的高层封装,是目前写 MCP 服务器最省事的姿势——比手写 JSON-RPC 样板少 80% 代码。

常见问题: - 报 No module named 'fastmcp':确认激活了虚拟环境,旧文里常见的 from mcp.server.fastmcp import FastMCP 是 1.x 写法,2.x 起独立成包,直接 from fastmcp import FastMCP。 - 装成全局污染环境:永远在虚拟环境里装。

步骤 2:注册第一个 Tool(让模型动手)

Tool 是模型可调用的函数。FastMCP 用类型注解自动生成参数 schema,用docstring作为给模型看的描述——这两者决定了模型能不能正确选对你的工具。

# repo_analyzer/server.py
from fastmcp import FastMCP
from pathlib import Path
import os, json

mcp = FastMCP("repo-analyzer")
REPO_ROOT = Path(os.environ.get("REPO_ROOT", ".")).resolve()

@mcp.tool()
def count_lines(pattern: str = "**/*.py") -> dict:
    """统计仓库内匹配 pattern 的文件总数与代码总行数。
    pattern 例如 "**/*.py" 或 "src/**/*.ts"。"""
    files = total = 0
    for p in REPO_ROOT.glob(pattern):
        if p.is_file():
            total += sum(1 for _ in p.open(encoding="utf-8", errors="ignore"))
            files += 1
    return {"files": files, "lines": total}

要点:函数名就是工具名,docstring 务必写清楚「这个工具干什么、参数怎么填」,模型据此决定是否调用。返回值用 dict/list 而非手拼字符串,FastMCP 会序列化成结构化结果。

步骤 3:用 Resource 暴露只读数据(让模型看见)

Resource 是应用层选择、模型可读取的数据。它支持静态 URI 和带参数的动态模板两种。下面同时演示两种:

@mcp.resource("repo://manifest")
def manifest() -> str:
    """仓库清单:顶层目录与文件,供模型快速了解结构。"""
    entries = [str(p.relative_to(REPO_ROOT)) for p in sorted(REPO_ROOT.iterdir())]
    return json.dumps({"root": str(REPO_ROOT), "entries": entries}, ensure_ascii=False)

@mcp.resource("repo://file://{path}")
def read_file(path: str) -> str:
    """读取仓库内任意文本文件。{path} 是相对仓库根的路径。"""
    target = (REPO_ROOT / path).resolve()
    # 防目录穿越:禁止跳出仓库根
    if not str(target).startswith(str(REPO_ROOT)):
        raise ValueError(f"路径越界: {path}")
    return target.read_text(encoding="utf-8", errors="ignore")

预期结果:客户端能看到两个资源——repo://manifest 和一个 URI 模板 repo://file://{path}。模型可以列出资源、按模板读取具体文件。

常见问题:动态模板里花括号变量名要和函数参数名一致;uri:// 协议头是自定义的,只要不与现有冲突即可,但建议语义化。

步骤 4:用 Prompt 提供可复用剧本

Prompt 是带参数的提示词模板,由用户主动插入对话。它最适合沉淀团队反复用的「提问套路」,比如代码审查、提交信息、事故复盘。

@mcp.prompt()
def review_file(path: str) -> str:
    """生成针对指定文件的代码审查提示词,引导模型按固定维度出报告。"""
    return f"""请审查仓库中 {path} 文件,按以下维度输出:
1. 潜在 bug 与边界条件漏洞
2. 可读性与命名改进建议
3. 修复后的关键代码片段(diff 形式)
开始前请先用 repo://file://{path} 资源读取真实内容,不要臆测。"""

与 Tool 的关键区别:Tool 是模型在解决问题时自己决定调用的;Prompt 是用户在「/」斜杠菜单里主动选的,本质是一段预制的、参数化的人机沟通脚本。把团队代码规范塞进 Prompt,比每次手动写一大段提问高效得多。

步骤 5:注入 Context 做进度上报与日志

长任务里,模型(和用户)需要知道进度。FastMCP 提供 Context 对象,通过类型注解自动注入:

from fastmcp import Context

@mcp.tool()
async def deep_scan(ctx: Context, pattern: str = "**/*.py") -> dict:
    """深度扫描,带实时进度上报与日志。"""
    files = [p for p in REPO_ROOT.glob(pattern) if p.is_file()]
    todos = fixmes = 0
    for i, p in enumerate(files, 1):
        await ctx.report_progress(i, len(files))   # 进度条
        await ctx.info(f"已扫描 {p.name}")           # 日志
        for line in p.open(encoding="utf-8", errors="ignore"):
            if "TODO" in line: todos += 1
            if "FIXME" in line: fixmes += 1
    return {"scanned": len(files), "todos": todos, "fixmes": fixmes}

要点:带 Context 参数的工具要写成 async def;ctx 还能 read_resource() 读取本服务器暴露的资源、log.warning() 打不同级别日志。不要在 docstring 里描述 ctx 参数——它是框架注入的,不该出现在给模型的 schema 里。

步骤 6:用 stdio 启动并接入 Claude Code

在文件末尾加启动入口:

if __name__ == "__main__":
    mcp.run()        # 默认 stdio 传输,最适合本地编辑器集成

在待分析的仓库根目录创建 .mcp.json(项目级)或写入 ~/.claude.json(用户级):

{
  "mcpServers": {
    "repo-analyzer": {
      "command": "python",
      "args": ["-m", "repo_analyzer.server"],
      "env": { "REPO_ROOT": "/绝对路径/你的仓库" }
    }
  }
}

注意:env.REPO_ROOT 用绝对路径;python -m 需要 repo_analyzer/__init__.py 存在(空文件即可)。若用 uv 跑,command 换成 uv、args 前面加 ["run","python","-m",...]。

预期结果:在仓库目录启动 Claude Code,输入 /mcp 应看到 repo-analyzer 为 connected,并列出 repo://manifest、三个工具、一个 prompt。

步骤 7:用 MCP Inspector 独立验证

不要只靠 Claude Code 验证——它把 MCP 错误吞得很深。用官方可视化调试器独立测每个原语:

npx @modelcontextprotocol/inspector python -m repo_analyzer.server

浏览器打开后,在左侧 Tools / Resources / Prompts 三个标签分别点调用。这是定位「到底是服务器的问题还是模型没选对工具」的黄金手段:Inspector 里能调通、Claude 里调不通,多半是 docstring 不够清晰或 schema 有歧义。

实战案例:完整的代码仓库分析服务器

把上面片段合起来,就是一个可交付的实战项目——让 Claude Code 变成「懂你这个仓库」的代码分析师:

# repo_analyzer/server.py
from fastmcp import FastMCP, Context
from pathlib import Path
import os, json

mcp = FastMCP("repo-analyzer")
REPO_ROOT = Path(os.environ.get("REPO_ROOT", ".")).resolve()

# ---------- Tools:执行动作 ----------
@mcp.tool()
def count_lines(pattern: str = "**/*.py") -> dict:
    """统计匹配 pattern 的文件数与总行数。"""
    files = total = 0
    for p in REPO_ROOT.glob(pattern):
        if p.is_file():
            total += sum(1 for _ in p.open(encoding="utf-8", errors="ignore"))
            files += 1
    return {"files": files, "lines": total}

@mcp.tool()
def find_todos(pattern: str = "**/*.py", limit: int = 50) -> list[dict]:
    """扫描 TODO/FIXME 注释,返回文件、行号、内容。"""
    hits = []
    for p in REPO_ROOT.glob(pattern):
        if not p.is_file():
            continue
        for i, line in enumerate(p.open(encoding="utf-8", errors="ignore"), 1):
            if "TODO" in line or "FIXME" in line:
                hits.append({"file": str(p.relative_to(REPO_ROOT)),
                             "line": i, "text": line.strip()})
                if len(hits) >= limit:
                    return hits
    return hits

@mcp.tool()
async def deep_scan(ctx: Context, pattern: str = "**/*.py") -> dict:
    """深度扫描,带进度上报,统计 TODO/FIXME 数量。"""
    files = [p for p in REPO_ROOT.glob(pattern) if p.is_file()]
    todos = fixmes = 0
    for i, p in enumerate(files, 1):
        await ctx.report_progress(i, len(files))
        await ctx.info(f"扫描 {p.name}")
        for line in p.open(encoding="utf-8", errors="ignore"):
            if "TODO" in line: todos += 1
            if "FIXME" in line: fixmes += 1
    return {"scanned": len(files), "todos": todos, "fixmes": fixmes}

# ---------- Resources:暴露只读数据 ----------
@mcp.resource("repo://manifest")
def manifest() -> str:
    """仓库顶层清单。"""
    entries = [str(p.relative_to(REPO_ROOT)) for p in sorted(REPO_ROOT.iterdir())]
    return json.dumps({"root": str(REPO_ROOT), "entries": entries}, ensure_ascii=False)

@mcp.resource("repo://file://{path}")
def read_file(path: str) -> str:
    """读取仓库内文本文件。"""
    target = (REPO_ROOT / path).resolve()
    if not str(target).startswith(str(REPO_ROOT)):
        raise ValueError("路径越界")
    return target.read_text(encoding="utf-8", errors="ignore")

# ---------- Prompts:可复用剧本 ----------
@mcp.prompt()
def review_file(path: str) -> str:
    """生成针对某文件的代码审查提示词。"""
    return (f"审查 {path}:\n1. bug 与边界条件\n2. 可读性改进\n"
            f"3. 修复 diff\n先用 repo://file://{path} 读真实内容。")

@mcp.prompt()
def tech_debt_report() -> str:
    """生成技术债务盘点提示词。"""
    return ("先调 find_todos 收集所有 TODO/FIXME,"
            "再按文件聚合、按严重度排序,输出一份 Markdown 技术债报告。")

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

端到端验证对话:接入后对 Claude Code 说——

「用 repo-analyzer 帮我盘点这个仓库的技术债,并审查 src/auth.py」

模型会:调 find_todos / deep_scan(Tool)、读 repo://file://src/auth.py(Resource)、若你用了 /review_file 斜杠命令则套用 Prompt 模板——三大原语在同一轮对话里协同工作,这就是「完整的 MCP 服务器」该有的样子。

工作流与架构图

flowchart LR
    U([用户在 Claude Code]) -->|斜杠命令选 Prompt| CC[Claude Code 主机]
    CC -->|JSON-RPC over stdio| SV[FastMCP 服务器]
    SV --> T[Tools 执行动作]
    SV --> R[Resources 读数据]
    SV --> P[Prompts 给剧本]
    T --> FS[(本地文件系统)]
    R --> FS
    T -->|ctx.report_progress| CC
    subgraph 谁触发
      T ~>|模型自主| CC
      R ~>应用/用户选择
      P ~>|用户主动|
    end

下面这张时序图说明一轮「盘点技术债 + 审查文件」请求里,三种原语如何接力:

sequenceDiagram
    participant U as 用户
    participant CC as Claude Code
    participant S as FastMCP 服务器
    participant FS as 文件系统
    U->>CC: 盘点技术债并审查 auth.py
    CC->>S: tools/call find_todos
    S->>FS: glob + 读取
    FS-->>S: 命中列表
    S-->>CC: TODO/FIXME 数组
    CC->>S: resources/read repo://file://src/auth.py
    S->>FS: 读取文件
    FS-->>S: 源码
    S-->>CC: 文件内容
    CC->>S: tools/call deep_scan (带 Context)
    S-->>CC: report_progress (n/total)
    CC->>CC: 套用 review_file 提示词模板组织报告
    CC-->>U: 技术债报告 + 审查意见

常见坑与排查清单

延伸阅读

训练营适用性评估

维度 评估
适用营型 中级 Python 工程营 / AI 工具链开发营 / 企业内部 Claude Code 落地营
学员基础 会 Python 函数与类型注解,用过 Claude Code 基本功能
建议时长 教学 1.5 小时 + 动手 2 小时(含把自己仓库接进去)
验收标准 服务器三原语在 Inspector 全部调通,并能在 Claude Code 完成一次「盘点技术债 + 审查文件」复合任务
可裁剪 只想讲 Tool 原语可在 1 小时内完成;Resource/Prompt 是本篇区分度所在,不建议砍

参考来源

  1. Build an MCP Server in Python 2026: Tools, Resources, and Prompts from Scratch — 从零构建 MCP 服务器,系统覆盖三大原语
  2. How to Build MCP Servers in Python: Complete FastMCP Tutorial — Firecrawl 出品的 FastMCP 完整实战,含 tools/resources/prompts
  3. FastMCP in Python: Build a Real MCP Server (2026 Guide) — 生产级 FastMCP 服务器,含 GitHub OAuth 等进阶
  4. MCP 官方快速开始:Server — Anthropic 官方协议文档与 server quickstart
  5. How to Build a Python MCP Server with FastMCP (pyrastra) — Tools / Resources / Prompts 与 Streamable HTTP 传输详解

[!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学通信知识星球海报-扫码加入