1. MCP 与 JSON‑RPC:需要理解一次的“无聊”基础
在上一讲里我们讨论了为什么需要 MCP,以及它如何融入 Apps SDK 栈。这一讲我们把焦点缩小到最“无聊”的一层——MCP 消息格式,这样你就能自信地阅读原始 JSON 日志,明白 ChatGPT 到底发给你的服务器了什么、服务器又返回了什么。
MCP 使用 JSON‑RPC 2.0 作为数据传输:所有请求、响应和通知,都是具有可预期模式的普通 JSON 对象。
也就是说,不再是“每个服务自创一套格式”,而是有一个基础契约:
- 请求必须包含字段 jsonrpc(通常为 "2.0")、唯一的 id、字符串方法名 method,以及包含参数的 params 对象;
- 响应通过 id 与请求关联,并且只包含 result 或 error 二者之一;
- 通知(notifications)与请求类似,但没有 id,也不期望收到响应。
大致如下:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/list",
"params": {
"cursor": null
}
}
这是一个 request。成功时的回复:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"tools": [],
"nextCursor": null
}
}
如果你想到“这不就是普通的 RPC 吗”,没错。MCP 只是进一步固定了有哪些具体方法(tools/list、tools/call、resources/list、prompts/list,…)以及它们使用什么格式来接收参数并返回数据。
要把握的一点是:JSON‑RPC 是“请求—响应—通知”的骨架;MCP 则规定了“具体有哪些请求以及它们的内容”。
2. Request:MCP 如何提出要执行的操作
从请求开始。请求总是朝“某人想做点什么”的方向。通常是客户端 → 服务器(ChatGPT → 你的 MCP 服务器),但 MCP 也允许反向请求,即服务器请求客户端做 sampling 或 elicitation。本讲我们主要关注经典情形:客户端请求服务器。
任何 MCP request 都有三个关键字段:
- jsonrpc —— JSON‑RPC 协议版本,通常 "2.0"。
- id —— 请求标识;可以是任意 JSON 类型,但实践中多为数字或字符串。关键是对活动请求来说 id 必须唯一。
- method —— 形如 "tools/list" 或 "tools/call" 的字符串。MCP 规定了一组允许的方法。
还有一个 params 对象,具体方法的参数都放在这里。
示例:请求工具列表
假设 ChatGPT 刚连接到你的 MCP 服务器,想知道它可以调用哪些 tools。它会发送大致如下的请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": null
}
}
字段 cursor 用于分页——如果工具很多,服务器可以分批返回。
对于我们的教学应用(挑选礼物)这里暂时会比较无聊:就一两个工具,但协议不变。先把它当作直观示例;更正式的结构我们会在 tools 章节再看。
示例:调用工具(tools/call)
现在稍微有意思一点。假设我们已有 MCP tool suggest_gifts,你会在讲 MCP 服务器时实现它。它期望的参数有:
- occasion —— 场合(Birthday、Wedding 等),
- budget —— 美元金额,
- recipient —— 字符串,描述收礼人。
当 ChatGPT 决定使用这个工具时,会构造如下 MCP 请求:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "birthday",
"budget": 100,
"recipient": "friend who loves board games"
}
}
}
注意以下几点。
首先,工具名称来自你在服务器端的声明(server.registerTool("suggest_gifts", …))。其次,arguments 对象必须符合你在工具描述中提供的 JSON Schema。
如果 GPT 试图发送不符合模式的参数(例如,budget: "一百美元"),服务器可以根据实现选择在协议层或业务逻辑层返回错误。此时只需抓住这种请求的一般形态;在下文工具章节中我们会更系统地看这些消息。
资源与提示的 Requests
对资源与提示的请求也类似。MCP 规范定义了这些方法:
- resources/list —— 枚举可用资源;
- resources/read(或 resources/get)—— 通过 URI 读取具体资源;
- prompts/list —— 获取可用提示列表;
- prompts/get —— 获取某个提示的内容。
读取包含礼物目录的资源的请求示例:
{
"jsonrpc": "2.0",
"id": 15,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
先记住两点。其一,每个原语都有 */list 与 */get/*/read 方法。其二,方法名总是在字符串字段 method 中,所有内容都在 params 对象里。
3. Reply:MCP 如何响应——result 与 error
响应(reply)总是通过 id 与请求关联。这就像许多分布式系统中的 correlationId:你看日志,发现 id=7 的请求收到了 id=7 的响应,那就是一对。
JSON‑RPC 规定了一个简单规则:响应中要么是 result,要么是 error,两者不可并存。在此之上,MCP 进一步细化了不同方法(tools/list、tools/call 等)的 result 结构,并推荐错误码。
成功回复(result)
来看我们 suggest_gifts 的 tools/call 成功回复示例。服务器完成处理,找到了合适的礼物,并在 result 字段返回列表:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Here are some gift ideas for your friend..."
}
],
"structuredContent": {
"gifts": [
{ "name": "Board game: Catan", "price": 45 },
{ "name": "Dice set", "price": 20 }
]
},
"isError": false
}
}
这里有几点很重要。
- 首先,content 与 structuredContent 是你在 Apps SDK 中已经见过的 MCP tools 响应部分。模型会使用 content 中的文本,而你的组件会把 structuredContent 中的数据以更友好的方式渲染出来。
- 其次,isError 标志属于业务结果。从协议角度看一切成功:JSON 合法、方法存在、参数可解析。但业务逻辑可能认为“我没有找到任何礼物创意,从 UX 角度算作错误”。那么你就设置 isError: true,并在 content 中描述问题。
- 第三,MCP 规范对不同方法(tools/list、tools/call、*/list、*/get)详细描述了 result 中应包含的字段。比如对于 tools/list,服务器应返回工具描述数组,其中包含名称、标题、描述和输入参数的 JSON Schema。
错误回复(error)
如果在协议或服务器层面出了问题,就会返回 error 对象而不是 result。它通常包含:
- code —— 数字错误码;
- message —— 人类可读描述;
- data —— 可选的附加数据(堆栈、详情等)。
示例:模型调用了不存在的方法:
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: tools/col"
}
}
-32601 是 JSON‑RPC 中经典的 “method not found”。
有一条细微但非常重要的界线:
协议错误——违反 MCP/JSON‑RPC 规则时:未知方法、params 字段类型不正确、非法 JSON。此时应当在顶层返回 error。
业务错误——协议符合,但操作因领域原因失败:目录为空、没有访问某资源的权限、无效的业务标识等。此时 MCP 通常建议返回合法的 result,但标记为 isError: true,并在内容中描述问题。
这种区分对 ChatGPT 与调试工具很有帮助:你一眼就能看出是技术性故障,还是业务逻辑有意识的拒绝。
4. Notifications:单向消息
通知(notification)是“不等待响应的信件”。在 JSON‑RPC 中,通知看起来像普通请求,但没有 id 字段。客户端不应该对其发送 reply。
在 MCP 中,通知用于各种事件:tools/resources/prompts 列表变更、长任务进度、日志消息等。
你几乎一定会遇到的最简单示例——工具列表发生变化的通知。MCP tools 规范描述了 capability listChanged 以及通知 tools/list_changed:当可用工具集合发生变化时,服务器会发送它。
通知可能是这样:
{
"jsonrpc": "2.0",
"method": "tools/list_changed",
"params": {
"reason": "New tool 'suggest_gift_cards' was added"
}
}
不需要对它作出响应。客户端收到这样的通知后,可能会决定:“好的,再调用一次 tools/list 来更新本地工具缓存。”
其他常见的 MCP 通知(我们会在“流与事件”模块详细讲):
- 长任务进度事件(notifications/progress);
- 服务器日志(notifications/logging/message);
- 资源(resources/list_changed)和提示(prompts/list_changed)的变更。
现在只需记住一点:通知 = 没有 id 的请求,且不期望响应。如果你在日志中看到没有 id 的 JSON,那很可能就是 notification。
洞见
经实测,ChatGPT App 会忽略发给它的 MCP 通知消息。不过,鉴于 ChatGPT Apps 尚处于早期阶段,近期完全支持 MCP 协议各侧面的可能性很大。因此仍建议你学习 MCP 协议的这一部分。
5. tools/resources/prompts 在消息中的样子
下面进入“重头戏”:MCP 消息里,那些我们常提到的 tools、resources 与 prompts 到底是怎样描述的。
Tools:描述与调用
在协议层面,tools 有两个主要过程:
- discovery —— 客户端发现有哪些工具;
- invocation —— 客户端调用具体工具。
之前我们已经略看过 tools/list 与 tools/call。现在更系统地看:它们覆盖了哪些过程,result 里会返回什么。
5.1.1. 工具列表 —— tools/list
我们已经看过 tools/list 的 request。看看响应结构。MCP 规范说明:result.tools 中应返回一个对象数组,每个对象描述一个工具。每个工具必须包含:
- name —— 唯一名称,后续用它来调用 tools/call;
- title —— 简短标题(人和模型都会看到);
- description —— 更详细的工具说明,就像你向同事解释它的功能;
- inputSchema —— 工具参数的 JSON Schema。
以我们的 suggest_gifts 为例,tools/list 的响应可能(大幅简化)是这样:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas for a given occasion and budget.",
"inputSchema": {
"type": "object",
"properties": {
"occasion": { "type": "string" },
"budget": { "type": "number" },
"recipient": { "type": "string" }
},
"required": ["occasion", "budget"]
}
}
],
"nextCursor": null
}
}
如果你已经在 Apps SDK 中为注册工具写过 inputSchema,你实际上已经见过这个对象,只不过是“从上层”——以 TypeScript 对象的形式。MCP 只是把它通过协议发给客户端。
5.1.2. 调用工具 —— tools/call
我们已经提到调用格式。MCP 规范说明 params 必须包含:
- name —— 工具名称;
- arguments —— 符合 inputSchema 的对象。
例如:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "wedding",
"budget": 150,
"recipient": "coworker from marketing"
}
}
}
而响应中,服务器在 result 里返回 content、structuredContent,以及可选的 _meta(例如指明 openai/outputTemplate,以便把该工具和某个特定小部件关联起来)。
tools/list → tools/call 这一组合,就是 MCP tools 的基础工作循环:先发现,再使用。
Resources:可寻址的数据
在 MCP 中,Resources 是客户端可以通过 URI 访问的任何数据:文件、数据库记录、配置、目录等。
它们有一套标准操作:
- resources/list —— 查询可用的资源;
- resources/read —— 读取某个具体资源(或其片段)。
设想一个资源 gift_catalog,描述礼物的基础目录:类别、品牌、最小/最大价格。服务器可以把它声明为 URI "mcp://gift-server/resources/gift_catalog"。
resources/list 的响应(简化)可能是:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "mcp://gift-server/resources/gift_catalog", // 只是一个唯一的字符串。mcp 不是一个协议。
"name": "gift_catalog",
"description": "Base catalog of gifts with categories and prices",
"mimeType": "application/json"
}
],
"nextCursor": null
}
}
而读取资源 —— resources/read:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
响应可包含内容本身与元数据:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"contents": [
{
"uri": "mcp://gift-server/resources/gift_catalog",
"mimeType": "application/json",
"text": "{\"categories\":[\"boardgames\",\"books\"]}"
}
]
}
}
核心思想是:资源是可寻址的数据,而 tools 是操作。MCP 在协议中把两者都显式化了。
Prompts:可复用的模板
Prompts 是“预制的提示词”或模板,服务器可以提供给客户端。MCP 将其视为一种原语,具有:
- 名称;
- 人类可读的标题/描述;
- 内容(常见为 system 提示模板或 few‑shot 示例集合)。
照例,有两个方法:
- prompts/list —— 查询有哪些提示;
- prompts/get —— 获取某个提示的内容。
例如,你希望为生成附带礼物的祝贺消息设定一个特殊风格。那就可以在 MCP 服务器里声明一个 prompt:gift_congrats_style。
prompts/list 的响应可能是:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"prompts": [
{
"name": "gift_congrats_style",
"description": "Style guide for birthday congratulations in a friendly tone"
}
]
}
}
而 prompts/get 会返回文本(或结构化内容)本身,客户端随后可以把它作为 system 提示的一部分传给 LLM。请求与响应示例:
{
"jsonrpc": "2.0",
"id": 11,
"method": "prompts/get",
"params": {
"name": "gift_congrats_style"
}
}
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"prompt": {
"name": "gift_congrats_style",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a friendly assistant that writes short, warm birthday congratulations..."
}
]
}
]
}
}
}
6. 这与 Apps SDK 和我们的组件有何关联
现在 MCP‑JSON 也许看上去仍有点“笨重”。我们把它与你在 Apps SDK 中已经做过的事情联系起来看看。
回顾一下,在小部件前端你可能写过这样的代码:
// 在 ChatGPT 沙箱中的 React 组件内
async function fetchGifts() {
const result = await window.openai.callTool("suggest_gifts", {
occasion: "birthday",
budget: 50,
recipient: "friend who loves sci-fi"
});
console.log(result);
}
在 Apps SDK 层面,这是一个便捷函数,它会:
- 知道 MCP 服务器的 URL(来自应用配置);
- 能通过名称 suggest_gifts 找到工具描述;
- 把你的调用打包成 MCP request tools/call;
- 通过所选传输(HTTP/SSE)发送;
- 等待 MCP reply,解包 result,并以 JavaScript 中的 result 返回给你。
如果画成时序图,大致如下:
sequenceDiagram
participant Widget
participant AppsSDK as Apps SDK
participant MCP as MCP 服务器
Widget->>AppsSDK: window.openai.callTool("suggest_gifts", {...})
AppsSDK->>MCP: JSON { id:7, method:"tools/call", params:{...} }
MCP-->>AppsSDK: JSON { id:7, result:{ content, structuredContent } }
AppsSDK-->>Widget: result (ToolOutput)
Widget->>Widget: setState(toolOutput)
理解 MCP 格式能带来两项很棒的能力。
第一,你可以正确地阅读原始 MCP 日志(例如 MCP Inspector,我们会单讲一节),看清到底发出了哪个 tools/call、里面的参数是什么、result 或 error 返回了什么。
第二,在设计工具与资源时,不仅能用 TypeScript 类型来思考,还能用 MCP 模式来思考:它在 JSON 中会是什么样子?对其他客户端(比如也能连到你 MCP 服务器的 agent)是否友好?
7. 小练习:阅读并“修理” MCP‑JSON
要让 MCP 格式真正“变成自己的”,最好亲手把几个消息剖开看看。拿一个完整对话 tools/list → tools/call → 结果 为例。
客户端想要工具列表
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
我们看到:
- 这是一个 request(有 id);
- 方法是 tools/list,也就是查询工具(discovery);
- 参数为空,无分页。
服务器回复:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas",
"inputSchema": { "type": "object", "properties": { "occasion": { "type": "string" } } }
}
]
}
}
可以立刻看出这是对同一请求的响应(相同的 id: 1),协议成功(有 result,没有 error),客户端也就知道存在 tool suggest_gifts。
客户端调用工具
接着客户端执行 tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "anniversary"
}
}
}
如果服务器还期望 budget,但模型没有提供,服务器可以:
- 返回协议错误(比如带 “invalid params” 的顶层 error);
- 或采取默认决策(比如使用中位预算),返回正常的 result。
用我们上面的术语来说,第一种是协议错误(顶层 error);第二种属于业务逻辑:你依然返回合法的 result,并决定是否把它视为业务错误(isError: true),还是正常行为。
参数错误时的响应可能是:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Missing required property 'budget' in arguments"
}
}
再次强调要把它和业务错误区分开:这里违反了模式(参数不符合 schema),因此使用 error 是合适的。
有问题的示例:找出 bug
下面这个 JSON,初学者有时会写成这样:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"tool": "suggest_gifts",
"args": {
"occasion": "birthday",
"budget": 100
}
}
}
乍一看似乎合理,但如果你对照 MCP 规范,就会发现 tool 与 args 字段名并不符合预期的 name 与 arguments。
MCP‑SDK 的客户端/服务器大概率不会生成这样的 JSON,但如果你在不了解规范的情况下手工集成,就有可能踩到类似的坑。这也正是我们在课程中“裸讲”协议而不是只讲 SDK 封装的原因。
8. 使用 MCP 消息时的常见错误
错误 1:混淆协议错误与业务错误。
很多开发者习惯把一切“出了问题”的情况都包成顶层 error——既包括资源不存在、也包括参数错误、甚至数据库崩溃。在 MCP 里,分清楚很有用:如果是 JSON 结构与调用模式被破坏(错误的方法、字段、类型),这才是该返回 error 的场景。如果工具只是没能完成领域操作(该预算下没有礼物、找不到用户),更好的做法是返回合法的 result,并设置 isError: true,在 content 中给出清晰信息。这样模型与调试器就能正确地区分“传输通道坏了”和“业务逻辑有意识地拒绝”。
错误 2:忽略 id 字段与请求关联。
有时你会在 MCP 服务器日志里看到手写输出缺少 id,或者在多个活动请求上复用相同的 id。在单线程 hello‑world 中也许还能“活”,但一旦出现并行调用或重试,就很难分辨哪个响应对应哪个请求。JSON‑RPC 要求在请求生命周期内保持 id 唯一,MCP 也依赖这一规则。如果你使用官方 SDK,无需操心 id;但一旦你自己写传输或日志,请记得保存并输出 id——这是你排查奇怪问题时的第一抓手。
错误 3:对同一方法返回不稳定的 result 结构。
有时会忍不住“稍微”改变响应格式:有时返回礼物数组,有时返回只有一行文本的对象,有时只给 text 而没有 structuredContent。模型也许还能“适应”,但你的组件与其他 MCP 客户端很可能不行。MCP 规范为每个方法描述了可预期的 result 结构;尽量遵守它。如果确实需要不同格式,最好新增独立的工具或版本,而不是在线修改 schema。
错误 4:params 中字段冗余或缺失。
自定义实现的典型问题——在 params 中添加 MCP 不期望的字段,或者漏掉必填字段。比如在 tools/call 里发送 toolName 而不是 name,或在 resources/read 里发送 resourceId 而不是 uri。MCP‑SDK 通常会校验并抛出清晰异常,但如果你更接近协议层工作,可能会为“为什么服务器不理解我”而困惑很久。一个好习惯是:在处理器旁边放一份来自规范或工作中客户端日志的正确 JSON 请求示例,并用它对照你发送的内容。
错误 5:把 notifications 当作“第二条响应通道”。
有的开发者看到 notifications,就开始通过通知发送操作结果,而不是用普通的 replies:“反正已经在 MCP 里、有 SSE 了,我们都用通知推吧”。问题在于,JSON‑RPC 通知按定义并不绑定具体的 id,客户端也不会把它当作某个请求的响应。结果是难以调试,也无法知道某条消息属于哪个工具调用。通知非常适合用于事件(tools/resources/prompts 列表变更、新的进度、日志到来),但不适合用于 tools/call 等常规请求的回复。
错误 6:不看 MCP 日志与检查器。
最“人性化”的错误——只通过 ChatGPT UI 来调试集成:“点了按钮,没来东西,以后再说吧”。在你看不到原始 MCP 消息(requests、replies、notifications)之前,很难判断问题在哪一层:是模型没调用 tool,还是 Apps SDK 没到 MCP 服务器,抑或服务器返回了错误的 JSON,又或者是在组件渲染时才出的问题。MCP Inspector/Jam 与结构化 MCP 日志是你最好的朋友。一旦你在日志中亲眼看到一次 tools/call 和 tools/list,MCP 消息格式就不再是“魔法”,而会成为日常工程实践。
GO TO FULL VERSION