本教程带你用 Python + FastMCP 从零构建一个可被 Claude Code 直接调用的本地 MCP 服务器——「代码片段管家(Snippet Vault)」。学完你将掌握:用装饰器把普通 Python 函数变成 MCP 工具、只读资源、提示模板;用 Context 向客户端回报进度与日志;用 uv/uvx 打包并通过 claude mcp add 接入 Claude Code;用 mcp dev 全链路调试。适合写过 Python、想给自己(或团队)的 Claude Code 加一套私有工具的工程师。前置已覆盖 Go/Rust 构建 MCP 的路径,本篇补齐最主流的 Python FastMCP 这条官方推荐路线。
uv(curl -LsSf https://astral.sh/uv/install.sh | sh),用于依赖管理与 uvx 运行claude 可正常启动)手写 MCP 意味着自己实现 JSON-RPC 2.0、能力协商、stdio 帧解析、工具 schema 校验……而 FastMCP 把这套底层全部封装,让你只写业务函数:
| 原语 | 装饰器 | 语义 | 在 Snippet Vault 中的角色 |
|---|---|---|---|
| 工具 Tool | @mcp.tool |
LLM 可调用、有副作用 | 增删改查片段 |
| 资源 Resource | @mcp.resource |
只读数据,LLM 按需读取 | 片段索引、单个片段内容 |
| 提示 Prompt | @mcp.prompt |
可复用的消息模板 | “审查这段代码”模板 |
FastMCP 会根据函数签名与类型注解自动生成 JSON Schema,Claude 据此决定何时调用、传什么参数。它的开发者体验接近 FastAPI,是目前 Anthropic 官方在 Python 侧首推的构建方式(FastMCP 已与官方 mcp Python SDK 合流,2026 年稳定在 3.x)。
创建项目目录并用 uv 初始化:
mkdir snippet-vault && cd snippet-vault
uv init --name snippet-vault
uv add fastmcp
uv init 会生成 pyproject.toml 和一个示例 main.py。把 main.py 删掉,我们从头写。确认 FastMCP 版本在 2.x/3.x:
uv run python -c "import fastmcp; print(fastmcp.__version__)"
# 预期输出类似:3.2.x 或 2.x
常见问题
uv: command not found:没装 uv,回到前置条件重装。fastmcp.__version__ 报 AttributeError:版本过老,uv add fastmcp@latest 升级。先写一个只有两个工具的最小版本,跑通“装饰器 → schema → Claude 调用”闭环。新建 server.py:
from fastmcp import FastMCP
mcp = FastMCP("Snippet Vault")
@mcp.tool
def ping(message: str = "hello") -> str:
"""健康检查:原样回显消息,证明链路畅通。"""
return f"pong: {message}"
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输,无需任何参数
预期结果:mcp.run() 会让进程进入 stdio 监听,不会打印任何东西到终端——stdio 协议要求 stdout 只能是 JSON-RPC 帧,任何 print 都会破坏协议。这是新手最大的坑。
常见问题
print,改用第三步介绍的 ctx.info(...) 或 logging 输出到 stderr。mcp.run(),或被 shell 的管道提前关闭了 stdin。我们用一个 JSON 文件作为持久化(教学够用,生产可换 SQLite/Postgres)。新建 store.py:
from __future__ import annotations
import json, uuid, threading
from pathlib import Path
from typing import TypedDict
class Snippet(TypedDict):
id: str
title: str
language: str
code: str
tags: list[str]
_PATH = Path.home() / ".snippet-vault.json"
_LOCK = threading.Lock()
def _load() -> list[Snippet]:
if not _PATH.exists():
return []
return json.loads(_PATH.read_text("utf-8"))
def _save(data: list[Snippet]) -> None:
_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")
def add(title: str, language: str, code: str, tags: list[str]) -> Snippet:
with _LOCK:
data = _load()
item: Snippet = {"id": uuid.uuid4().hex[:8], "title": title,
"language": language, "code": code, "tags": tags or []}
data.append(item)
_save(data)
return item
def all_snippets() -> list[Snippet]:
return _load()
def get(sid: str) -> Snippet | None:
return next((s for s in _load() if s["id"] == sid), None)
def delete(sid: str) -> bool:
with _LOCK:
data = _load()
before = len(data)
data = [s for s in data if s["id"] != sid]
_save(data)
return len(data) < before
预期结果:首次写入会在 ~/.snippet-vault.json 生成文件。_LOCK 保证并发安全。
回到 server.py,接入数据层,并写带类型注解 + docstring 的工具——这两样直接决定 Claude 调用的准确率:
from fastmcp import FastMCP, Context
import store
mcp = FastMCP("Snippet Vault")
@mcp.tool
def save_snippet(title: str, language: str, code: str, tags: list[str] | None = None) -> dict:
"""保存一段代码片段。
Args:
title: 片段标题,简短描述用途。
language: 语言标签,如 python、sql、bash。
code: 代码本体。
tags: 可选标签列表,便于检索。
"""
item = store.add(title, language, code, tags or [])
return {"id": item["id"], "title": item["title"], "saved": True}
@mcp.tool
def search_snippets(query: str, ctx: Context) -> list[dict]:
"""按关键词在标题、标签、代码中模糊检索片段。"""
q = query.lower()
hits: list[dict] = []
all_items = store.all_snippets()
for i, s in enumerate(all_items, 1):
ctx.report_progress(i, len(all_items)) # 向客户端推送进度
blob = f"{s['title']} {' '.join(s['tags'])} {s['code']}".lower()
if q in blob:
hits.append({"id": s["id"], "title": s["title"], "language": s["language"]})
ctx.info(f"检索完成,命中 {len(hits)} 条") # 写入客户端日志
return hits
@mcp.tool
def get_snippet(snippet_id: str) -> dict:
"""根据 ID 取片段完整内容。"""
s = store.get(snippet_id)
if s is None:
raise ValueError(f"未找到片段: {snippet_id}")
return s
@mcp.tool
def delete_snippet(snippet_id: str) -> dict:
"""删除指定 ID 的片段。"""
deleted = store.delete(snippet_id)
return {"id": snippet_id, "deleted": deleted}
if __name__ == "__main__":
mcp.run()
为什么这样写:
save_snippet 而非 handle_data)。Claude 是“读 schema 决定是否调用”,描述越准,误触发越少。ctx: Context 参数不会出现在工具 schema 里——FastMCP 识别出它是上下文对象,自动注入,对 LLM 不可见。用它做 report_progress/info/debug,既不污染协议,又能让用户在 Claude Code 里看到进度。ValueError 而非返回 None:FastMCP 会把它转成结构化错误,Claude 能据此重试或换参数。预期结果:四个工具就绪。Claude 在收到“帮我存一段快速排序的 Python 片段”时,应能自主选择 save_snippet 并填好参数。
常见问题
str/int/float/bool/list/dict/Optional/Union 及 Pydantic 模型;自定义类需要先转成 TypedDict 或 BaseModel。missing required argument: ctx:你把 ctx 放在了有默认值的参数之后,或没从 fastmcp 导入 Context。工具负责“动作”,资源负责“可读数据”,提示负责“任务模板”。补上后两类:
from fastmcp import FastMCP
# —— 静态资源:片段目录 ——
@mcp.resource("snippets://index")
def snippet_index() -> list[dict]:
"""所有片段的精简目录,便于 LLM 一览全貌。"""
return [{"id": s["id"], "title": s["title"], "language": s["language"]}
for s in store.all_snippets()]
# —— 资源模板:按 ID 取单个片段(URI 含占位符,动态解析)——
@mcp.resource("snippet://{snippet_id}")
def snippet_by_id(snippet_id: str) -> str:
s = store.get(snippet_id)
return f"# {s['title']}\n```{s['language']}\n{s['code']}\n```" if s else "(片段不存在)"
# —— 提示模板:固定一套审查流程 ——
@mcp.prompt
def review_snippet(snippet_id: str, focus: str = "正确性与边界") -> str:
"""生成一段“代码审查”提示,聚焦指定维度。"""
s = store.get(snippet_id)
if not s:
return f"未找到片段 {snippet_id},请先用 search_snippets 查找。"
return (
f"请审查以下 {s['language']} 片段,重点:{focus}。\n"
f"标题:{s['title']}\n代码:\n{s['code']}\n"
"输出:1) 潜在 bug;2) 改进建议;3) 重写后的版本。"
)
把上面三段追加到 server.py(mcp = FastMCP(...) 之后、mcp.run() 之前)。至此工具/资源/提示三原语齐备,与 Claude Code 的对接面完整。
资源 vs 工具的取舍:只读、想让 LLM 自主“翻阅”的用资源;需要副作用、精确参数校验的用工具。snippets://index 给 Claude 一个总览入口,避免它一上来就盲目调用 search_snippets。
要让 Claude Code 用 uvx 一键拉起服务器,得声明一个命令行入口。编辑 pyproject.toml:
[project]
name = "snippet-vault"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastmcp"]
[project.scripts]
snippet-vault = "server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["."]
再在 server.py 末尾把启动逻辑包成函数:
def main() -> None:
mcp.run()
然后本地自测入口:
uv run snippet-vault --help # 进程能起来、不报错即通过(stdio 模式会等待输入,Ctrl+C 退出)
常见问题
uvx: package not found:还没发布。本地开发用 claude mcp add ... -- uv run snippet-vault;想用 uvx 直接跑,先 uv build 再 uv tool install ./dist/*.whl。hatchling 报找不到包:确认 packages = ["."] 与 server.py 同级。Claude Code 支持三种传输:本地 stdio、HTTP、远程 SSE。本地工具用 stdio 最省事。两种接入方式任选其一。
方式 A:claude mcp add(命令行)
# 从项目目录运行;-- 之后的参数就是启动命令
claude mcp add snippet-vault -- uv run --directory $(pwd) snippet-vault
--directory 很关键——否则 Claude Code 可能在别的工作目录找不到 pyproject.toml。
方式 B:.mcp.json(项目级配置,可提交到仓库共享)
在项目根写 .mcp.json:
{
"mcpServers": {
"snippet-vault": {
"command": "uv",
"args": ["run", "--directory", "/绝对路径/snippet-vault", "snippet-vault"]
}
}
}
放进仓库后,团队成员 claude 一启动就会自动加载(首次会提示授权)。范围控制:claude mcp add --scope user 全局可用,--scope project 写入 .mcp.json,--scope local 仅本人本机。
验证连接:
claude mcp list # 应看到 snippet-vault ✓ connected
claude mcp get snippet-vault # 列出它暴露的 tools/resources/prompts
进 Claude Code 交互界面,输入:“帮我存一段二分查找的 Python 实现,标签加上算法和查找,然后告诉我现在库里都有哪些片段。” Claude 应依次调用 save_snippet → snippets://index(或 search_snippets)。
常见问题
claude mcp list 显示 ✗ failed to connect:进入项目目录跑 uv run snippet-vault 看真实报错;99% 是缺依赖或路径不对。.claude/settings.json 把 mcp__snippet-vault__* 加入 permissions.allow。写 MCP 最大的痛点是“它跑在 stdio 黑盒里,出错难定位”。官方调试器 mcp dev 能可视化地手动触发每个工具/资源/提示,并看到完整的 JSON-RPC 往返:
uv run mcp dev server.py
执行后终端会打印一个本地 Web 地址(默认 http://localhost:5173),浏览器打开即可:
save_snippet → 右侧填表单 → Run,下方看返回值。snippets://index 与模板 snippet://{snippet_id}。review_snippet,预览生成的消息。ctx.report_progress 推送的进度——这是验证 Context 是否工作的最直观方式。调试心法:
import logging; logging.basicConfig(stream=sys.stderr),绝不 print 到 stdout。claude --debug,或在 Inspector 里观察入站请求。把前面八步拼成一个真实场景:新人提交 PR 前,让 Claude Code 调用你的服务器自检是否符合团队片段库的既有范式。
.mcp.json,把 snippet-vault 设为 --scope project。review_snippet 提示 + search_snippets 工具组合:让 Claude 先按“错误处理”检索现有范式,再据此审查当前改动。Stop 事件触发时若改动文件含 # TODO,自动调用 search_snippets("TODO 处理范式") 把规范片段贴进对话。这样一个“私有知识 + 工具调用”的闭环就建好了,且全程零外部 API、零密钥。
flowchart TD
A[Python 函数 + 装饰器] -->|FastMCP 自动生成| B[JSON Schema]
B --> C[stdio / uvx 启动 server.py]
C --> D{调试通道}
D -->|mcp dev Inspector| E[可视化手动触发]
D --> |claude mcp add . mcp.json| F[Claude Code 加载]
F --> G[自然语言触发]
G --> H[Claude 按 schema 选工具]
H --> I[store.py 读写 JSON]
I --> J[Context 回报进度/日志]
J --> G
flowchart LR
Q{这份能力是?}
Q -->|有副作用/需参数校验| T[@mcp.tool<br/>工具]
Q -->|只读数据/可被浏览| R[@mcp.resource<br/>资源]
Q -->|固定任务模板| P[@mcp.prompt<br/>提示]
T --> CT[命名动词化 + 详尽 docstring]
R --> CR[URI 清晰 + 静态/模板二选一]
P --> CP[参数少 + 产出可预期]
gofastmcp.com):Context 进阶、认证、部署到 Cloudflare 的最权威出处。modelcontextprotocol.io):传输层、生命周期、能力协商的底层定义,想从“会用”到“会改协议”必读。code.claude.com/docs/en/mcp):claude mcp add/get/list、三种 scope、.mcp.json 的官方说明。modelcontextprotocol/python-sdk):FastMCP 已与其合流,底层 API 与示例库。claude mcp add/list/get、三种 scope 与 .mcp.json 格式权威说明。[!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 等场景全覆盖。
