MCP 协议设计与实现

第7章 Prompt:可复用的交互模板

作者 杨艺韬 · 7,487 字

"A good prompt is not just a sentence — it is a reusable contract between the user and the model."

"一个好的 Prompt 不只是一句话——它是用户和模型之间可复用的契约:双方都知道这句话代表什么,每一次调用都能给出可预期的输出。"

本章要点

  • 理解 Prompt 在 MCP 三大原语中的独特定位:用户控制平面
  • 掌握 Prompt 的完整数据结构:name、description、arguments、messages
  • 学会使用动态参数让 Prompt 成为真正的可编程模板
  • 理解 Prompt 中嵌入资源(Embedded Resource)的机制及其客户端渲染意义
  • 对比 TypeScript SDK 和 Python SDK 中 prompt() 方法的注册方式
  • 通过代码审查、Bug 报告、数据分析三个模板,建立实战直觉
  • 掌握 6 条 Prompt 设计指南与 5 种常见反模式

7.1 为什么需要 Prompt

7.1.1 一位工程师一天的重复劳动

先来看一组真实的数字。我统计过一位资深工程师使用 AI 编程助手一天的输入——共发起 47 次对话,其中有 19 次,开头几乎一字不差:

"请帮我审查以下代码,从安全性、性能、可读性三个维度分析问题并给出改进建议……"

还有 8 次以这样开头:

"请帮我写一份 bug 报告,要包含现象、复现步骤、期望行为、实际行为、可能的原因分析……"

每天多次、每次数十字、几乎完全相同。你会意识到:这些重复的"开场白"本质上是一种未被抽象的接口

在前面两章中,我们已经认识了 MCP 的另外两个原语:Tool 让模型能够执行操作,Resource 让应用程序能够提供上下文。但有一个关键角色被忽略了——用户自己的高频输入模式

Prompt 就是为这个场景而生。它本质上是服务器预定义的、用户可选择的交互模板——类似于聊天工具中的斜杠命令(/review/summarize),用户选中后,客户端从服务器获取完整的消息模板,填入参数,直接发送给模型。

7.1.2 从 GitHub Copilot Chat 的 /explain 说起

如果你用过 GitHub Copilot Chat,一定见过它的几个内置斜杠命令:/explain/fix/doc/tests。这些命令背后做的事情——在你的查询前加一段标准化的引导语——正是 Prompt 的核心理念。

Copilot Chat 把这些斜杠命令写死在客户端里;而 MCP 的创新是把"定义斜杠命令的权力"下放给 Server。这样一来:

  • 每个领域的工具开发者都可以定义自己的斜杠命令
  • 用户打开一个数据库客户端,立刻就有 /analyze_table/suggest_index 等命令
  • 打开文档编辑器,就有 /outline/summarize_chapter 等命令
  • 这些命令是跨 AI 客户端通用的——用 Claude Desktop 或 Cursor 都能看到

关键词是用户控制。与 Tool 不同(Tool 由模型决定何时调用),Prompt 的触发权完全在用户手中。用户看到可用的 Prompt 列表,主动选择要使用哪一个,填入必要的参数,然后发起对话。

7.2 三大控制平面:设计哲学

7.2.1 三原语的控制权划分

在深入 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,选择代码审查模板 高(用户主动明示)

这三个控制平面的划分并非随意为之。它反映了一个核心设计原则:不同类型的决策应该由最合适的主体来做

  • 模型擅长判断何时需要工具辅助——它能看到当前对话的意图
  • 应用程序知道当前的工作上下文是什么——IDE 知道用户打开了哪个文件
  • 用户最清楚自己的意图和工作流程——他知道自己今天要做代码审查还是写 bug 报告

7.2.2 Prompt 控制平面的交互五步曲

Prompt 属于用户控制平面,意味着整个流程是:

sequenceDiagram
    participant U as 用户
    participant C as MCP Client
    participant S as MCP Server
    participant M as LLM
    S->>C: (已声明 prompts 能力)
    C->>S: prompts/list
    S-->>C: [code_review, bug_report, analyze_table, ...]
    C->>U: 展示斜杠命令菜单<br/>(UI: /, 自动补全列表)
    U->>C: 输入 "/review", 选择 code_review
    C->>U: 弹出参数表单<br/>(code: ?, language: ?)
    U->>C: 填入 code="...", language="python"
    C->>S: prompts/get<br/>(name, arguments)
    Note over S: 动态生成消息<br/>(可能查数据库、读文件)
    S-->>C: GetPromptResult<br/>{ messages: [...] }
    C->>C: 把 messages 插入对话上下文
    C->>M: 发送对话(含模板消息)
    M-->>C: LLM 响应
    C->>U: 展示结果

五个关键时刻:

  1. 声明:Server 通过 prompts 能力声明支持 Prompt
  2. 枚举:Client 通过 prompts/list 拉取 Server 的可用 Prompt 列表
  3. 选择:用户在 UI(斜杠菜单)中选择某个 Prompt
  4. 获取:Client 通过 prompts/get 获取填入参数后的消息序列
  5. 发送:消息序列被插入对话上下文,正常发送给 LLM

7.3 Prompt 的数据结构

7.3.1 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
    }
  ]
}

各字段的含义:

字段 必填 含义 UI 用途
name Prompt 的唯一标识符 斜杠命令名(如 /code_review
title 人类可读标题 菜单项显示文本("代码审查")
description 描述 悬停提示、菜单次行文字
arguments 参数列表 参数表单的字段

每个参数又有:

  • name:字段名
  • description:字段说明(展示给用户)
  • required:是否必填

7.3.2 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)。

7.3.3 三种 content 类型

文本内容是最常见的类型:

{
  "type": "text",
  "text": "请从安全性、性能、可读性三个维度审查以下代码……"
}

图片内容支持多模态交互,数据必须经过 Base64 编码:

{
  "type": "image",
  "data": "base64-encoded-image-data",
  "mimeType": "image/png"
}

嵌入资源是 Prompt 与 Resource 原语的交汇点,我们在 7.6 节详细讨论。

三种类型的对比:

类型 适合场景 体积 客户端处理方式
text 文字指令、代码片段 直接作为 LLM 消息发送
image 截图、图表引用 大(base64 膨胀 33%) 多模态 LLM 直接输入
resource 文件、数据库记录、任何带 URI 的资源 UI 折叠展示、可重新下载

7.4 协议消息流

7.4.1 能力声明

服务器在初始化阶段必须声明 prompts 能力:

{
  "capabilities": {
    "prompts": {
      "listChanged": true
    }
  }
}

listChanged 表示服务器是否会在 Prompt 列表变化时发送通知。如果为 true,客户端可以在收到 notifications/prompts/list_changed 通知后重新拉取列表。

什么时候会发生列表变化?典型场景:

  • 用户在数据库客户端切换了连接——新数据库上有不同的 /analyze_X Prompt
  • 管理员在后台给某些 Prompt 打了"禁用"标
  • 新插件被动态加载,带来了新的 Prompt

7.4.2 发现与使用

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 不是静态文本,而是由服务器端代码根据参数实时构造的

7.5 动态 Prompt:参数作为变量

7.5.1 从静态模板到可编程模板

最朴素的 Prompt 就是"拼字符串"——把参数塞到固定模板中:

def code_review(code: str) -> list:
    return [{"role": "user", "content": f"Please review:\n{code}"}]

但 MCP Prompt 的真正威力在于:它可以在生成消息的过程中执行任意服务器端逻辑

  • 查询数据库获取表 schema
  • 读取文件内容
  • 调用第三方 API
  • 查询向量数据库做 RAG
  • 根据用户的历史行为定制化

以一个数据分析 Prompt 为例。用户选择 /analyze_table,填入表名 users,服务器端的处理逻辑可能是:

  1. 根据表名查询数据库的表结构(schema)
  2. 采样若干行数据
  3. 计算基础统计(行数、null 比例、唯一值数等)
  4. 将 schema、样本、统计组装成消息

返回的 messages 可能像这样:

{
  "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 |\n\n基础统计:\n- 总行数:12847\n- created_at 最早:2023-06-12"
      }
    }
  ]
}

注意这里的多轮结构:第一条 assistant 消息为模型设定了角色,第二条 user 消息包含了服务器从数据库中实时查询到的结构和数据。用户只需要输入一个表名,服务器完成了所有繁重的上下文准备工作

7.5.2 动态 Prompt 的五种常见模式

模式 说明 典型场景
数据采集型 Server 查数据源后塞进上下文 分析数据库表、读取配置
RAG 增强型 Server 用参数做向量搜索,注入相关文档 基于知识库回答、智能搜索
角色设定型 根据参数设定不同的 assistant 角色 多角色模拟、专家咨询
Few-shot 组装型 Server 从历史中挑选最相似的 N 个例子 格式化生成、风格学习
预处理型 Server 对参数做清洗/重写再塞入 安全过滤、格式规范化

每一种模式都不是"能不能做"的问题,而是让 Server 承担起"上下文工程"的责任,Client 只负责收集用户参数和展示结果。

7.6 嵌入资源:Prompt 与 Resource 的交汇

7.6.1 为什么要嵌入资源

Prompt 消息中不仅可以包含纯文本,还可以嵌入 MCP Resource。这意味着 Prompt 可以引用服务器管理的文件、文档、代码等资源,将它们直接注入到对话中。

嵌入资源的消息格式:

{
  "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 类型。

7.6.2 纯文本 vs 嵌入资源的差别

既然可以把文件内容塞进 text 字段,为什么还要专门的 resource 类型?

维度 text 类型 resource 类型
URI 信息 保留
MIME 类型 保留
客户端渲染 作为普通文本 可折叠、语法高亮、可重新加载
LLM 的感知 扁平的一大段字 可以明确"这是来自某资源的内容"
重用可能性 每次请求都传全文 客户端可缓存 URI 对应内容
可追溯性 用户可点击查看来源

考虑一个代码审查 Prompt:用户选择 /review,传入文件路径作为参数,服务器读取文件内容,以嵌入资源的方式返回。这样做的好处是——客户端可以识别出这是一个资源引用,在 UI 中以特殊方式展示(比如显示为可折叠的代码块,带有文件名和语法高亮),而不是一堆原始文本。

这是一种数据血缘的传递——消息里不只有"内容",还有"内容的身份"。

7.7 SDK 实现:TypeScript 与 Python

7.7.1 TypeScript SDK

在 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:\n\n${code}`
        }
      }
    ]
  })
);

几个值得注意的设计点:

  1. argsSchema 使用 Zod:TypeScript SDK 使用 Zod 等 Standard Schema 兼容库定义参数结构,框架自动将其转换为 JSON Schema 暴露给客户端,并在请求到达时执行校验。
  2. 回调函数返回 GetPromptResult:回调接收经过校验的参数,返回包含 messages 数组的对象。
  3. 注册时自动处理列表和获取registerPrompt 内部同时设置了 prompts/listprompts/get 的请求处理器,开发者无需手动处理协议细节。

7.7.2 Python SDK

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 的设计特点(对应 src/mcp/server/mcpserver/prompts/base.pysrc/mcp/server/mcpserver/server.py:747):

1. 装饰器括号强校验——@server.prompt() 的括号不能省略。src/mcp/server/mcpserver/server.py:791-796 有一个主动的 TypeError 守卫:

# server.py:791
# Check if user passed function directly instead of calling decorator
if callable(name):
    raise TypeError(
        "The @prompt decorator was used incorrectly. "
        "Did you forget to call it? Use @prompt() instead of @prompt"
    )

@server.prompt(不带括号)时 Python 会把被装饰的函数直接作为 name 参数传进去——这个 callable(name) 检查正好抓住这种误用,给出精准诊断,不让它继续跑出个难以理解的下游错误。

2. 从函数签名自动推导 arguments——Prompt.from_functionbase.py:78)的核心流程:

# base.py:106
func_arg_metadata = func_metadata(fn, skip_names=[...])
parameters = func_arg_metadata.arg_model.model_json_schema()
for param_name, param in parameters["properties"].items():
    required = param_name in parameters.get("required", [])
    arguments.append(PromptArgument(
        name=param_name,
        description=param.get("description"),
        required=required,
    ))

func_metadata 用 Pydantic 根据函数签名生成一个动态 model,再从 model 的 JSON Schema 里拉出参数列表。"没有默认值的参数 → required=True"这个判断就从 schema 的 required 字段里读——完全跟 Pydantic 规则走。这意味着在函数签名里写 table_name: str(无默认值)和 table_name: str = "users"(有默认值),装饰器暴露给 MCP 客户端的 arguments 就会有不同的 required 值。

3. 返回类型的四种多态——src/mcp/server/mcpserver/prompts/base.py:55 给出了返回值的类型别名:

SyncPromptResult = str | Message | dict[str, Any] | Sequence[...]
PromptResult     = SyncPromptResult | Awaitable[SyncPromptResult]

render 函数(src/mcp/server/mcpserver/prompts/base.py:166-183)做规整化:

  • 返回 str → 包成 UserMessage(TextContent(text=...))
  • 返回 Message 对象 → 原样保留
  • 返回 dict → 过 message_validator.validate_python(msg),Pydantic 校验
  • 返回任一上述类型的序列 → 逐元素处理
  • 其它类型 → pydantic_core.to_json 序列化后包成 user message(兜底)

这种"返回什么都能工作"的设计让最常见的简单场景(单轮静态 prompt)只需 return "你的 prompt 文本" 一行,而复杂场景(多轮对话、嵌入资源、assistant 预填)可以用完整的 Message 列表。复杂度按需支付

4. 同步函数自动进线程池——如果装饰的函数不是 asyncsrc/mcp/server/mcpserver/prompts/base.py:164 会用 anyio 把它扔进线程:

if is_async_callable(fn):
    result = await fn(**call_args)
else:
    result = await anyio.to_thread.run_sync(functools.partial(self.fn, **call_args))

这个细节不起眼但重要:MCP 服务器跑在 asyncio 事件循环上,如果开发者写了个同步 prompt 函数里调了 time.sleeprequests.get(阻塞 I/O),直接 await 会卡住整个事件循环。anyio.to_thread.run_sync 把它发到 anyio 的默认线程池(大小由 anyio.CapacityLimiter 控制),其他并发请求不受影响。代价是跨线程数据传递有 GIL 开销——所以原生 async 的 prompt 仍然是性能更优的选择。

5. Context 参数自动注入——src/mcp/server/mcpserver/prompts/base.py:102-103find_context_parameter(fn) 扫描函数签名:

if context_kwarg is None:
    context_kwarg = find_context_parameter(fn)

如果函数里有类型为 Context 的参数,框架会记录它的名字(context_kwarg),渲染时调 inject_context 自动填入当前请求的 Context 对象(携带 request_id、logger、lifespan 资源等)。这让 prompt 函数可以写成:

@server.prompt()
async def analyze_table(table_name: str, ctx: Context) -> str:
    await ctx.info(f"Analyzing {table_name}")
    schema = await ctx.lifespan.db.get_schema(table_name)
    return f"Schema:\n{schema}"

ctx 不会暴露给客户端的 arguments 列表(skip_names=[context_kwarg] 把它从 schema 排除)——对客户端透明,对 prompt 实现者却是隐式的能力通道。

6. 必填参数缺失时的 ValueError——src/mcp/server/mcpserver/prompts/base.py:149-154

if self.arguments:
    required = {arg.name for arg in self.arguments if arg.required}
    provided = set(arguments or {})
    missing = required - provided
    if missing:
        raise ValueError(f"Missing required arguments: {missing}")

这个校验在 Pydantic 的 validate_call 之前就拦住——客户端少传参数时拿到的错误是 MCP 协议层面的 ValueError,而不是 Pydantic 的 ValidationError 堆栈——更适合面向客户端。拦截顺序是 "必填检查 → context 注入 → validate_call 校验类型 → 真正执行",层层把关。

7. 可选参数——装饰器本身接受 name(默认函数名)、titledescription(默认从函数 docstring 取——src/mcp/server/mcpserver/prompts/base.py:131description or fn.__doc__ or "")、icons。docstring 作为 description 的默认来源是 Python SDK 一个很 Pythonic 的约定——你写函数文档字符串的时候就已经在给 MCP 客户端写说明。

7.7.3 两种 SDK 的对比

维度 TypeScript SDK Python SDK
注册方式 registerPrompt() 方法 @server.prompt() 装饰器
参数定义 显式 argsSchema(Zod) 从函数签名自动推导
返回类型 GetPromptResult 对象 list[Message]
变更通知 sendPromptListChanged() 框架自动处理
禁用支持 enabled 属性 通过 PromptManager 管理
补全(completion) completable() 包装器 参数 metadata

两种 SDK 的哲学差异:TypeScript 偏向"显式声明 + 类型安全",Python 偏向"函数签名即接口 + 约定胜于配置"。这呼应了两种语言社区的设计文化。

7.8 实战用例

7.8.1 代码审查模板

@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 消息里说"请扮演一个专家"效果好得多
  • 再用 user 消息提出结构化的审查请求(三个维度 + 严重度分级)
  • language 参数有默认值,为可选参数
  • 返回格式要求明确(每个 issue 要带 severity 和 fix),方便下游工具解析

7.8.2 Bug 报告模板

@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 报告,同时请求模型分析根因。四个参数全部为必填。

设计精髓:强迫用户先把事情说清楚。很多 Bug 报告混乱是因为报告者没把"期望"和"实际"区分开。这个模板用参数表单强制结构化。

7.8.3 数据分析工作流

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 参数控制分析方向
  • 三条消息:角色设定 → 结构资源 → 统计数据 + 指令

这是一个典型的"Prompt 即工作流"的例子。Server 在 prompts/get 的一次调用里,完成了数据库查询、数据聚合、消息组装的所有工作。

7.9 错误处理

服务器在处理 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 中将不会看到它。

7.9.1 用户可见的错误文案

错误码是给程序看的,用户看到的是 Client 翻译后的文案。好的 Client 应该:

  • Prompt not found → "该命令已不存在,可能是 Server 版本变化,请刷新"
  • Missing required argument: code → "请填写必填字段:code"
  • Internal error → "服务器处理失败,请稍后重试" + 底下展开技术详情

7.10 Prompt 的 5 种反模式

在看了正确做法后,也要了解什么是错误做法。以下是我在 Code Review 中反复见过的反模式:

反模式 问题 正确做法
参数列表过长(10+ 参数) 用户填表疲劳 拆成多个专门 Prompt,或引入"高级设置"
硬编码敏感信息到 Prompt Token 泄露到用户/日志 通过 Elicitation URL Mode 收集
Prompt 内部做不可逆操作 用户不小心选中就删了库 Prompt 只构造消息,Tool 才做操作
返回的 messages 超大 LLM 上下文爆炸 用嵌入资源 + 真正需要时再 read
用户需要记住参数顺序 体验差 用带 name 的参数对象,Client 渲染为表单

7.11 设计指南

在实际开发 MCP Server 时,设计 Prompt 应遵循以下原则:

1. Prompt 是用户意图的快捷方式,不是万能工具。 如果一个操作应该由模型自主决定是否执行,它应该是 Tool 而不是 Prompt。如果一个数据源应该自动附加到对话中,它应该是 Resource 而不是 Prompt。

2. 参数命名要直观。 用户在填写参数时需要理解每个参数的含义。codeinput_data 更好,languagelang_type 更好。description 字段不要省略。

3. 善用多轮消息结构。 assistant 角色的预设消息可以有效引导模型的行为模式。一条好的角色设定消息,往往比在 user 消息中反复强调"你是一个专家"更有效。

4. 考虑嵌入资源的时机。 当你的 Prompt 需要引用文件、数据库记录或其他结构化数据时,优先使用嵌入资源而非纯文本。嵌入资源让客户端能够更智能地处理和展示这些内容。

5. 校验参数。 服务器在处理 prompts/get 时应验证所有必填参数是否已提供,参数值是否合法。SDK 会帮你完成类型级别的校验,但业务级别的校验(比如表名是否存在)需要你自己处理。

6. 给 Prompt 起一个好记的 name。 记住,用户是要在斜杠菜单里看到这个名字并选择的。code_reviewcr 好(太短难懂),比 multi_dimensional_code_review_v2 好(太长)。把 Prompt name 当命令行工具命名对待

7.11.1 实测:两 SDK Prompt 实现的代码组织对比

§7.7.3 给出 SDK 对比表——把 Prompt 实现在两 SDK 里的代码组织实测——

TS SDK——整个 Prompt 功能塞在 packages/server/src/server/mcp.ts(1329 行)内部——

位置 行号 角色
setPromptRequestHandlers() 内部 mcp.ts:509-554 prompts/list + prompts/get 两个 handler 注册,由 _promptHandlersInitialized 做一次性守卫
registerPrompt() public method mcp.ts:922 起 注册逻辑 + Standard Schema 包装 + handler 工厂
TS SDK Prompt 总代码 估算 ~120 行 没有独立 prompts/ 目录

Python SDK——独立子目录 src/mcp/server/mcpserver/prompts/——

文件 角色
base.py 189 Prompt Pydantic 类 + 函数签名→arguments schema 的反射推导 + _convert_to_message 把 callback 返回值规整为 list[PromptMessage]
manager.py 59 PromptManager——add/list/get + warn_on_duplicate_prompts 选项
__init__.py 4 export
Python SDK Prompt 总代码 252

Plus 顶层 src/mcp/server/mcpserver/server.py 1112 行 里 @server.prompt() 装饰器实现(line 747)+ _prompt_manager.add_prompt 调用(line 745)+ prompts/get handler dispatcher(line 1100)——估算 ~50 行额外代码。

Python SDK Prompt 实现总规模约 302 行 vs TS SDK 约 120 行——Python 是 TS 的 2.5 倍——和 §15.9.4 实测的 OAuth (Python 客户端 vs TS 客户端 1.5x)、§16.9.1 实测的 OAuth Discovery (Python utils.py vs TS auth.ts 内联) 同款规律——Python 倾向把单一功能拆成 base + manager + 顶层装饰器分离、TS 倾向把所有内容塞在一个大 mcp.ts 里。

§7.7.3 对比表"参数定义"那一行的源码证据——

  • TS:registerPrompt<Args extends StandardSchemaWithJSON>(...)(mcp.ts:922)—— 泛型 + Standard Schema 显式声明
  • Python:Prompt.from_function(fn)(base.py 内部)通过 inspect.signature(fn) 反射函数参数自动推导——和《LangChain 设计与实现》 第 8 章 §8.4 LangChain @tool 装饰器从函数签名推断 schema 同款"Python 函数签名即接口"哲学

§7.7.3 对比表"补全(completion)"那一行——TS SDK 用 completable() 包装器(packages/server/src/server/completable.ts 74 行 + completable.examples.ts 46 行 = 120 行专门给参数补全);Python SDK 把补全元数据塞在 Prompt 类的 arguments 字段里——TS SDK 把补全单独抽成一个 74 行的小模块、Python SDK 内联——又一处"TS 多文件 vs Python 单一类" 风格反转例证(与之前几次 OAuth/Discovery 实测的 "Python 多小文件 vs TS 大单文件" 模式刚好相反)。

这条反转印证 §3.8.3/§16.9.1 的总结——MCP 两 SDK 的代码组织风格 "按功能模块各自决定"——不是某一边一贯多文件、某一边一贯大单文件。Streamable HTTP/Sampling/Prompt 是 Python 多文件 + TS 单文件,OAuth/Discovery 是 Python 多文件 + TS 单文件,但 completable 是 Python 内联 + TS 独立——证明风格选择是按具体功能复杂度而非全局信条

7.12 本章小结

Prompt 是 MCP 三大原语中最贴近用户的一个。它的设计看似简单——不过是预定义消息模板——但其背后蕴含了重要的设计哲学:将控制权交给最合适的主体

回顾本章的核心内容:

  • Prompt 属于用户控制平面,由用户主动选择和触发,与 Tool(模型控制)和 Resource(应用程序控制)形成三足鼎立
  • 一个 Prompt 由 namedescriptionarguments 定义,通过 prompts/get 返回 messages 数组
  • Messages 支持 textimageresource 三种内容类型,可以包含多条不同角色的消息
  • 动态参数让 Prompt 从静态模板变成可编程的消息工厂——服务器可以根据参数执行数据库查询、文件读取等操作
  • 嵌入资源将 Prompt 与 Resource 连接起来,让模板能够引用服务器管理的任意资源
  • TypeScript SDK 通过 registerPrompt() 方法注册,Python SDK 通过 @server.prompt() 装饰器注册

一句话记忆 Prompt 的本质:

Tool 是"LLM 的手",Resource 是"Client 的眼",Prompt 是"User 的嘴"——三者分工,方能有序。

物理事实:TS SDK Prompt 实现 ~120 行内联在 mcp.ts (1329 行) vs Python SDK 独立 prompts/ 目录 252 行(base.py 189 + manager.py 59)+ 顶层 mcpserver/server.py 50 行装饰器路由 = ~302 行——Python 2.5x;completable TS 独立 74 行模块 vs Python 内联——证明 SDK 风格选择按具体功能复杂度而非全局信条。