本教程面向想为 Claude Code(或任意支持 MCP 的客户端)打造自定义工具服务器的开发者,目标是带你用 Python 的 FastMCP 框架,从零写出一个可被 Claude Code 直接调用的 MCP 服务器。学完后你将掌握:用装饰器把普通 Python 函数变成工具(Tool)、资源(Resource)与提示词(Prompt);用 Context 对象实现日志上报、进度通知与会话状态;用 claude mcp 命令把服务器以三种作用域接入 Claude Code;并完成一个可运行的 Markdown 笔记库检索服务器实战项目。适合已具备 Python 基础、想扩展 AI 编程助手能力边界的工程师。
pip + venv。claude 命令可用),用于最后一步接入验证。async/await 的基本含义即可,不需要了解 JSON-RPC 协议细节——这正是 FastMCP 要帮你屏蔽的部分。MCP(Model Context Protocol)是一个开放协议,规范了 AI 模型如何从外部服务获取工具、资源与提示词。如果你手工实现,需要处理大量样板:JSON-RPC 报文收发、会话状态管理、请求与响应的格式化、能力协商……这些和你的业务逻辑毫无关系。
FastMCP 把这些都封装掉了。它的核心哲学一句话概括:写 Python,而不是协议样板。你只需要做三件事:
FastMCP 实例作为容器;@mcp.tool、@mcp.resource、@mcp.prompt 装饰普通函数;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 自动完成三件事:
add;a: int, b: int 推断出 JSON Schema。这意味着文档字符串和类型注解就是工具的"说明书"——写得越清楚,模型选错工具的概率越低。三个最佳实践:
int/str/list[str] 比 Any 好;必要时用 Pydantic 模型约束结构。异步工具同样支持,把 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
关键点:
ctx,它不是工具入参。async def。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 CodeClaude 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}" }
接入后还有两个关键机制:
mcp__<服务器名>__<工具名>,例如 mcp__notes-mcp-server__search_notes。在 Claude Code 里用 allowedTools: ["mcp__notes-mcp-server__*"] 可一键放行该服务器全部工具,免去逐个授权。推荐用 allowedTools 而非 bypassPermissions——前者精确授权、其它安全提示照常生效。ENABLE_TOOL_SEARCH=auto:5。验证接入:在项目里跑 claude,输入 /mcp,应看到 notes-mcp-server 为 connected,并列出 search_notes、add、summarize_topic 等组件。然后直接问:"帮我搜索笔记库里和'架构'相关的标题"——模型会自动调用 search_notes。
把前面所有部分串成一个完整、可直接跑的项目。功能:对一个本地 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。
gofastmcp.com,工具/资源/提示词/Context 的完整 API 与进阶(依赖注入、中间件、挂载子服务器)。modelcontextprotocol.io/specification,理解 JSON-RPC、能力协商、elicitation/sampling 等底层机制。code.claude.com/docs/en/mcp,claude mcp 全部子命令、作用域与 Tool Search 配置。pypi.org/project/fastmcp,版本与安装选项。/mcp 连接与调用链路。claude mcp add/list/get/remove、三种作用域、.mcp.json、Tool Search、输出上限与排查的速查依据。mcp__<server>__<tool> 命名约定、allowedTools 授权策略的官方说明。[!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 等场景全覆盖。
