Appearance
第7章 Prompt:可复用的交互模板
"A good prompt is not just a sentence—it is a reusable contract between the user and the model."
本章要点
- 理解 Prompt 在 MCP 三大原语中的独特定位:用户控制平面
- 掌握 Prompt 的完整数据结构:name、description、arguments、messages
- 学会使用动态参数让 Prompt 成为真正的模板
- 理解 Prompt 中嵌入资源(Embedded Resource)的机制
- 对比 TypeScript SDK 和 Python SDK 中
prompt()方法的注册方式 - 通过代码审查、Bug 报告、数据分析三个模板,建立实战直觉
7.1 为什么需要 Prompt
在前两章中,我们已经认识了 MCP 的另外两个原语:Tool 让模型能够执行操作,Resource 让应用程序能够提供上下文。但有一个问题被悄悄忽略了——用户自己呢?
设想一个日常场景:你在使用 AI 编程助手,每次做代码审查时,你都要手动输入一段冗长的提示词:"请从安全性、性能、可读性三个维度审查以下代码,指出问题并给出改进建议……"。这段话你可能每天要输入十几次。
Prompt 就是为解决这个问题而生的。它本质上是服务器预定义的、用户可选择的交互模板——类似于聊天工具中的斜杠命令(/review、/summarize),用户选中后,客户端从服务器获取完整的消息模板,填入参数,直接发送给模型。
关键词是用户控制。与 Tool 不同(Tool 由模型决定何时调用),Prompt 的触发权完全在用户手中。用户看到可用的 Prompt 列表,主动选择要使用哪一个,填入必要的参数,然后发起对话。
7.2 三大控制平面:设计哲学
在深入 Prompt 的技术细节之前,让我们先从宏观视角理解 MCP 的设计哲学。MCP 定义了三种原语,每一种对应一个不同的控制平面:
| 原语 | 控制者 | 类比 | 典型交互 |
|---|---|---|---|
| Tool | AI 模型 | 函数调用 | 模型判断需要查询数据库,自动调用 query_db |
| Resource | 应用程序 | 文件附件 | IDE 将当前打开的文件作为上下文附加到对话中 |
| Prompt | 用户 | 斜杠命令 | 用户输入 /review,选择代码审查模板 |
这三个控制平面的划分并非随意为之。它反映了一个核心设计原则:不同类型的决策应该由最合适的主体来做。模型擅长判断何时需要工具辅助;应用程序知道当前的工作上下文是什么;而用户最清楚自己的意图和工作流程。
Prompt 属于用户控制平面,意味着:
- 服务器暴露可用的 Prompt 列表给客户端
- 客户端展示这些 Prompt 给用户(通常以斜杠命令或菜单的形式)
- 用户主动选择一个 Prompt 并填入参数
- 客户端向服务器请求该 Prompt 的完整消息内容
- 消息内容被插入到对话中,发送给模型
7.3 Prompt 的数据结构
7.3.1 Prompt 定义
一个 Prompt 在协议层面由以下字段描述:
json
{
"name": "code_review",
"title": "Request Code Review",
"description": "Asks the LLM to analyze code quality and suggest improvements",
"arguments": [
{
"name": "code",
"description": "The code to review",
"required": true
},
{
"name": "language",
"description": "Programming language of the code",
"required": false
}
]
}各字段的含义:
name:Prompt 的唯一标识符,用于在prompts/get请求中引用。类似于函数名,不可重复。title:可选的人类可读标题,用于 UI 展示。如果你的 Prompt 名为code_review,title 可以是"代码审查"这样更友好的文本。description:可选的描述信息,帮助用户理解这个 Prompt 做什么。arguments:参数列表,每个参数有name、description和required三个字段。参数是 Prompt 实现动态化的关键——同一个模板,填入不同参数,生成不同的消息内容。
7.3.2 Prompt 结果:消息数组
当客户端调用 prompts/get 获取一个 Prompt 时,服务器返回的是一个 GetPromptResult,其核心是一个 messages 数组:
json
{
"description": "Code review prompt",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this Python code:\ndef hello():\n print('world')"
}
}
]
}每条消息(PromptMessage)包含两个字段:
role:"user"或"assistant",表示这条消息的角色。是的,Prompt 可以预设多轮对话——比如先用一条assistant消息设定角色("你是一位资深代码审查专家"),再用user消息提出具体请求。content:消息内容,支持三种类型——文本(text)、图片(image)和嵌入资源(resource)。
7.3.3 内容类型详解
文本内容是最常见的类型:
json
{
"type": "text",
"text": "请从安全性、性能、可读性三个维度审查以下代码……"
}图片内容支持多模态交互,数据必须经过 Base64 编码:
json
{
"type": "image",
"data": "base64-encoded-image-data",
"mimeType": "image/png"
}嵌入资源是 Prompt 与 Resource 原语的交汇点,我们在 7.6 节会详细讨论。
7.4 协议消息流
理解了数据结构后,让我们看看 Prompt 在协议层面是如何工作的。
7.4.1 能力声明
服务器在初始化阶段必须声明 prompts 能力:
json
{
"capabilities": {
"prompts": {
"listChanged": true
}
}
}listChanged 表示服务器是否会在 Prompt 列表变化时发送通知。如果为 true,客户端可以在收到 notifications/prompts/list_changed 通知后重新拉取列表。
7.4.2 发现与使用
Prompt 的交互流程分为两步:先发现(list),再使用(get)。
prompts/list 请求用于获取服务器所有可用的 Prompt,支持分页(通过 cursor 参数):
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/list",
"params": {
"cursor": "optional-cursor-value"
}
}prompts/get 请求用于获取特定 Prompt 的消息内容:
json
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello():\n print('world')"
}
}
}服务器收到请求后,根据参数动态生成消息数组并返回。这里的"动态生成"是关键——Prompt 不是静态文本,而是由服务器端代码根据参数实时构造的。
7.5 动态 Prompt:参数作为变量
Prompt 的真正威力在于动态化。参数(arguments)使得同一个 Prompt 定义可以根据不同的输入生成完全不同的消息内容。
以一个数据分析 Prompt 为例。用户选择 /analyze_table,填入表名 users,服务器端的处理逻辑可能是:
- 根据表名查询数据库的表结构(schema)
- 采样若干行数据
- 将 schema 和样本数据组装成消息
返回的 messages 可能像这样:
json
{
"messages": [
{
"role": "assistant",
"content": {
"type": "text",
"text": "我是一位数据分析专家,擅长从数据中发现洞察。"
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "请分析 users 表。\n\n表结构:\nid INTEGER PRIMARY KEY\nname TEXT NOT NULL\nemail TEXT UNIQUE\ncreated_at TIMESTAMP\n\n样本数据:\n| id | name | email | created_at |\n|----|------|-------|------------|\n| 1 | 张三 | zhang@ex.com | 2024-01-15 |\n| 2 | 李四 | li@ex.com | 2024-02-20 |"
}
}
]
}注意这里的多轮结构:第一条 assistant 消息为模型设定了角色,第二条 user 消息包含了服务器从数据库中实时查询到的结构和数据。用户只需要输入一个表名,服务器完成了所有繁重的上下文准备工作。
7.6 嵌入资源:Prompt 与 Resource 的交汇
Prompt 消息中不仅可以包含纯文本,还可以嵌入 MCP Resource。这意味着 Prompt 可以引用服务器管理的文件、文档、代码等资源,将它们直接注入到对话中。
嵌入资源的消息格式:
json
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "file:///project/src/main.py",
"mimeType": "text/x-python",
"text": "import os\nimport sys\n\ndef main():\n ..."
}
}
}资源内容可以是文本(text 字段),也可以是二进制数据(blob 字段,Base64 编码)。必须包含有效的 URI 和 MIME 类型。
这个机制有什么用?考虑一个代码审查 Prompt:用户选择 /review,传入文件路径作为参数,服务器读取文件内容,以嵌入资源的方式返回。这样做的好处是——客户端可以识别出这是一个资源引用,在 UI 中以特殊方式展示(比如显示为可折叠的代码块,带有文件名和语法高亮),而不是一堆原始文本。
7.7 SDK 实现:TypeScript 与 Python
7.7.1 TypeScript SDK
在 TypeScript SDK 中,通过 McpServer 类的 registerPrompt 方法注册 Prompt:
typescript
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const server = new McpServer({
name: 'my-server',
version: '1.0.0'
});
server.registerPrompt(
'review-code',
{
title: 'Code Review',
description: 'Review code for best practices',
argsSchema: z.object({
code: z.string(),
language: z.string().optional()
})
},
({ code, language }) => ({
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: `Please review this ${language ?? ''} code:\n\n${code}`
}
}
]
})
);几个值得注意的设计点:
argsSchema使用 Zod:TypeScript SDK 使用 Zod 等 Standard Schema 兼容库定义参数结构,框架自动将其转换为 JSON Schema 暴露给客户端,并在请求到达时执行校验。- 回调函数返回
GetPromptResult:回调接收经过校验的参数,返回包含messages数组的对象。 - 注册时自动处理列表和获取:
registerPrompt内部同时设置了prompts/list和prompts/get的请求处理器,开发者无需手动处理协议细节。
7.7.2 Python SDK
Python SDK 使用装饰器风格注册 Prompt,更加简洁:
python
from mcp.server.mcpserver import MCPServer
server = MCPServer(name="my-server")
@server.prompt()
def analyze_table(table_name: str) -> list[Message]:
"""Analyze a database table structure and content."""
schema = read_table_schema(table_name)
return [
{
"role": "user",
"content": f"Analyze this schema:\n{schema}"
}
]
@server.prompt()
async def analyze_file(path: str) -> list[Message]:
"""Analyze a file with embedded resource."""
content = await read_file(path)
return [
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": f"file://{path}",
"text": content
}
}
}
]Python SDK 的设计特点:
- 装饰器
@server.prompt():注意括号不能省略。SDK 源码中专门做了检查——如果你写了@server.prompt(不带括号),会抛出TypeError并给出明确提示。 - 从函数签名推导参数:
Prompt.from_function(func)会通过 Python 的内省机制从函数签名中自动提取参数名、类型和是否必填,无需手动声明arguments。 - 支持同步和异步:回调函数可以是普通函数,也可以是
async函数。 - 可选参数:装饰器接受
name(默认使用函数名)、title、description、icons等可选参数。
7.7.3 两种 SDK 的对比
| 维度 | TypeScript SDK | Python SDK |
|---|---|---|
| 注册方式 | registerPrompt() 方法 | @server.prompt() 装饰器 |
| 参数定义 | 显式 argsSchema(Zod) | 从函数签名自动推导 |
| 返回类型 | GetPromptResult 对象 | list[Message] |
| 变更通知 | sendPromptListChanged() | 框架自动处理 |
| 禁用支持 | enabled 属性 | 通过 PromptManager 管理 |
7.8 实战用例
7.8.1 代码审查模板
python
@server.prompt(
name="code_review",
description="Multi-dimensional code review"
)
def code_review(code: str, language: str = "python") -> list[Message]:
return [
{
"role": "assistant",
"content": "I am a senior code reviewer. I will analyze code "
"from three perspectives: security, performance, "
"and readability."
},
{
"role": "user",
"content": f"Please review the following {language} code. "
f"For each issue found, indicate its severity "
f"(critical/warning/info) and provide a fix.\n\n"
f"```{language}\n{code}\n```"
}
]这个模板的设计要点:先用 assistant 消息设定专家角色,再用 user 消息提出结构化的审查请求。language 参数有默认值,为可选参数。
7.8.2 Bug 报告模板
python
@server.prompt(
name="bug_report",
description="Generate a structured bug report"
)
def bug_report(
title: str,
steps: str,
expected: str,
actual: str
) -> list[Message]:
return [
{
"role": "user",
"content": f"Please help me write a detailed bug report.\n\n"
f"**Title**: {title}\n"
f"**Steps to reproduce**:\n{steps}\n"
f"**Expected behavior**: {expected}\n"
f"**Actual behavior**: {actual}\n\n"
f"Please analyze the possible root cause and "
f"suggest debugging approaches."
}
]这个模板将用户的碎片化输入(标题、步骤、期望行为、实际行为)组装成结构化的 Bug 报告,同时请求模型分析根因。四个参数全部为必填。
7.8.3 数据分析工作流
typescript
server.registerPrompt(
'analyze-dataset',
{
title: 'Dataset Analysis',
description: 'Comprehensive analysis of a dataset',
argsSchema: z.object({
table_name: z.string(),
focus: z.enum(['trends', 'anomalies', 'correlations']).optional()
})
},
async ({ table_name, focus }, ctx) => {
// 服务器端动态查询数据库
const schema = await getTableSchema(table_name);
const stats = await getBasicStats(table_name);
return {
messages: [
{
role: 'assistant' as const,
content: {
type: 'text' as const,
text: 'I am a data analyst. I will provide insights based on the data structure and statistics.'
}
},
{
role: 'user' as const,
content: {
type: 'resource' as const,
resource: {
uri: `db://schemas/${table_name}`,
mimeType: 'application/json',
text: JSON.stringify(schema, null, 2)
}
}
},
{
role: 'user' as const,
content: {
type: 'text' as const,
text: `Basic statistics:\n${JSON.stringify(stats, null, 2)}\n\n` +
`Please perform a comprehensive analysis` +
(focus ? `, focusing on ${focus}` : '') + '.'
}
}
]
};
}
);这个示例展示了 Prompt 的高级用法:异步回调中执行数据库查询,结合嵌入资源和文本内容构建多条消息,可选的 focus 参数控制分析方向。
7.9 错误处理
服务器在处理 Prompt 请求时应返回标准的 JSON-RPC 错误码:
-32602(Invalid params):Prompt 名称不存在,或缺少必填参数。在两个 SDK 中,如果请求的 Prompt 未注册或已被禁用,框架会自动返回此错误。-32603(Internal error):服务器内部处理错误,比如回调函数中的数据库查询失败。
从 TypeScript SDK 源码可以看到具体的错误处理逻辑:
typescript
const prompt = this._registeredPrompts[request.params.name];
if (!prompt) {
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`Prompt ${request.params.name} not found`
);
}
if (!prompt.enabled) {
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`Prompt ${request.params.name} disabled`
);
}除了 not found 之外,还有 disabled 状态——这意味着 Prompt 可以被注册但暂时禁用,客户端在 prompts/list 中将不会看到它。
7.10 设计指南
在实际开发 MCP Server 时,设计 Prompt 应遵循以下原则:
1. Prompt 是用户意图的快捷方式,不是万能工具。 如果一个操作应该由模型自主决定是否执行,它应该是 Tool 而不是 Prompt。如果一个数据源应该自动附加到对话中,它应该是 Resource 而不是 Prompt。
2. 参数命名要直观。 用户在填写参数时需要理解每个参数的含义。code 比 input_data 更好,language 比 lang_type 更好。description 字段不要省略。
3. 善用多轮消息结构。 assistant 角色的预设消息可以有效引导模型的行为模式。一条好的角色设定消息,往往比在 user 消息中反复强调"你是一个专家"更有效。
4. 考虑嵌入资源的时机。 当你的 Prompt 需要引用文件、数据库记录或其他结构化数据时,优先使用嵌入资源而非纯文本。嵌入资源让客户端能够更智能地处理和展示这些内容。
5. 校验参数。 服务器在处理 prompts/get 时应验证所有必填参数是否已提供,参数值是否合法。SDK 会帮你完成类型级别的校验,但业务级别的校验(比如表名是否存在)需要你自己处理。
7.11 本章小结
Prompt 是 MCP 三大原语中最贴近用户的一个。它的设计看似简单——不过是预定义消息模板——但其背后蕴含了重要的设计哲学:将控制权交给最合适的主体。
回顾本章的核心内容:
- Prompt 属于用户控制平面,由用户主动选择和触发,与 Tool(模型控制)和 Resource(应用程序控制)形成三足鼎立
- 一个 Prompt 由
name、description、arguments定义,通过prompts/get返回messages数组 - Messages 支持
text、image、resource三种内容类型,可以包含多条不同角色的消息 - 动态参数让 Prompt 从静态模板变成可编程的消息工厂——服务器可以根据参数执行数据库查询、文件读取等操作
- 嵌入资源将 Prompt 与 Resource 连接起来,让模板能够引用服务器管理的任意资源
- TypeScript SDK 通过
registerPrompt()方法注册,Python SDK 通过@server.prompt()装饰器注册
在下一章中,我们将进入 SDK 实战环节,从源码层面深入理解 TypeScript SDK 的服务器实现。