本教程带你用 Python FastMCP 从零构建一个可被 Claude Code 直接调用的 MCP 服务器,把一个 SQLite 知识库变成"可对话工具"——模型不仅能搜索词条,还能读取结构化资源、写入新词条。全程零额外依赖(除 fastmcp 外只用 Python 标准库),适合已写过 Python、想让 Claude Code 接入私有数据或内部 API 的工程师。学完你将掌握:用装饰器声明工具/资源、用 Context 注入协议能力、用 stdio 传输跑通服务、以及用 claude mcp 命令把服务器接入会话并调试。
curl -LsSf https://astral.sh/uv/install.sh | shclaude 命令可用)为什么选 FastMCP?它现在是 MCP 生态的事实标准——FastMCP 1.0 已在 2024 年并入官方 Python SDK,独立维护的 FastMCP(当前 v3,由 Prefect 团队维护)据官方 README 称每天下载量达百万级,"某种版本的 FastMCP 驱动着跨语言 70% 的 MCP 服务器"。它的核心承诺是:你只写普通 Python 函数,schema、校验、文档、传输协商全部自动生成。
在写代码前,先理清 MCP 暴露给模型的三大原语,这决定了你如何切分功能:
| 原语 | 语义 | 何时用 | 何时不用 |
|---|---|---|---|
| Tool(工具) | 模型主动调用的"动作",有副作用、可写、可计算 | 搜索、写入、查询、调外部 API | 纯静态大文件(模型每次都拉一遍浪费上下文) |
| Resource(资源) | 模型可"读取"的数据,URI 寻址,可订阅 | 文档、配置、列表、只读数据集 | 需要参数化决策的逻辑(那是工具的活) |
| Prompt(提示词) | 可复用的参数化模板 | 固定工作流入口 | 一次性问答 |
一条经验法则:"读静态数据"用资源,"做决策/有副作用"用工具。本教程聚焦前两者——这也是绝大多数 MCP 服务器的核心。
mkdir kb-mcp && cd kb-mcp
uv init --no-readme
uv add fastmcp
uv add fastmcp 会创建虚拟环境并写入 pyproject.toml。完成后验证:
uv run python -c "import fastmcp; print(fastmcp.__version__)"
预期输出形如 3.x.x。
新建 server.py:
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
@mcp.tool
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
注意三点:① 装饰器是 @mcp.tool(v3 支持无括号写法,带括号 @mcp.tool() 等价);② 函数的类型注解会被自动转成 JSON Schema——没有注解就没有参数定义;③ docstring 会成为工具描述,模型据此决定是否调用,所以务必写清楚。
FastMCP 自带一个开发命令,直接打开 MCP Inspector 图形界面:
uv run fastmcp dev server.py
浏览器会弹出 Inspector,你能逐个调用 add 工具、查看返回的 JSON。这是脱离 Claude Code、快速验证逻辑的最佳手段——先在 Inspector 里跑通,再接入 Claude Code。
常见问题:
- command not found: fastmcp → 用 uv run fastmcp 而非裸 fastmcp,确保走的是项目虚拟环境。
- Inspector 启动后无工具 → 检查 @mcp.tool 是否漏写、函数是否有 docstring。
现在构建一个真正有用的服务器。场景:你有一张术语表(这里用 5G/通信术语填充,贴合本机用户的领域),希望 Claude Code 能"查、读、写"。我们用 Python 标准库 sqlite3,零额外依赖。
新建 db.py,负责初始化和连接:
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent / "kb.sqlite3"
def get_conn() -> sqlite3.Connection:
"""每次调用返回一个新连接(MCP 工具可能并发,避免共享连接)"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # 让结果像字典一样取值
return conn
def init_db() -> None:
conn = get_conn()
conn.execute("""
CREATE TABLE IF NOT EXISTS terms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
term TEXT NOT NULL UNIQUE,
definition TEXT NOT NULL
)
""")
# 仅在空表时写入种子数据
if conn.execute("SELECT COUNT(*) FROM terms").fetchone()[0] == 0:
seeds = [
("AMF", "Access and Mobility Management Function,5G 核心网接入与移动性管理网元。"),
("SMF", "Session Management Function,会话管理功能,负责 PDU 会话的建立与释放。"),
("UPF", "User Plane Function,用户面功能,负责数据面转发与 QoS 执行。"),
("gNB", "5G 基站,连接 5G 核心网 NG 接口与空口 NR。"),
("PDU Session", "终端与数据网络之间的会话,承载用户数据。"),
]
conn.executemany("INSERT INTO terms(term, definition) VALUES(?, ?)", seeds)
conn.commit()
conn.close()
为什么每个工具都新建连接? MCP 工具可能被模型并发调用,sqlite3.Connection 不是线程安全的。复用单连接会随机抛 SQLite objects created in a thread can only be used in that same thread。新建连接成本极低(本地文件),换来的是正确性。
把核心逻辑写进 server.py:
from fastmcp import FastMCP, Context
from db import get_conn, init_db
init_db() # 进程启动时确保表与种子数据就绪
mcp = FastMCP("5G 知识库")
# ---------- 工具:模型主动调用的"动作" ----------
@mcp.tool
def search_terms(query: str, limit: int = 10) -> list[dict]:
"""按关键词模糊搜索术语。query 为空时返回全部。
Args:
query: 搜索关键词(匹配 term 或 definition)
limit: 返回条数上限,默认 10
"""
conn = get_conn()
pattern = f"%{query}%"
rows = conn.execute(
"SELECT id, term, definition FROM terms "
"WHERE term LIKE ? OR definition LIKE ? LIMIT ?",
(pattern, pattern, limit),
).fetchall()
conn.close()
return [dict(r) for r in rows]
@mcp.tool
def add_term(term: str, definition: str, ctx: Context) -> dict:
"""向知识库新增一个术语。
Args:
term: 术语名(唯一)
definition: 该术语的释义
"""
ctx.info(f"准备写入术语: {term}") # 进度/日志,会回传给客户端
conn = get_conn()
try:
cur = conn.execute(
"INSERT INTO terms(term, definition) VALUES(?, ?)",
(term, definition),
)
conn.commit()
ctx.info(f"写入成功,新 id = {cur.lastrowid}")
return {"id": cur.lastrowid, "term": term, "ok": True}
except sqlite3.IntegrityError:
return {"ok": False, "error": f"术语 '{term}' 已存在"}
finally:
conn.close()
# ---------- 资源:模型可"读取"的数据 ----------
@mcp.resource("kb://terms")
def all_terms() -> list[dict]:
"""返回知识库全部术语列表(只读快照)"""
conn = get_conn()
rows = conn.execute("SELECT id, term FROM terms ORDER BY term").fetchall()
conn.close()
return [dict(r) for r in rows]
# 资源模板:用 URI 参数动态寻址,模型可按 id 读取单条
@mcp.resource("kb://term/{term_id}")
def term_by_id(term_id: int) -> dict:
"""按 id 读取单条术语的完整释义"""
conn = get_conn()
row = conn.execute(
"SELECT id, term, definition FROM terms WHERE id = ?", (term_id,)
).fetchone()
conn.close()
if row is None:
return {"error": "not found"}
return dict(row)
if __name__ == "__main__":
mcp.run()
逐行解读关键点:
Context 依赖注入:add_term 的最后一个参数声明为 ctx: Context,FastMCP 检测到类型后自动注入协议上下文——ctx.info(...) 会作为日志回传客户端,ctx.report_progress(current, total) 可上报进度条。这是 FastMCP 区别于裸 SDK 的核心便利:你不用关心协议帧,只写 ctx。kb://terms 是静态 URI(无参数),kb://term/{term_id} 是模板(花括号占位)。模板让模型能像访问 REST 端点一样按 id 取数。list[dict]、int 这些注解直接生成 JSON Schema,模型因此"知道"该传什么、能拿到什么。返回值也会被 schema 校验,所以保持返回结构稳定很重要。uv run fastmcp dev server.py
在 Inspector 里依次:调用 search_terms(query="AMF")、读取资源 kb://term/1、调用 add_term(...) 看日志是否回显。全部通过后再进入下一步——这是规避"Claude Code 里报错却不知是哪一层"的最有效习惯。
MCP 服务器本质是被客户端拉起的子进程(stdio 传输)。Claude Code 通过 claude mcp 命令管理这些注册项。
在项目根目录执行:
claude mcp add kb-server -- uv run --directory $(pwd) server.py
要点:
- kb-server 是你给服务器起的名字,后续用它引用。
- -- 是分隔符:其左侧是 Claude Code 的参数,右侧是真正启动服务器的命令。Claude Code 会把右侧整条命令作为子进程拉起。
- uv run --directory $(pwd) server.py 确保无论从哪个目录启动,都能定位到项目的虚拟环境。Windows PowerShell 下把 $(pwd) 换成绝对路径。
claude mcp list # 列出所有已注册服务器及连接状态
claude mcp get kb-server # 查看单个服务器的命令、参数、环境变量
claude mcp list 会显示 kb-server ... ✓ connected。若显示连接失败,多半是命令路径或环境问题(见下方排错)。
启动会话:claude。然后自然语言提问:
帮我查一下知识库里关于 UPF 的定义,并新增一条术语:SBA,定义为"服务化架构,5G 核心网基于服务化接口的架构风格"。
模型会自行决定:调用 search_terms 查询、读取 kb://terms 浏览目录、调用 add_term 写入。你可以输入 /mcp 查看当前会话里各服务器的工具与连接健康度。
| 作用域 | 标志 | 存储位置 | 适用 |
|---|---|---|---|
local(默认) |
-s local |
用户配置,绑定当前项目 | 个人开发,不进版本库 |
project |
-s project |
项目根 .mcp.json |
团队共享,应提交到 git |
user |
-s user |
用户全局配置 | 跨所有项目复用 |
团队共享时,把 .mcp.json 直接写在项目根:
{
"mcpServers": {
"kb-server": {
"command": "uv",
"args": ["run", "--directory", ".", "server.py"]
}
}
}
队友 clone 后首次启动会话,Claude Code 检测到 .mcp.json 会提示是否信任并加载——这是把内部 MCP 工具分发给团队的标准姿势。
| 现象 | 根因 | 解决 |
|---|---|---|
claude mcp list 显示未连接 |
启动命令在 Claude Code 的 shell 里找不到 | 用绝对路径;确认 uv 在 PATH;改用 claude mcp add kb -- /abs/venv/bin/python /abs/server.py |
| 工具列表里没有你的工具 | 漏写 @mcp.tool 或函数无 docstring |
每个工具必须有装饰器 + docstring,否则不暴露给模型 |
| 模型传参类型不对 | 注解缺失或用了无 schema 的类型 | 参数必须有 int/str/bool/... 注解;避免用 Any、裸 dict 作入参 |
sqlite3 线程报错 |
共享了连接 | 每个工具内部新建连接(见步骤 4) |
| 改了代码模型却"没生效" | 旧子进程仍在运行 | /mcp 里 reconnect,或重启会话;stdio 服务器在启动时加载代码 |
| 资源读不到 | URI 拼写或模板参数不匹配 | 在 Inspector 的 Resources 面板核对 URI 与模板花括号 |
flowchart TD
A[uv add fastmcp<br/>初始化项目] --> B[写 server.py<br/>@mcp.tool / @mcp.resource]
B --> C[fastmcp dev server.py<br/>Inspector 自测]
C --> D{自测通过?}
D -- 否 --> B
D -- 是 --> E[claude mcp add kb -- uv run server.py]
E --> F[claude mcp list 验证连接]
F --> G[claude 会话中自然语言调用]
G --> H[/mcp 查看健康度与工具]
H --> I{需要团队共享?}
I -- 是 --> J[claude mcp add -s project<br/>生成 .mcp.json 并提交]
I -- 否 --> K[完成]
J --> K
llms.txt 供模型直接消费。claude mcp 命令全集、作用域、-- 分隔符、HTTP/SSE 传输。mcp.run() 换成 Streamable HTTP 部署到云端,即可跨机器共享(本系列已有"Streamable HTTP 远程部署"专题,可作为下一篇)。ctx.report_progress、await ctx.read_resource(...) 让工具间互相读资源、ctx.session 访问低层会话——这是构建复杂服务器的进阶入口。search_terms 加分页;② 把 SQLite 换成对外 REST API 封装;③ 新增 @mcp.prompt 暴露一个"术语复盘"模板。每个作业都能独立验收。uv pip install fastmcp、@mcp.tool / mcp.run() 基础用法,以及 FastMCP 在生态中的地位。@mcp.tool、@mcp.resource、资源模板、Context 依赖注入的 API 细节。claude mcp add/list/get/remove、-- 分隔符、local/project/user 三作用域与 .mcp.json 配置。uv/uvx 跑 Python MCP 服务器的实践,对应本文步骤 7 的启动命令选型。[!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 等场景全覆盖。
