CodeGym /课程 /ChatGPT Apps /MCP 消息格式:requests, replies, notifications, tools/resource...

MCP 消息格式:requests, replies, notifications, tools/resources/prompts

ChatGPT Apps
第 6 级 , 课程 1
可用

1. MCP 与 JSON‑RPC:需要理解一次的“无聊”基础

在上一讲里我们讨论了为什么需要 MCP,以及它如何融入 Apps SDK 栈。这一讲我们把焦点缩小到最“无聊”的一层——MCP 消息格式,这样你就能自信地阅读原始 JSON 日志,明白 ChatGPT 到底发给你的服务器了什么、服务器又返回了什么。

MCP 使用 JSON‑RPC 2.0 作为数据传输:所有请求、响应和通知,都是具有可预期模式的普通 JSON 对象。

也就是说,不再是“每个服务自创一套格式”,而是有一个基础契约:

  • 请求必须包含字段 jsonrpc(通常为 "2.0")、唯一的 id、字符串方法名 method,以及包含参数的 params 对象;
  • 响应通过 id 与请求关联,并且只包含 resulterror 二者之一;
  • 通知(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/listtools/callresources/listprompts/list,…)以及它们使用什么格式来接收参数并返回数据。

要把握的一点是:JSON‑RPC 是“请求—响应—通知”的骨架;MCP 则规定了“具体有哪些请求以及它们的内容”。

2. Request:MCP 如何提出要执行的操作

从请求开始。请求总是朝“某人想做点什么”的方向。通常是客户端 → 服务器(ChatGPT → 你的 MCP 服务器),但 MCP 也允许反向请求,即服务器请求客户端做 sampling 或 elicitation。本讲我们主要关注经典情形:客户端请求服务器。

任何 MCP request 都有三个关键字段:

  1. jsonrpc —— JSON‑RPC 协议版本,通常 "2.0"
  2. id —— 请求标识;可以是任意 JSON 类型,但实践中多为数字或字符串。关键是对活动请求来说 id 必须唯一。
  3. 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 如何响应——resulterror

响应(reply)总是通过 id 与请求关联。这就像许多分布式系统中的 correlationId:你看日志,发现 id=7 的请求收到了 id=7 的响应,那就是一对。

JSON‑RPC 规定了一个简单规则:响应中要么result要么error两者不可并存。在此之上,MCP 进一步细化了不同方法(tools/listtools/call 等)的 result 结构,并推荐错误码。

成功回复(result

来看我们 suggest_giftstools/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
  }
}

这里有几点很重要。

  • 首先,contentstructuredContent 是你在 Apps SDK 中已经见过的 MCP tools 响应部分。模型会使用 content 中的文本,而你的组件会把 structuredContent 中的数据以更友好的方式渲染出来。
  • 其次,isError 标志属于业务结果。从协议角度看一切成功:JSON 合法、方法存在、参数可解析。但业务逻辑可能认为“我没有找到任何礼物创意,从 UX 角度算作错误”。那么你就设置 isError: true,并在 content 中描述问题。
  • 第三,MCP 规范对不同方法(tools/listtools/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 有两个主要过程:

  1. discovery —— 客户端发现有哪些工具;
  2. invocation —— 客户端调用具体工具。

之前我们已经略看过 tools/listtools/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 里返回 contentstructuredContent,以及可选的 _meta(例如指明 openai/outputTemplate,以便把该工具和某个特定小部件关联起来)。

tools/listtools/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 层面,这是一个便捷函数,它会:

  1. 知道 MCP 服务器的 URL(来自应用配置);
  2. 能通过名称 suggest_gifts 找到工具描述;
  3. 把你的调用打包成 MCP request tools/call
  4. 通过所选传输(HTTP/SSE)发送;
  5. 等待 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、里面的参数是什么、resulterror 返回了什么。

第二,在设计工具与资源时,不仅能用 TypeScript 类型来思考,还能用 MCP 模式来思考:它在 JSON 中会是什么样子?对其他客户端(比如也能连到你 MCP 服务器的 agent)是否友好?

7. 小练习:阅读并“修理” MCP‑JSON

要让 MCP 格式真正“变成自己的”,最好亲手把几个消息剖开看看。拿一个完整对话 tools/listtools/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 规范,就会发现 toolargs 字段名并不符合预期的 namearguments

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/calltools/list,MCP 消息格式就不再是“魔法”,而会成为日常工程实践。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION