本教程聚焦 MCP(Model Context Protocol)中最容易被忽视却价值极高的两个原语——Resources(资源) 和 Prompts(提示词模板)。绝大多数入门教程只讲 Tool,导致开发者把所有事情都塞进"工具"里:把只读文档也包装成 Tool、把固定提示词也写成 Tool。本教程将带你用一个真实的"运维知识库(Runbook KB)"项目,正确区分三类原语,并构建一个可被 Claude Code 直接挂载、可检索、可复用的 MCP 服务器。适合已跑通过第一个 MCP 服务器、想从"能用"迈向"设计正确"的开发者。
pip install uv 或系统包管理器)很多开发者第一次写 MCP 服务器时,会陷入一个误区:把所有能力都做成 @mcp.tool()。比如读取一份部署文档,明明是纯只读、无副作用的"查"操作,却被包成了一个 Tool。这会带来三个问题:
@文件 一样把资源挂进上下文,而 Tool 只能在模型主动"调用"时才注入结果。只读文档做成 Resource,可控性强得多。MCP 官方对三原语的定位非常清晰:
| 原语 | 类比 | 控制方 | 典型用途 |
|---|---|---|---|
| Resources | GET 端点 | 用户/应用主导 | 把数据载入上下文(文档、配置、日志) |
| Tools | POST 端点 | 模型主导 | 执行有副作用的操作(查询数据库、发请求) |
| Prompts | 可复用模板 | 用户主导 | 标准化交互模式(评审、分诊、解释) |
记住一句话:只读的、给模型"看"的,用 Resources;有副作用的、让模型"做"的,用 Tools;固定套路、让用户一键触发的,用 Prompts。
Resources 通过 URI 暴露数据。最简单的形式是静态资源,URI 固定,例如 docs://index 永远返回同一份目录。更强大的是模板资源(URI Template),URI 里带参数,例如 docs://{path},模型可以传不同的 path 读取不同文档。
在 FastMCP(MCP Python SDK v1.x 稳定线,生产推荐)里,一个 Resource 就是一个被 @mcp.resource(...) 装饰的普通函数:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""个性化问候"""
return f"Hello, {name}!"
注意三点:
- 装饰器的字符串是 URI 模板,{name} 会自动从路径里解析成函数参数。
- 函数的类型注解就是参数 schema——你不需要手写 JSON Schema。
- docstring 会作为该资源的描述返回给客户端,决定模型/用户能否找到它。
Resources 还支持订阅/更新通知:当底层数据变化时,服务器可以主动通知客户端资源内容已更新(mcp.send_resource_updated(uri)),客户端据此刷新。这对"实时日志""配置热更新"场景非常有用。
Prompts 是参数化的提示词模板。在 FastMCP 里,@mcp.prompt() 装饰一个函数,函数的返回值(字符串,或结构化消息列表)就是最终注入对话的提示词:
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
"""生成一段问候提示词"""
styles = {
"friendly": "请写一段温暖友好的问候",
"formal": "请写一段正式专业的问候",
}
return f"{styles.get(style, styles['friendly'])},对象是 {name}。"
最关键的实际效果:在 Claude Code 中,每个服务器 Prompt 都会自动变成一条斜杠命令,命名规则是 /mcp__<服务器名>__<prompt名>。上面这个 prompt 挂到名为 demo 的服务器后,你直接输入 /mcp__demo__greet_user,Claude Code 会弹出参数表单,填完即把生成好的提示词送入对话。
这意味着 Prompts 是给"人"用的快捷方式,而 Tools 是给"模型"用的。把团队约定好的提问范式沉淀成 Prompt,比每个人各自手写提示词稳定得多。
现在动手构建一个真实可用的项目 RunbookKB。场景:一个 SRE 团队把所有事故 runbook(Markdown 文档)放在本地目录,希望 Claude Code 能够:读取任意 runbook、按关键词检索、并一键发起"事件分诊"。
runbook-kb/
├── kb_server.py # MCP 服务器主文件
├── pyproject.toml # 依赖声明(mcp)
└── knowledge/ # 知识库根目录(纯 Markdown)
├── guides/
│ ├── deploy.md
│ └── rollback.md
└── incidents/
└── db-high-cpu.md
先建两篇示例文档。knowledge/guides/deploy.md:
# 部署 runbook
1. 确认 CI 产物版本号与发布单一致
2. 灰度 10% 流量,观察 5 分钟错误率
3. 全量发布,监控 P99 延迟
回滚请参考 rollback.md
knowledge/incidents/db-high-cpu.md:
# 数据库 CPU 飙高
- 现象:RDS CPU > 90%,慢查询增多
- 根因排查:先查 pg_stat_activity 找长事务
- 缓解:kill 慢查询,扩容只读节点
uv init runbook-kb
cd runbook-kb
uv add "mcp>=1.27,<2" # 锁定 v1.x 稳定线
mkdir -p knowledge/guides knowledge/incidents
为什么是
<2?因为 MCP Python SDK v2 是 pre-release(2.0.0b1),它面向 2026-07-28 规范,稳定版计划 2026-07-27 才发布,且每个预发布都可能有不兼容变更。生产环境必须把上界钉死在<2,避免uv/pip意外拉到不稳定的 v2。本文用稳定 v1.x 的 FastMCP API;文末会单独讲 v2 的新写法。
kb_server.py把三原语各放一个真实例子:
from pathlib import Path
from mcp.server.fastmcp import FastMCP
KB_ROOT = Path(__file__).parent / "knowledge"
mcp = FastMCP("RunbookKB")
# ========== Resources:只读数据 ==========
@mcp.resource("docs://index")
def list_docs() -> str:
"""列出知识库中所有 runbook 的相对路径清单。"""
files = sorted(KB_ROOT.rglob("*.md"))
lines = [f"- {f.relative_to(KB_ROOT)}" for f in files]
return "# 知识库目录\n" + "\n".join(lines)
@mcp.resource("docs://{path}")
def read_doc(path: str) -> str:
"""按相对路径读取一篇 runbook 全文。"""
target = (KB_ROOT / path).resolve()
# 防路径穿越:解析后必须仍在 KB_ROOT 之内
if not str(target).startswith(str(KB_ROOT.resolve())):
return "错误:路径越界,拒绝访问。"
if not target.exists():
return f"错误:文档 {path} 不存在。"
return target.read_text(encoding="utf-8")
# ========== Tools:有副作用的检索 ==========
@mcp.tool()
def search_kb(query: str, limit: int = 5) -> str:
"""在知识库全文中按关键词检索,返回命中文档片段。"""
hits, needle = [], query.lower()
for f in KB_ROOT.rglob("*.md"):
text = f.read_text(encoding="utf-8")
idx = text.lower().find(needle)
if idx >= 0:
start = max(0, idx - 60)
snippet = text[start:idx + len(needle) + 120].replace("\n", " ")
hits.append(f"[{f.relative_to(KB_ROOT)}] …{snippet}…")
if len(hits) >= limit:
break
return "\n\n".join(hits) if hits else "未找到匹配文档。"
# ========== Prompts:可复用提示词 ==========
@mcp.prompt()
def incident_triage(symptom: str, severity: str = "P2") -> str:
"""生成一份生产事件分诊的标准化提示词。"""
return (
f"你是一名 SRE。请对以下生产事件做分诊:\n"
f"- 现象:{symptom}\n- 级别:{severity}\n\n"
f"请先调用 search_kb 工具检索相关 runbook,"
f"再用 docs:// 资源读取全文,最后给出:\n"
f"1. 可能根因(按概率排序)\n"
f"2. 立即缓解步骤\n3. 需要通知的负责人。"
)
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
操作说明:search_kb 是 Tool(有"检索"这个执行动作),docs://index 与 docs://{path} 是 Resource(纯只读),incident_triage 是 Prompt(用户一键触发的固定套路)。一个文件里三种原语各司其职,正是本教程的核心主张。
预期结果:服务器能以 stdio 方式被 Claude Code 拉起,对外暴露 2 个资源、1 个工具、1 个提示词。
常见问题:
- ImportError: mcp:依赖没装好,确认 uv add "mcp>=1.27,<2" 执行成功。
- 路径越界误报:Windows 下 startswith 比较要小心大小写,可用 Path.is_relative_to()(Python 3.9+)替代,更稳妥:if not target.is_relative_to(KB_ROOT.resolve()):。
在动 Claude Code 之前,先用官方 Inspector 验证三原语是否都注册成功:
cd runbook-kb
uv run mcp dev kb_server.py
这会启动一个本地 Web UI(默认 http://localhost:5173)。连接后你会看到三个标签页:
docs://index,点开返回目录清单;输入 path=guides/deploy.md 测试模板资源。search_kb,参数 query=CPU,应返回 db-high-cpu.md 的片段。incident_triage,填入 symptom=数据库CPU 90%,能看到生成的完整提示词。常见问题:Resources 列表为空——通常是 docstring 为空或函数没被装饰器正确标注,检查 @mcp.resource 的 URI 是否以 协议:// 开头(如 docs://)。
确认无误后,把它注册为 Claude Code 的 stdio MCP 服务器:
claude mcp add runbook-kb -- uv run --with "mcp>=1.27,<2" kb_server.py
命令中
--之后是启动服务器的完整命令。用uv run --with可以在不污染全局环境的情况下临时注入mcp依赖。
验证挂载成功:
claude mcp list # 应出现 runbook-kb
claude mcp get runbook-kb # 查看其暴露的工具/资源/提示词
然后在 Claude Code 交互界面里:
1. 输入 /mcp__runbook-kb__incident_triage —— Prompt 变成的斜杠命令,填参数即可发起分诊。
2. 让模型"读一下 docs://index"——它会通过 Resource 把目录载入上下文。
3. 让模型"搜一下 CPU 相关的 runbook"——它会调用 search_kb 工具。
flowchart LR
U([用户在 Claude Code]) -->|/mcp__...__incident_triage| P[Prompt 模板]
U -->|@ docs://index 挂载| R[Resource 只读数据]
M[(Claude 模型)] -->|主动调用| T[Tool 检索]
P --> M
R --> M
T --> KB[(knowledge/*.md)]
R --> KB
M -->|综合输出| U
三条路径各不相同:Prompt 由用户主动触发(人主导),Resource 由用户/应用附加进上下文(人主导),Tool 由模型在推理时自主决定调用(模型主导)。三者最终都汇入模型的上下文窗口,共同支撑一次高质量的回答。
真实知识库里文档会增删,固定写死的资源列表不现实。FastMCP 支持用 @mcp.resource 配合目录动态列举资源,以及 Completions(补全) 能力——当用户在 URI 模板里输入 {path} 时,客户端可以请求服务器返回候选项。
一个轻量做法是再加一个"列举可用路径"的静态资源,并把它和模板资源配合:
@mcp.resource("docs://paths.json")
def available_paths() -> str:
"""返回所有可用文档路径的 JSON,供客户端做补全提示。"""
import json
paths = [str(f.relative_to(KB_ROOT)) for f in KB_ROOT.rglob("*.md")]
return json.dumps(paths, ensure_ascii=False)
当文档变化时,主动通知客户端刷新:
# 例如在某次写入文件后
mcp.send_resource_updated("docs://paths.json")
这样客户端缓存的资源就会失效重取,保证知识库"热更新"。
MCPServer 新写法MCP Python SDK 正在向 v2 演进(面向 2026-07-28 规范,稳定版计划 2026-07-27)。v2 把类名从 FastMCP 统一为 MCPServer,API 更精简,"15 行一个服务器"的写法变成:
from mcp.server import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
装饰器语义、三原语划分与本教程完全一致,迁移成本极低。但务必等 v2 正式稳定后再用于生产,预发布期间每个版本都可能有破坏性变更,且要在依赖里写 <2 上界。本教程的 RunbookKB 只需把 from mcp.server.fastmcp import FastMCP 换成 from mcp.server import MCPServer、FastMCP( 换成 MCPServer( 即可在 v2 上跑。
回顾 RunbookKB 做了什么:用 Resources 暴露只读的 runbook 全文与目录(语义正确、可被附加、可热更新),用 Tool 承担有副作用的全文检索,用 Prompt 把"事件分诊"沉淀成团队共享的一键斜杠命令。三者各归其位,没有把只读数据硬塞进 Tool,也没有把固定提示词写成 Tool。这就是"设计正确"的 MCP 服务器与"能跑就行"的 MCP 服务器的本质区别。
可运行的扩展方向:
- 把 search_kb 升级为向量检索(嵌入 + 相似度),从"关键词命中"升级为"语义命中"。
- 给 read_doc 增加权限维度,按用户角色过滤可见文档。
- 增加 Elicitation(v1.x 文档已列入 server 能力),让服务器在执行中反问用户补充信息,形成"检索→反问→精化"的闭环。
claude mcp add 的 stdio/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 等场景全覆盖。
