MCP(Model Context Protocol)一共有四大交互原语:Tools(工具)、Resources(资源)、Prompts(提示词)、Sampling(采样)。前三者已有大量实战文章覆盖,唯独 Prompts(提示词原语) 这块长期被一笔带过。本教程专攻这一原语,目标是用 Python FastMCP 从零搭建一个 可参数化、可动态生成、可内嵌资源、可编排多消息 的提示词模板库,并把它注册进 Claude Code 变成斜杠命令。适用人群:已跑通过至少一个 MCP 服务器、想理解"提示词为什么是独立原语"并把它工程化的开发者。
npm i -g @anthropic-ai/claude-code),并能正常启动pip install fastmcp很多人第一次接触 MCP 时会困惑:"我已经有 Tools 了,为啥还要 Prompts?不就是把一段提示词塞进工具里返回吗?" 这是一个关键的概念分水岭。MCP 规范用"控制权归属"来区分三大原语:
| 原语 | 谁来触发 | 触发方式 | 典型用途 |
|---|---|---|---|
| Tools(工具) | 模型控制 | LLM 自己决定何时调用 | 查数据库、调 API、改文件 |
| Resources(资源) | 应用控制 | 客户端程序决定何时挂载 | 注入文件、配置、上下文文档 |
| Prompts(提示词) | 用户控制 | 人显式选择/触发 | 复用工作流、标准化指令模板 |
理解这张表,就理解了提示词原语的定位:它不是给模型用的,是给人用的。 Tools 是 LLM 的"手脚",Prompts 是人的"快捷指令"。当一段精心调试过的提示词(比如"代码审查七步法""提交信息生成器""安全审计开场白")需要反复使用、需要带参数、需要在团队间共享时,把它做成 Prompt 原语,比每次手敲、或者藏在某个工具里要干净得多。
提示词原语在协议层只有两个方法:
prompts/list:返回服务器上所有可用提示词的清单(名字、描述、参数定义)prompts/get:客户端传入 {name, arguments},服务器返回一组已经填好参数的消息(messages)注意 prompts/get 返回的是最终消息,而不是一个模板字符串。这意味着服务器可以在返回前做任何计算:读文件、查数据库、拼多轮对话。这是"动态提示词"的核心。
flowchart LR
U([用户]) -->|点击斜杠命令| CC[Claude Code 客户端]
CC -->|prompts/list| S[(MCP 服务器)]
S -->|Prompt 清单\n含参数定义| CC
U -->|填入参数| CC
CC -->|prompts/get\nname+arguments| S
S -->|运行时计算\n读文件/查库/拼消息| S
S -->|messages 数组| CC
CC -->|注入对话上下文| LLM([LLM])
LLM -->|按指令产出| U
先用一个零参数的提示词把链路跑通。创建 review_prompts.py:
from fastmcp import FastMCP
mcp = FastMCP("代码审查提示词库")
@mcp.prompt()
def commit_message() -> str:
"""生成规范的 Git 提交信息"""
return (
"你是一位资深工程师。请阅读用户接下来提供的 diff,"
"按 Conventional Commits 规范生成一条提交信息:\n"
"格式为 `type(scope): summary`,空一行后用一两句写动机与影响。\n"
"type 从 feat/fix/docs/refactor/test/chore 中选择。"
)
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
预期结果:FastMCP 会自动把函数名 commit_message 注册成提示词名,把 docstring 作为描述。它最终向客户端暴露的 prompts/list 响应长这样:
{
"prompts": [
{
"name": "commit_message",
"description": "生成规范的 Git 提交信息",
"arguments": []
}
]
}
常见问题:如果服务器启动后客户端看不到任何提示词,绝大多数原因是 docstring 缺失。FastMCP 把 docstring 当作描述,没有 docstring 的函数在某些版本里会被跳过或触发校验告警。给每个 prompt 函数都写一行 docstring,是第一条纪律。
静态提示词只是热身。提示词原语的真正价值在于参数化。FastMCP 会根据函数签名自动推断参数定义:
@mcp.prompt()
def code_review(
language: str,
focus: str = "readability",
severity: str = "major",
) -> str:
"""按指定语言和关注点生成代码审查指令。
Args:
language: 编程语言,如 python / typescript
focus: 审查关注点 security / performance / readability
severity: 只报告该级别以上的问题 minor / major / critical
"""
checklist = {
"security": "输入校验、越权、注入、敏感信息泄露",
"performance": "时间复杂度、N+1 查询、不必要的拷贝、锁粒度",
"readability": "命名、函数长度、副作用、注释与代码一致性",
}[focus]
return (
f"你是 {language} 代码审查专家。\n"
f"本次只关注【{focus}】方面,检查清单:{checklist}。\n"
f"只报告 {severity} 及以上级别的问题,忽略更轻微的瑕疵。\n"
"输出格式:用 Markdown 表格列出【位置 | 问题 | 建议 | 严重度】,"
"最后给一句话总结是否可合并。"
)
预期结果:prompts/list 现在会返回带三个参数的清单,其中 language 标记为必填(无默认值),focus 和 severity 为可选。用户在 Claude Code 里触发这个命令时,会被依次询问这些参数。
常见问题:参数名不要用 Python 关键字或包含空格;可选参数一定要带默认值,否则会被识别成必填。类型注解尽量只用 str/int/float/bool,复杂类型(dataclass、自定义类)在参数推断上不可靠,应放到函数体内构造。
回到那个关键认知:prompts/get 返回的是计算后的消息。所以服务器可以在返回前读文件、查数据库、调别的 API。这把提示词从"死模板"升级成"活的指令生成器"。
from pathlib import Path
SAFE_ROOT = Path("/home/gprshome/repo") # 限定可读根目录,防止越界
@mcp.prompt()
def review_file(path: str, focus: str = "readability") -> str:
"""读取本地源文件并生成审查指令(运行时动态加载内容)。
Args:
path: 相对仓库根的文件路径
focus: 审查关注点
"""
# 安全:解析后必须仍在受控根目录内
target = (SAFE_ROOT / path).resolve()
if not str(target).startswith(str(SAFE_ROOT)):
return f"拒绝访问:{path} 超出允许的根目录。"
if not target.exists():
return f"文件不存在:{target}"
content = target.read_text(encoding="utf-8")
return (
f"请审查以下文件,关注【{focus}】:\n\n"
f"```{target.suffix.lstrip('.')}\n{content}\n```\n"
"只指出需要修改的地方,给出行号与改法。"
)
预期结果:用户调用 /mcp__review-prompts__review_file,填入 path=src/auth.py focus=security,服务器现场读出该文件全文,连同审查指令一起作为一条 user 消息注入对话。LLM 立刻就能基于真实代码开始审查。
常见问题:
- 路径越权:动态读文件务必做路径收敛校验(如上面的 startswith 检查),否则用户构造 ../../etc/passwd 之类路径就能读到敏感文件。
- 大文件爆上下文:先判断 len(content),超过阈值(如 20k 字符)就只截取头部并提示"文件过大,已截断"。提示词原语不自带分页,全靠你自己控制返回体积。
当提示词需要携带一段有明确 URI 和 MIME 类型的数据时(比如一个文件、一张图、一段配置),不要把内容直接拼进字符串,而应该用 embedded_resource。这样客户端知道这是一段结构化资源,可以按 MIME 类型正确渲染(图片显示成图、代码高亮、二进制还原)。
下面是 prompts/get 最终返回的消息结构(FastMCP 会帮你生成这样的 JSON,这里展示协议真相以便理解):
{
"description": "审查 src/auth.py 的安全性",
"messages": [
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "file:///home/gprshome/repo/src/auth.py",
"mimeType": "text/x-python",
"text": "def login(user, pwd):\n ..."
}
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "请基于上面的代码做安全审查,重点查越权与注入。"
}
}
]
}
为什么用 embedded_resource 而不是把代码塞进 text?
1. 客户端能按 MIME 类型做语法高亮,可读性更好。
2. URI 让这段数据有"身份",方便调试时溯源("这条指令引用了哪个文件")。
3. 二进制内容(图片、PDF)只能走 blob(base64),无法塞进纯文本。
常见问题:MIME 类型尽量填准(.py 用 text/x-python,.ts 用 text/typescript),填错不会报错,但会丢失高亮。资源 URI 用规范的 file://、https:// 或自定义 scheme(如 repo://src/auth.py)。
一条提示词可以返回多条消息,每条带不同 role(user / assistant)。这能实现"预编排对话"——把一个多轮工作流的开场,一次性塞进上下文,省去和 LLM 来回试探。
下面这条提示词用"用户提需求 → 假装助手先表态 → 用户追加约束"的三段式,给安全审计定好基调:
@mcp.prompt()
def security_audit(target: str):
"""编排多轮安全审计开场白。
Args:
target: 审计目标,如 '用户认证服务'
"""
return [
{
"role": "user",
"content": {"type": "text",
"text": f"我们要对【{target}】做一次安全审计。"}
},
{
"role": "assistant",
"content": {"type": "text",
"text": "明白。我会先枚举信任边界与攻击面,"
"再逐项核查常见漏洞(OWASP Top 10)。"}
},
{
"role": "user",
"content": {"type": "text",
"text": "很好。请先输出威胁清单(表格),"
"再按修复成本给出优先级排序。不要急着改代码。"}
},
]
预期结果:调用后,对话历史里会直接出现这三条消息,LLM 读到的上下文像是"已经聊过两轮",于是它会顺着既定结构产出威胁清单,而不是漫无边际地发散。
常见问题:assistant 消息不要写得太满(不要替 LLM 把答案说完),否则它会直接复述。assistant 消息的作用是"定调子、定结构、定约束",而不是"给答案"。
服务器写好后,注册进 Claude Code 有两种方式。
方式 A:命令行注册(推荐)
claude mcp add review-prompts -- python /home/gprshome/review_prompts.py
方式 B:项目级 .mcp.json
在项目根目录创建 .mcp.json:
{
"mcpServers": {
"review-prompts": {
"command": "python",
"args": ["/home/gprshome/review_prompts.py"]
}
}
}
注册后验证:
/mcp 查看已连接服务器,确认 review-prompts 状态为 connected。/mcp__review-prompts__code_review。输入该命令,Claude Code 会依次询问你 language、focus、severity 的值,填完后服务器计算消息并注入对话。常见问题:
- 命令不出现在列表:用绝对路径指定 Python 解释器和脚本,避免 venv 未激活导致 ModuleNotFoundError: fastmcp。
- 触发后报"prompt not found":确认函数名(注册名=函数名,下划线保留),且该函数有 docstring。
- 想换名字:用 @mcp.prompt(name="audit") 显式指定,注册名就变成 /mcp__review-prompts__audit。
把上面六个步骤整合成一个完整可运行的服务器。这个服务器对外提供四个提示词,覆盖静态、参数化、动态读文件、多消息编排四种形态:
# /home/gprshome/review_prompts.py
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP("代码审查提示词库")
SAFE_ROOT = Path("/home/gprshome/repo")
MAX_CHARS = 20000 # 单文件上限,防止爆上下文
# 1) 静态:提交信息生成器
@mcp.prompt()
def commit_message() -> str:
"""生成规范的 Conventional Commits 提交信息"""
return (
"阅读用户提供的 diff,按 Conventional Commits 规范生成提交信息:\n"
"`type(scope): summary`,空一行写动机与影响。\n"
"type 选自 feat/fix/docs/refactor/test/chore。"
)
# 2) 参数化:可调语言/关注点/严重度的审查指令
@mcp.prompt()
def code_review(language: str, focus: str = "readability",
severity: str = "major") -> str:
"""生成参数化的代码审查指令。
Args:
language: 编程语言,如 python / typescript
focus: 关注点 security / performance / readability
severity: 只报该级别以上 minor / major / critical
"""
checklist = {
"security": "输入校验、越权、注入、敏感信息泄露",
"performance": "复杂度、N+1 查询、冗余拷贝、锁粒度",
"readability": "命名、函数长度、副作用、注释一致性",
}.get(focus, "通用质量")
return (
f"你是 {language} 审查专家,只看【{focus}】:{checklist}。\n"
f"只报告 {severity} 及以上问题。"
"输出表格【位置 | 问题 | 建议 | 严重度】,末尾给合并结论。"
)
# 3) 动态读文件 + 内嵌资源
@mcp.prompt()
def review_file(path: str, focus: str = "readability"):
"""读取仓库内源文件并生成审查指令。
Args:
path: 相对仓库根的文件路径
focus: 关注点
"""
target = (SAFE_ROOT / path).resolve()
if not str(target).startswith(str(SAFE_ROOT)):
return {"role": "user", "content": {"type": "text",
"text": f"拒绝访问:{path} 超出允许根目录。"}}
if not target.exists():
return {"role": "user", "content": {"type": "text",
"text": f"文件不存在:{target}"}}
content = target.read_text(encoding="utf-8")[:MAX_CHARS]
return [
{"role": "user", "content": {"type": "resource", "resource": {
"uri": target.as_uri(),
"mimeType": f"text/x-{target.suffix.lstrip('.') or 'plain'}",
"text": content,
}}},
{"role": "user", "content": {"type": "text", "text": (
f"基于上面文件,关注【{focus}】,列出需要修改之处"
"(带行号与改法)。"
)}},
]
# 4) 多消息编排:安全审计开场
@mcp.prompt()
def security_audit(target: str):
"""编排多轮安全审计开场白。
Args:
target: 审计目标,如 '用户认证服务'
"""
return [
{"role": "user", "content": {"type": "text",
"text": f"我们要对【{target}】做安全审计。"}},
{"role": "assistant", "content": {"type": "text",
"text": "明白。我先枚举信任边界与攻击面,再按 OWASP Top 10 核查。"}},
{"role": "user", "content": {"type": "text",
"text": "请先输出威胁清单(表格),再按修复成本排序,先别改代码。"}},
]
if __name__ == "__main__":
mcp.run()
注册并试跑:
# 注册
claude mcp add review-prompts -- python /home/gprshome/review_prompts.py
# 进入任意项目,启动 Claude Code 后:
/mcp # 确认 connected
/mcp__review-prompts__code_review # 选 python / security / critical
/mcp__review-prompts__review_file # 输入 src/auth.py
/mcp__review-prompts__security_audit # 输入 用户认证服务
你会观察到:code_review 只注入一段纯文本指令;review_file 注入了带高亮的代码块加指令;security_audit 直接在对话里铺好三段开场,LLM 顺着给威胁表。同一台服务器,四种形态,全部由"提示词原语"统一承载。
prompts/list 为空:99% 是没写 docstring,或函数用了不可推断的参数类型。.mcp.json 路径与解释器;MCP 连接失败时 /mcp 会显示具体报错(如 ModuleNotFoundError)。prompts/get 里截断,或只返回摘要+URI,让用户决定是否进一步展开。.mcp.json 和脚本提交到仓库,队友拉取后 /mcp 自动加载;提示词随仓库版本化,比散落在各人 ~/.claude/commands/ 里更易维护。~/.claude/commands/*.md)轻量、纯文本、单文件;提示词原语胜在可参数化、可动态计算、可跨客户端复用(同一个 MCP 服务器在 Claude Code、Cursor、IDE 插件里都能用)。需要逻辑与参数就上提示词原语,只是一段固定文案就用原生命令。sequenceDiagram
participant U as 用户
participant CC as Claude Code(客户端)
participant S as 提示词服务器(FastMCP)
participant FS as 本地文件系统
Note over CC,S: 启动阶段
CC->>S: spawn stdio 进程
S-->>CC: initialize 完成
CC->>S: prompts/list
S-->>CC: [commit_message, code_review, review_file, security_audit]
Note over U,FS: 使用阶段
U->>CC: 输入 /mcp__...__review_file path=src/auth.py
CC->>S: prompts/get {name: review_file, arguments:{path, focus}}
activate S
S->>FS: 读取 + 路径收敛校验
FS-->>S: 文件内容
S->>S: 截断 + 拼消息 + 内嵌 resource
S-->>CC: messages[user:resource, user:text]
deactivate S
CC->>CC: 注入对话历史
CC->>U: LLM 基于注入内容产出审查结果
modelcontextprotocol.io/specification/2025-06-18/server/prompts——prompts/list、prompts/get 的权威协议定义。gofastmcp.com——@mcp.prompt() 装饰器、消息返回类型的最新用法。prompts/list、prompts/get、arguments schema、embedded resource 与多消息结构。[!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 等场景全覆盖。
