扫码
40.67M · 2026-04-17
:::tip 本章要点
prompt() 方法的注册方式在前两章中,我们已经认识了 MCP 的另外两个原语:Tool 让模型能够执行操作,Resource 让应用程序能够提供上下文。但有一个问题被悄悄忽略了——用户自己呢?
设想一个日常场景:你在使用 AI 编程助手,每次做代码审查时,你都要手动输入一段冗长的提示词:"请从安全性、性能、可读性三个维度审查以下代码,指出问题并给出改进建议……"。这段话你可能每天要输入十几次。
Prompt 就是为解决这个问题而生的。它本质上是服务器预定义的、用户可选择的交互模板——类似于聊天工具中的斜杠命令(/review、/summarize),用户选中后,客户端从服务器获取完整的消息模板,填入参数,直接发送给模型。
关键词是用户控制。与 Tool 不同(Tool 由模型决定何时调用),Prompt 的触发权完全在用户手中。用户看到可用的 Prompt 列表,主动选择要使用哪一个,填入必要的参数,然后发起对话。
在深入 Prompt 的技术细节之前,让我们先从宏观视角理解 MCP 的设计哲学。MCP 定义了三种原语,每一种对应一个不同的控制平面:
graph TB
subgraph MCP["MCP 三大原语"]
direction TB
T["Tool<br/>工具"]
R["Resource<br/>资源"]
P["Prompt<br/>提示模板"]
end
M["AI 模型"] -->|"模型决定调用"| T
A["应用程序"] -->|"应用程序管理"| R
U["用户"] -->|"用户主动选择"| P
T -->|"执行操作<br/>写数据库、调 API"| S1["MCP Server"]
R -->|"提供上下文<br/>文件、数据库记录"| S2["MCP Server"]
P -->|"获取模板<br/>预定义的消息序列"| S3["MCP Server"]
style T fill:#f59e0b,color:#fff,stroke:none
style R fill:#3b82f6,color:#fff,stroke:none
style P fill:#10b981,color:#fff,stroke:none
style M fill:#8b5cf6,color:#fff,stroke:none
style A fill:#ec4899,color:#fff,stroke:none
style U fill:#6366f1,color:#fff,stroke:none
| 原语 | 控制者 | 类比 | 典型交互 |
|---|---|---|---|
| Tool | AI 模型 | 函数调用 | 模型判断需要查询数据库,自动调用 query_db |
| Resource | 应用程序 | 文件附件 | IDE 将当前打开的文件作为上下文附加到对话中 |
| Prompt | 用户 | 斜杠命令 | 用户输入 /review,选择代码审查模板 |
这三个控制平面的划分并非随意为之。它反映了一个核心设计原则:不同类型的决策应该由最合适的主体来做。模型擅长判断何时需要工具辅助;应用程序知道当前的工作上下文是什么;而用户最清楚自己的意图和工作流程。
Prompt 属于用户控制平面,意味着:
一个 Prompt 在协议层面由以下字段描述:
{
"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 实现动态化的关键——同一个模板,填入不同参数,生成不同的消息内容。当客户端调用 prompts/get 获取一个 Prompt 时,服务器返回的是一个 GetPromptResult,其核心是一个 messages 数组:
{
"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)。文本内容是最常见的类型:
{
"type": "text",
"text": "请从安全性、性能、可读性三个维度审查以下代码……"
}
图片内容支持多模态交互,数据必须经过 Base64 编码:
{
"type": "image",
"data": "base64-encoded-image-data",
"mimeType": "image/png"
}
嵌入资源是 Prompt 与 Resource 原语的交汇点,我们在 7.6 节会详细讨论。
理解了数据结构后,让我们看看 Prompt 在协议层面是如何工作的。
服务器在初始化阶段必须声明 prompts 能力:
{
"capabilities": {
"prompts": {
"listChanged": true
}
}
}
listChanged 表示服务器是否会在 Prompt 列表变化时发送通知。如果为 true,客户端可以在收到 notifications/prompts/list_changed 通知后重新拉取列表。
Prompt 的交互流程分为两步:先发现(list),再使用(get)。
sequenceDiagram
participant User as 用户
participant Client as MCP Client
participant Server as MCP Server
Note over Client,Server: 第一步:发现可用 Prompt
Client->>Server: prompts/list
Server-->>Client: 返回 Prompt 列表<br/>[code_review, bug_report, ...]
Client->>User: 展示斜杠命令菜单
Note over User,Server: 第二步:用户选择并使用
User->>Client: 选择 /code_review<br/>填入 code 参数
Client->>Server: prompts/get<br/>name="code_review"<br/>arguments={code: "..."}
Server-->>Client: 返回 messages 数组
Client->>Client: 将 messages 插入对话
Client->>Server: 正常的 LLM 对话流程
opt Prompt 列表变更
Server--)Client: notifications/prompts/list_changed
Client->>Server: prompts/list
Server-->>Client: 更新后的 Prompt 列表
end
prompts/list 请求用于获取服务器所有可用的 Prompt,支持分页(通过 cursor 参数):
{
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/list",
"params": {
"cursor": "optional-cursor-value"
}
}
prompts/get 请求用于获取特定 Prompt 的消息内容:
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello():n print('world')"
}
}
}
服务器收到请求后,根据参数动态生成消息数组并返回。这里的"动态生成"是关键——Prompt 不是静态文本,而是由服务器端代码根据参数实时构造的。
Prompt 的真正威力在于动态化。参数(arguments)使得同一个 Prompt 定义可以根据不同的输入生成完全不同的消息内容。
以一个数据分析 Prompt 为例。用户选择 /analyze_table,填入表名 users,服务器端的处理逻辑可能是:
返回的 messages 可能像这样:
{
"messages": [
{
"role": "assistant",
"content": {
"type": "text",
"text": "我是一位数据分析专家,擅长从数据中发现洞察。"
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "请分析 users 表。nn表结构:nid INTEGER PRIMARY KEYnname TEXT NOT NULLnemail TEXT UNIQUEncreated_at TIMESTAMPnn样本数据: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 消息包含了服务器从数据库中实时查询到的结构和数据。用户只需要输入一个表名,服务器完成了所有繁重的上下文准备工作。
Prompt 消息中不仅可以包含纯文本,还可以嵌入 MCP Resource。这意味着 Prompt 可以引用服务器管理的文件、文档、代码等资源,将它们直接注入到对话中。
嵌入资源的消息格式:
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "file:///project/src/main.py",
"mimeType": "text/x-python",
"text": "import osnimport sysnndef main():n ..."
}
}
}
资源内容可以是文本(text 字段),也可以是二进制数据(blob 字段,Base64 编码)。必须包含有效的 URI 和 MIME 类型。
这个机制有什么用?考虑一个代码审查 Prompt:用户选择 /review,传入文件路径作为参数,服务器读取文件内容,以嵌入资源的方式返回。这样做的好处是——客户端可以识别出这是一个资源引用,在 UI 中以特殊方式展示(比如显示为可折叠的代码块,带有文件名和语法高亮),而不是一堆原始文本。
在 TypeScript SDK 中,通过 McpServer 类的 registerPrompt 方法注册 Prompt:
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:nn${code}`
}
}
]
})
);
几个值得注意的设计点:
argsSchema 使用 Zod:TypeScript SDK 使用 Zod 等 Standard Schema 兼容库定义参数结构,框架自动将其转换为 JSON Schema 暴露给客户端,并在请求到达时执行校验。GetPromptResult:回调接收经过校验的参数,返回包含 messages 数组的对象。registerPrompt 内部同时设置了 prompts/list 和 prompts/get 的请求处理器,开发者无需手动处理协议细节。Python SDK 使用装饰器风格注册 Prompt,更加简洁:
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 等可选参数。| 维度 | TypeScript SDK | Python SDK |
|---|---|---|
| 注册方式 | registerPrompt() 方法 | @server.prompt() 装饰器 |
| 参数定义 | 显式 argsSchema(Zod) | 从函数签名自动推导 |
| 返回类型 | GetPromptResult 对象 | list[Message] |
| 变更通知 | sendPromptListChanged() | 框架自动处理 |
| 禁用支持 | enabled 属性 | 通过 PromptManager 管理 |
@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.nn"
f"```{language}n{code}n```"
}
]
这个模板的设计要点:先用 assistant 消息设定专家角色,再用 user 消息提出结构化的审查请求。language 参数有默认值,为可选参数。
@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.nn"
f"**Title**: {title}n"
f"**Steps to reproduce**:n{steps}n"
f"**Expected behavior**: {expected}n"
f"**Actual behavior**: {actual}nn"
f"Please analyze the possible root cause and "
f"suggest debugging approaches."
}
]
这个模板将用户的碎片化输入(标题、步骤、期望行为、实际行为)组装成结构化的 Bug 报告,同时请求模型分析根因。四个参数全部为必填。
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)}nn` +
`Please perform a comprehensive analysis` +
(focus ? `, focusing on ${focus}` : '') + '.'
}
}
]
};
}
);
这个示例展示了 Prompt 的高级用法:异步回调中执行数据库查询,结合嵌入资源和文本内容构建多条消息,可选的 focus 参数控制分析方向。
服务器在处理 Prompt 请求时应返回标准的 JSON-RPC 错误码:
-32602(Invalid params):Prompt 名称不存在,或缺少必填参数。在两个 SDK 中,如果请求的 Prompt 未注册或已被禁用,框架会自动返回此错误。-32603(Internal error):服务器内部处理错误,比如回调函数中的数据库查询失败。从 TypeScript SDK 源码可以看到具体的错误处理逻辑:
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 中将不会看到它。
在实际开发 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 会帮你完成类型级别的校验,但业务级别的校验(比如表名是否存在)需要你自己处理。
Prompt 是 MCP 三大原语中最贴近用户的一个。它的设计看似简单——不过是预定义消息模板——但其背后蕴含了重要的设计哲学:将控制权交给最合适的主体。
回顾本章的核心内容:
name、description、arguments 定义,通过 prompts/get 返回 messages 数组text、image、resource 三种内容类型,可以包含多条不同角色的消息registerPrompt() 方法注册,Python SDK 通过 @server.prompt() 装饰器注册在下一章中,我们将进入 SDK 实战环节,从源码层面深入理解 TypeScript SDK 的服务器实现。