MCP 协议设计与实现

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

作者 杨艺韬 · 7,440 字

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

“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
原语控制者类比典型交互安全信任级别
ToolAI 模型函数调用模型判断需要查询数据库,自动调用 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 用途
namePrompt 的唯一标识符斜杠命令名(如 /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.pyserver.py:747):

1. 装饰器括号强校验——@server.prompt() 的括号不能省略。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. 返回类型的四种多态——base.py:55 给出了返回值的类型别名:

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

render 函数(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. 同步函数自动进线程池——如果装饰的函数不是 asyncbase.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 参数自动注入——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——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 取——base.py:131description or fn.__doc__ or "")、icons。docstring 作为 description 的默认来源是 Python SDK 一个很 Pythonic 的约定——你写函数文档字符串的时候就已经在给 MCP 客户端写说明。

7.7.3 两种 SDK 的对比

维度TypeScript SDKPython 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,或引入”高级设置”
硬编码敏感信息到 PromptToken 泄露到用户/日志通过 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 行)内部——

位置行号角色
setupPromptHandlers() 内部mcp.ts:514-555prompts/list + prompts/get 两个 handler 注册
registerPrompt() public methodmcp.ts:922 起注册逻辑 + Standard Schema 包装 + handler 工厂
TS SDK Prompt 总代码估算 ~120 行没有独立 prompts/ 目录

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

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

Plus 顶层 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) 反射函数参数自动推导——和 ch08 §8.4.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 风格选择按具体功能复杂度而非全局信条。