MCP Events 让你的 MCP server 在更新一发生时就推送给 ChatGPT,无需任何 agent 轮询。自 2026 年 9 月 29 日 OpenAI DevDay 起,ChatGPT 在所有套餐上支持该提案中的 MCP Events 规范,协议版本为 2026-07-28(MCP 2.0)。你的 server 需要新增三个 method(events/list、events/subscribe、events/unsubscribe),声明 events(在 server/discover 中),通过回调验证 challenge,并用 Standard Webhooks HMAC 为每次 delivery 签名。ChatGPT 只接受 webhook 方式投递。
本指南覆盖 OpenAI 文档中记录的每一种消息结构、你的 server 需要实施的安全规则,以及针对整个闭环的一轮测试。刚接触该协议?先从「什么是 MCP」看起。你可以在 Apifox 中发送这些 JSON-RPC 调用并 mock 回调。
MCP Events 一览
| 项目 | ChatGPT 的要求 |
|---|---|
| 协议 | MCP 2.0,版本 2026-07-28 |
| 能力 | 在 capabilities 中声明 "events": {}(server/discover) |
| Method | events/list、events/subscribe、events/unsubscribe,与你的 tools 使用同一个已鉴权的接口 |
| 投递方式 | 仅支持 webhook:不支持 polling、streaming、gap 或 terminated 通知 |
| 签名 | Standard Webhooks HMAC-SHA256 |
| Header | webhook-id、webhook-timestamp、webhook-signature、X-MCP-Subscription-Id |
| 密钥 | 由 ChatGPT 提供,以 whsec_ 开头、base64 解码后为 24-64 字节 |
| 载荷上限 | 256 KiB(262,144 字节),每个请求一个 event |
| 订阅 ID | 确定性生成:principal、回调 URL、event 名称、arguments |
| 回调 | HTTPS、需通过 challenge 验证、不允许私有地址、不跟随重定向 |
来源:OpenAI 的 MCP Events 指南 以及草案 MCP Events 设计草案。
为什么用 event 取代 polling
没有 event 时,一个盯着评审评论的 agent 只能定时调用 tool 并对比结果:什么都没变时白白消耗请求,真有变化时又滞后。MCP Events 把方向反过来:你的 server 知道记录何时变化,于是主动把更新发出去。这里的取舍就是经典的 webhooks 与 polling 之争,如今被搬进了 MCP。
OpenAI 的 DevDay 回顾用一块项目看板举例:用户让 ChatGPT 盯住新任务,一旦有任务进来,ChatGPT 就阅读关联文档并起草一份计划。文档又补充了两个场景:把某个频道里的 bug 报告变成草稿 pull request(message.created,按 channel_id 过滤),以及把评审评论应用到某份文档上(comment.created,按 document_id 过滤)。
该规范来自 MCP 的 Triggers and Events 工作组(repo),并被标记为实验性,因此请把你的代码固定到 2026-07-28。想了解本次发布的其他内容,请看 DevDay 2026 汇总。
声明并定义 event
把 events 加入 server/discover 响应的 capabilities 中:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "events": {} }
}
}
然后从 events/list 返回各 event 的定义。每个定义包含 name、description、delivery: ["webhook"]、用于订阅 arguments 的 inputSchema(即你的过滤器,例如 document_id),以及 payloadSchema,用于每次 delivery 中的 data 对象。请使用稳定的名称和具体的描述,在你的 server 上应用过滤器,并且只列出当前已连接账号有权看到的 event。
events/subscribe:请求与响应
当用户让 ChatGPT 监控某项内容时,ChatGPT 会调用 events/subscribe:
{
"jsonrpc": "2.0",
"id": 2,
"method": "events/subscribe",
"params": {
"name": "comment.created",
"arguments": { "document_id": "doc_123" },
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123",
"secret": "whsec_<base64-encoded-signing-key>"
},
"cursor": null
}
}
在接受之前,先为该用户授予该 event 及其 arguments 的权限,对照你的定义校验二者,要求 whsec_ 密钥的 base64 解码后为 24-64 字节,验证回调(见下一节),并保存该订阅及其所有者、过滤器、URL、密钥和过期时间。然后返回:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
有三条规则保证订阅的正确性:
- 确定性 ID。根据已鉴权的 principal、回调 URL、event 名称和 arguments 推导出
id。设计草案建议对该键取截断的 SHA-256。 - 幂等 upsert。身份相同的重复订阅会更新已有记录。比较 arguments 时使用规范化 JSON,避免键顺序造成重复记录。
- 在
refreshBefore之前刷新。ChatGPT 会在该时间之前以相同身份和它上次保存的 cursor 重新调用events/subscribe;你返回新的过期时间。若刷新携带了新的密钥,请替换它,并在一小段窗口期内用新旧两把密钥同时签名。对于无法重放的 event,返回cursor: null。
在发送任何数据之前先验证回调
在发送应用数据之前,先 POST 一个已签名的验证 body,其中带有一个全新的、一次性的、短时效的 challenge:
{ "type": "verification", "challenge": "a-single-use-random-value" }
为它设置唯一的 webhook-id(例如 msg_verification_123)以及与该次 delivery 相同的签名 header。ChatGPT 会回显 {"challenge": "a-single-use-random-value"},并返回 2xx。用常量时间比较,然后启用投递。失败时返回 JSON-RPC 错误 -32015(CallbackEndpointError),并附上 data.reason,例如 challenge_failed 或 timeout。按 principal 和 URL 在一段有限时间内缓存验证通过的结果,这样刷新时就不必重新发起 challenge。
引入 challenge 是因为密钥由订阅方提供:如果没有它,任何人都可能把你的 server 指向受害者的 URL 并对其发起洪泛攻击。对每个出站请求都要施加同样的加固措施:仅使用 HTTPS,在建立连接时解析并校验地址,阻断私有、本地及其他非公网地址,并且绝不跟随重定向。
投递并签名 event
当发生匹配的 event 时,向回调 URL POST 一个 event 对象:
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Can we add the rollout dates to this section?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
发送 Content-Type: application/json、webhook-id(与 eventId 相同)、webhook-timestamp(签名时的 Unix 秒数)、webhook-signature 和 X-MCP-Subscription-Id。body 只序列化一次,并发送这些精确的字节,因为签名覆盖的就是它们。
保持在 256 KiB 以内;对于较大的记录,只发送摘要并另外暴露一个读取 tool。把用户生成的文本当作数据,绝不要把模型指令放进载荷。对瞬时失败使用带上限的指数退避重试,保持 event ID 不变,并让每次尝试都重新签名。不要重试 410 或 413。event 可能乱序到达,因此请让写入类 tool 具备幂等性。可靠 webhook 设计指南介绍了重试时间表。
验证 Standard Webhooks 签名
按照 Standard Webhooks 规范,被签名的内容是 ${webhook-id}.${webhook-timestamp}.${body},HMAC-SHA256 密钥是将 whsec_ 去掉后对密钥做 base64 解码得到的结果,而 header 中保存一个或多个以空格分隔的 v1,<base64> 签名。MCP 草案要求接收方拒绝超过 5 分钟的时间戳,并按 webhook-id 去重。
ChatGPT 会在自己那侧做验证,但一个你自己实现的严格接收方能成为很好的本地测试基准。下面这个只使用 Node 内置模块:
// receiver.mjs: strict Standard Webhooks receiver for local tests (Node 18+)
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const MAX_BYTES = 256 * 1024;
const TOLERANCE_S = 5 * 60;
const seen = new Set();
export function verify(raw, h, secret, now = Math.floor(Date.now() / 1000)) {
const id = h["webhook-id"], ts = h["webhook-timestamp"], sigs = h["webhook-signature"];
if (!id || !ts || !sigs) return false;
const t = Number(ts);
if (!Number.isInteger(t) || Math.abs(now - t) > TOLERANCE_S) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.`).update(raw).digest();
return sigs.split(" ").some((s) => {
const [version, b64] = s.split(",");
const got = Buffer.from(b64 ?? "", "base64");
return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
});
}
if (SECRET) createServer((req, res) => {
const chunks = [];
let size = 0;
req.on("data", (c) => { size += c.length; if (size <= MAX_BYTES) chunks.push(c); });
req.on("end", () => {
if (size > MAX_BYTES) return res.writeHead(413).end();
const raw = Buffer.concat(chunks);
if (!verify(raw, req.headers, SECRET)) return res.writeHead(401).end();
let body;
try { body = JSON.parse(raw); } catch { return res.writeHead(400).end(); }
if (body.type === "verification") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ challenge: body.challenge }));
}
const id = req.headers["webhook-id"];
if (!seen.has(id)) { seen.add(id); console.log(req.headers["x-mcp-subscription-id"], body.name, id); }
res.writeHead(200).end();
});
}).listen(8787);
用 WEBHOOK_SECRET=whsec_... node receiver.mjs 运行它。超过 256 KiB 时它返回 413,签名错误或过期时返回 401,会回显 challenge,并且每个 webhook-id 只记录一次。我们已用 Standard Webhooks JavaScript 库中的签名测试向量校验过 verify()。原理部分可参阅 webhook 签名验证。
你的 server 会阻断私有地址,因此默认不会向 localhost 投递。草案仅在显式配置时才允许非公网目标:添加一条仅开发环境使用的白名单条目,或通过隧道暴露接收方。
在 ChatGPT 之前先自测
下面是一轮 Apifox 测试,覆盖 OpenAI 列出的各种失败模式。创建一个包含 MCP_URL、MCP_TOKEN、CALLBACK_URL 和 WEBHOOK_SECRET 的环境,把每个 JSON-RPC 调用以 POST 发送到 {{MCP_URL}},带上 Authorization: Bearer {{MCP_TOKEN}},以及 Streamable HTTP 绑定 要求的 header:Accept: application/json, text/event-stream、MCP-Protocol-Version: 2026-07-28,并将 Mcp-Method 设为对应的 method。每个 body 还需要在 params._meta 中带上 io.modelcontextprotocol/protocolVersion 和 io.modelcontextprotocol/clientCapabilities。OpenAI 的示例省略了它,而缺少 _meta 同样会返回 -32602,那样第 3 步就会因为错误的原因而通过。
- 发现。发送
server/discover,断言$.result.capabilities.events存在且$.result.supportedVersions包含2026-07-28。发送events/list,断言每个 event 的delivery包含webhook。 - 幂等订阅。用
{{CALLBACK_URL}}和{{WEBHOOK_SECRET}}发送上面的订阅 body,并配置一个「提取变量」后置操作,把$.result.id保存为SUB_ID。原样重发一次,再把arguments的键重排后重发一次,两次都断言$.result.id等于{{SUB_ID}}。 - 校验。发送一个解码后不足 24 字节的密钥,然后发送一个
http://回调。草案把这两种情况都映射为-32602(InvalidParams);断言$.error.code。私有 IP 的回调也必须失败。 - 验证 challenge。在 Apifox 中创建一个 mock 接口,返回
{"challenge": "wrong"},并用它的云端 mock URL 作为回调发起订阅。断言$.error.code为-32015、$.error.data.reason为challenge_failed。然后把CALLBACK_URL指向你自己的接收方,确认订阅成功。 - 超大载荷。触发一个 body 超过 262,144 字节的 event。你的发送方应当拒绝它;如果有一个漏了过去,接收方会返回
413,而你的 server 日志应显示只有一次尝试,而不是重试。 - 重放与篡改。从你 server 的出站日志中复制一次已签名 delivery 的 header 和 body,放进一个新的 Apifox 请求。立刻重发:
200,且没有第二条日志。改动 body 的一个字节:401。5 分钟后再重发:401。
把这些步骤保存为测试场景,并用 Apifox CLI 在 CI 中运行。MCP server 测试手册介绍了 tool 调用,如何测试 webhook 则更深入地讲解了接收方。之后通过 plugin 把 server 接入 ChatGPT,并运行 OpenAI 的生命周期检查清单。
FAQ
什么是 MCP Events?一种实验性的 MCP 扩展,让 server 主动向 client 推送 event 通知,而不必由 client 轮询。ChatGPT 在 2026-07-28 上支持其 webhook 模式。
ChatGPT 支持 MCP Events 的 polling 或 streaming 吗?不支持。ChatGPT 仅支持 webhook 投递和回调验证。不支持 polling、streaming,也不支持 gap 和 terminated 通知。
哪些 ChatGPT 套餐可以使用 MCP Events?OpenAI 的 DevDay 回顾称该功能对所有套餐开放。
签名密钥由谁生成?由订阅方生成。ChatGPT 会发送 whsec_ 密钥,放在 delivery.secret 中;你的 server 负责校验、保存并用它签名,绝不自行生成。
它与 Agents API 有何不同?MCP Events 把数据从你的 server 推入 ChatGPT。OpenAI Agents API 则运行你构建的 agent,并通过 streaming 或 webhooks 汇报它们的进度。
下一步
先给一个 server 加上 "events": {},用一个过滤器暴露一个 event,并在接入 ChatGPT 之前用本地接收方跑完这六项检查。把这些检查保存为测试场景,让它随每次提交运行。
HiFox:将 Agent 变成真正的队友
另外,我们也在思考,AI 如何从个人提效走进团队协作。
HiFox 是一个让人和 AI Agent 在同一个工作现场协作的平台:你可以像给同事分派任务一样指派 Agent,在任务看板中跟踪进度、查看结果,让 Agent 成为团队里的队友。
👉 立即体验 HiFox:https://hifox.com
AI Coding 交流群
如果你也在用 AI 写代码,或者正在研究 Cursor、Claude Code 这些工具,
欢迎加入以下交流群。群里平时会聊一些 AI 编程的实际用法、开发工作流,还有各种新工具和新玩法。