首页 › tutorials › 用FastMCP构建首个Python MCP服务器

概述

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

前置条件

一、为什么是 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,让客户端的模型知道这个工具叫什么、做什么、需要什么参数。这种"装饰器即接口"的设计,让一个工具的定义可以短到三行。

二、第一步:搭建项目骨架

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

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,写入最小可运行骨架:

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)是模型可以执行的函数。写一个普通函数并加装饰器:

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

FastMCP 自动完成三件事:

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

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

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

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

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

资源(Resource)是给模型读取的只读数据,通过 URI 访问。资源与工具的区别:工具是"动作",资源是"数据"。

静态资源,URI 固定:

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

资源模板用于按参数动态生成内容,在 URI 中用 {占位符}:

@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 里会变成 / 斜杠命令。定义时把可变部分作为参数:

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

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

六、第五步:用 Context 打通双向通信

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

通过依赖注入获取 Context(推荐写法):

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

关键点:

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,按场景调行为

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

@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() 参数:

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 管理命令:

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 在别的目录启动时会找不到文件。

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

# 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 长这样(也可手写):

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

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

"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:

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()

接入与验证:

# 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://架构设计 读全文,最后综合输出——整个过程你只说了一句话。

工作流程图

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。

延伸阅读

训练营适用性评估

参考来源

  1. How to Create an MCP Server in Python — FastMCP — FastMCP 官方五步入门,本教程"搭建骨架/工具/资源/模板/运行"步骤的直接依据。
  2. MCP Context — FastMCP — Context 对象官方文档,日志、进度、会话状态、elicit/sample 等 API 细节来源。
  3. Claude Code MCP Commands — Quick Reference — claude mcp add/list/get/remove、三种作用域、.mcp.json、Tool Search、输出上限与排查的速查依据。
  4. Connect to external tools with MCP — Claude Code Docs — 传输类型(stdio/http/sse)、mcp__<server>__<tool> 命名约定、allowedTools 授权策略的官方说明。
  5. fastmcp · PyPI — 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学通信知识星球海报-扫码加入