กิจกรรม MCP คืออะไร: สร้างและทดสอบเซิร์ฟเวอร์ MCP ส่ง Webhook ให้ ChatGPT สมัครรับ

MCP Events อธิบาย: วิธีที่ ChatGPT สมัครรับข้อมูลจากเซิร์ฟเวอร์ MCP ของคุณด้วย events/subscribe, การลงนาม Webhooks มาตรฐาน, ข้อจำกัด 256 KiB และการทดสอบใน Apidog

Medy Evrard

2 October 2026

กิจกรรม MCP คืออะไร: สร้างและทดสอบเซิร์ฟเวอร์ MCP ส่ง Webhook ให้ ChatGPT สมัครรับ

Apidog สำหรับองค์กร

การติดตั้งแบบ On-Premises

SSO & RBAC

รองรับมาตรฐาน SOC 2

สำรวจ Apidog Enterprise

MCP Events ช่วยให้เซิร์ฟเวอร์ MCP ของคุณสามารถส่งการอัปเดตไปยัง ChatGPT ได้ทันทีที่เกิดเหตุการณ์ขึ้น โดยไม่ต้องให้เอเจนต์คอยตรวจสอบ (polling) ตั้งแต่วันที่ 29 กันยายน 2026 ซึ่งเป็นวัน OpenAI DevDay, ChatGPT รองรับข้อกำหนด MCP Events ที่เสนอในเวอร์ชันโปรโตคอล 2026-07-28 (MCP 2.0) สำหรับทุกแผนบริการ เซิร์ฟเวอร์ของคุณเพิ่มสามเมธอด (events/list, events/subscribe, events/unsubscribe) โฆษณา events ใน server/discover ผ่านการตรวจสอบความท้าทายการยืนยัน callback และลงนามการส่งทุกครั้งด้วย Standard Webhooks HMAC ChatGPT ยอมรับการส่งผ่าน webhook เท่านั้น

ปุ่ม

คู่มือนี้ครอบคลุมรูปแบบข้อความแต่ละแบบที่ OpenAI ได้จัดทำเอกสารไว้, กฎความปลอดภัยที่เซิร์ฟเวอร์ของคุณบังคับใช้, และการทดสอบวงจรทั้งหมด ผู้ที่ยังใหม่กับโปรโตคอลนี้? เริ่มต้นด้วย MCP คืออะไร คุณจะส่งการเรียก JSON-RPC และจำลอง callback ใน Apidog

MCP Events โดยสรุป

รายการ สิ่งที่ ChatGPT คาดหวัง
โปรโตคอล MCP 2.0, เวอร์ชัน 2026-07-28
ความสามารถ "events": {} ในความสามารถของ server/discover
เมธอด events/list, events/subscribe, events/unsubscribe, บนเอนด์พอยต์ที่ผ่านการยืนยันตัวตนเช่นเดียวกับเครื่องมือของคุณ
การส่ง Webhook เท่านั้น: ไม่มีการตรวจสอบ (polling), การสตรีมมิ่ง, การแจ้งเตือน gap หรือ terminated
การลงนาม Standard Webhooks HMAC-SHA256
ส่วนหัว webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id
รหัสลับ whsec_ พร้อม base64 ที่ถอดรหัสเป็น 24-64 ไบต์, จัดหาโดย ChatGPT
ข้อจำกัดของเพย์โหลด 256 KiB (262,144 ไบต์), หนึ่งเหตุการณ์ต่อหนึ่งคำขอ
ID การสมัครสมาชิก เป็นไปตามหลักการกำหนด: principal, URL callback, ชื่อเหตุการณ์, อาร์กิวเมนต์
Callback HTTPS, ตรวจสอบความท้าทายแล้ว, ไม่มีที่อยู่ส่วนตัว, ไม่มีการเปลี่ยนเส้นทาง

ที่มา: คู่มือ MCP Events ของ OpenAI และ ร่างการออกแบบ MCP Events

ทำไมต้องใช้เหตุการณ์แทนการตรวจสอบ (polling)

หากไม่มีเหตุการณ์ เอเจนต์ที่คอยดูความคิดเห็นในการรีวิวจะต้องเรียกใช้เครื่องมือตามเวลาที่กำหนดและเปรียบเทียบผลลัพธ์ ทำให้ใช้คำขอโดยเปล่าประโยชน์เมื่อไม่มีการเปลี่ยนแปลง และล่าช้าเมื่อมีการเปลี่ยนแปลง MCP Events พลิกทิศทาง: เซิร์ฟเวอร์ของคุณรู้เมื่อมีการเปลี่ยนแปลงข้อมูล จึงส่งการอัปเดตไป ข้อแลกเปลี่ยนคือข้อดีข้อเสียแบบดั้งเดิมของ webhooks กับ polling ซึ่งตอนนี้อยู่ภายใน MCP แล้ว

สรุป DevDay ของ OpenAI ใช้กระดานโปรเจกต์: ผู้ใช้ขอให้ ChatGPT ตรวจสอบงานใหม่ และเมื่อมีงานเข้ามา ChatGPT จะอ่านเอกสารที่เชื่อมโยงและร่างแผน เอกสารยังเพิ่มอีกสองกรณี: เปลี่ยนรายงานข้อผิดพลาดในช่องให้เป็นร่างคำขอดึง (message.created โดยกรองด้วย channel_id) และใช้ความคิดเห็นรีวิวกับเอกสาร (comment.created โดยกรองด้วย document_id)

ข้อกำหนดนี้มาจาก Triggers and Events Working Group (repo) ของ MCP และถูกระบุว่าเป็นแบบทดลอง ดังนั้นให้ยึดโค้ดของคุณกับ 2026-07-28 สำหรับส่วนที่เหลือของการเปิดตัว โปรดดูที่ ศูนย์รวม DevDay 2026

โฆษณาเหตุการณ์และกำหนดนิยาม

เพิ่ม events ไปยังความสามารถในการตอบกลับ server/discover ของคุณ:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": { "tools": {}, "events": {} }
  }
}

จากนั้นส่งคืนคำจำกัดความจาก events/list แต่ละรายการประกอบด้วย name, description, delivery: ["webhook"], inputSchema สำหรับอาร์กิวเมนต์การสมัครสมาชิก (ตัวกรองของคุณ เช่น document_id), และ payloadSchema สำหรับออบเจกต์ data ในการส่งแต่ละครั้ง ใช้ชื่อที่เสถียรและคำอธิบายที่เฉพาะเจาะจง ใช้ตัวกรองบนเซิร์ฟเวอร์ของคุณ และแสดงเฉพาะเหตุการณ์ที่บัญชีที่เชื่อมต่ออาจมองเห็นได้

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

ก่อนที่จะยอมรับ ให้ตรวจสอบสิทธิ์ผู้ใช้สำหรับเหตุการณ์และอาร์กิวเมนต์เหล่านั้น ตรวจสอบทั้งสองอย่างเทียบกับคำจำกัดความของคุณ กำหนดให้มีรหัสลับ whsec_ ซึ่ง base64 ถอดรหัสได้ 24-64 ไบต์ ตรวจสอบ callback (ในส่วนถัดไป) และจัดเก็บการสมัครสมาชิกพร้อมกับเจ้าของ ตัวกรอง URL รหัสลับ และวันหมดอายุ จากนั้นตอบกลับ:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "id": "sub_123",
    "refreshBefore": "2026-10-02T12:00:00Z",
    "cursor": null,
    "truncated": false
  }
}

สามกฎหลักที่ทำให้การสมัครสมาชิกถูกต้อง:

ตรวจสอบ Callback ก่อนส่งข้อมูลใดๆ

ก่อนส่งข้อมูลแอปพลิเคชัน ให้ส่ง POST เนื้อหาการยืนยันที่ลงนามแล้วพร้อมความท้าทายที่ใหม่ ใช้ครั้งเดียว และมีอายุสั้น:

{ "type": "verification", "challenge": "a-single-use-random-value" }

กำหนด webhook-id ที่ไม่ซ้ำกัน (เช่น msg_verification_123) และใช้ส่วนหัวการลงนามเดียวกับการส่งข้อมูล ChatGPT จะสะท้อนกลับ {"challenge": "a-single-use-random-value"} พร้อมสถานะ 2xx เปรียบเทียบในเวลาที่คงที่ จากนั้นเปิดใช้งานการส่งข้อมูล หากล้มเหลว ให้ส่งคืนข้อผิดพลาด JSON-RPC -32015 (CallbackEndpointError) พร้อม data.reason เช่น challenge_failed หรือ timeout แคชการผ่านแต่ละหลักการ (principal) และ URL เป็นระยะเวลาที่กำหนด เพื่อให้การรีเฟรชไม่จำเป็นต้องท้าทายใหม่

การท้าทายมีอยู่เนื่องจากผู้สมัครสมาชิกเป็นผู้จัดหารหัสลับ: หากไม่มี ใครๆ ก็สามารถมุ่งเป้าเซิร์ฟเวอร์ของคุณไปยัง URL ของเหยื่อและโจมตีด้วยคำขอจำนวนมากได้ ใช้การป้องกันที่เข้มงวดแบบเดียวกันกับทุกคำขอที่ส่งออก: ใช้ HTTPS เท่านั้น, แก้ไขและตรวจสอบที่อยู่ ณ เวลาที่เชื่อมต่อ, บล็อกที่อยู่ส่วนตัว, ท้องถิ่น และที่อยู่ที่ไม่ใช่สาธารณะอื่นๆ, และไม่เคยตามการเปลี่ยนเส้นทาง

ส่งและลงนามเหตุการณ์

เมื่อเกิดเหตุการณ์ที่ตรงกัน ให้ส่ง POST ออบเจกต์เหตุการณ์หนึ่งไปยัง URL callback:

{
  "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 แปลงเนื้อหาเป็น JSON เพียงครั้งเดียวและส่งไบต์เหล่านั้นโดยไม่เปลี่ยนแปลง เนื่องจากลายเซ็นครอบคลุมถึงข้อมูลดังกล่าว

รักษาขนาดข้อมูลให้ต่ำกว่า 256 KiB; สำหรับเรคคอร์ดขนาดใหญ่ ให้ส่งข้อมูลสรุปและเปิดเผยเครื่องมือสำหรับอ่าน ปฏิบัติต่อข้อความที่ผู้ใช้สร้างขึ้นเป็นข้อมูล และห้ามใส่อินสตรักชันของโมเดลในเพย์โหลด ลองใหม่สำหรับความล้มเหลวชั่วคราวด้วยการถอยหลังแบบเอ็กซ์โพเนนเชียลแบบจำกัด โดยคงรหัสเหตุการณ์และลงนามการพยายามแต่ละครั้งใหม่ ห้ามลองใหม่สำหรับ 410 หรือ 413 เหตุการณ์อาจมาถึงไม่เป็นลำดับ ดังนั้นให้สร้างเครื่องมือเขียนให้เป็น idempotent คู่มือการออกแบบ webhook ที่เชื่อถือได้ ครอบคลุมตารางการลองใหม่

ตรวจสอบลายเซ็น Standard Webhooks

ตาม ข้อกำหนด Standard Webhooks เนื้อหาที่ลงนามคือ ${webhook-id}.${webhook-timestamp}.${body} คีย์ HMAC-SHA256 คือรหัสลับที่ถอดรหัสด้วย base64 หลังจากลบ whsec_ ออก และส่วนหัวจะเก็บลายเซ็น v1, หนึ่งรายการหรือมากกว่าที่คั่นด้วยช่องว่าง ร่าง MCP แนะนำให้ผู้รับปฏิเสธการประทับเวลาที่เก่ากว่า 5 นาที และลบรายการซ้ำโดยใช้ webhook-id

ChatGPT ตรวจสอบจากฝั่งของตนเอง แต่การมีตัวรับที่เข้มงวดของคุณเองก็เป็นเครื่องมือทดสอบในเครื่องที่ดี โค้ดนี้ใช้ Node built-ins เท่านั้น:

// 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 มันจะส่งคืน 413 หากเกิน 256 KiB และ 401 สำหรับลายเซ็นที่ไม่ถูกต้องหรือเก่า, สะท้อนความท้าทาย, และบันทึก webhook-id แต่ละรายการเพียงครั้งเดียว เราได้ตรวจสอบ verify() เทียบกับ test vector การลงนามในไลบรารี JavaScript ของ Standard Webhooks สำหรับทฤษฎี โปรดอ่าน การยืนยันลายเซ็น webhook

เซิร์ฟเวอร์ของคุณบล็อกที่อยู่ส่วนตัว ดังนั้นจะไม่ส่งไปยัง localhost โดยค่าเริ่มต้น ร่างเอกสารอนุญาตให้ส่งไปยังเป้าหมายที่ไม่ใช่สาธารณะได้เฉพาะเมื่อกำหนดค่าอย่างชัดเจน: เพิ่มรายการใน allowlist สำหรับการพัฒนาเท่านั้น หรือเปิดเผยตัวรับผ่านอุโมงค์

ทดสอบก่อน ChatGPT

นี่คือการทดสอบ Apidog สำหรับโหมดความล้มเหลวที่ OpenAI ระบุไว้ สร้างสภาพแวดล้อมด้วย MCP_URL, MCP_TOKEN, CALLBACK_URL และ WEBHOOK_SECRET และส่งการเรียก JSON-RPC แต่ละครั้งเป็น POST ไปยัง {{MCP_URL}} พร้อม Authorization: Bearer {{MCP_TOKEN}} บวกกับส่วนหัวที่ Streamable HTTP binding กำหนด: Accept: application/json, text/event-stream, MCP-Protocol-Version: 2026-07-28 และ Mcp-Method ที่ตั้งค่าเป็นเมธอด เนื้อหาแต่ละส่วนยังต้องมี params._meta พร้อม io.modelcontextprotocol/protocolVersion และ io.modelcontextprotocol/clientCapabilities ตัวอย่างของ OpenAI ละเว้นส่วนนี้ และการขาดหายไปของ _meta ก็ส่งคืน -32602 เช่นกัน ดังนั้นขั้นตอนที่ 3 อาจผ่านด้วยเหตุผลที่ผิด

  1. **การค้นพบ (Discovery)** ส่ง server/discover และยืนยันว่า $.result.capabilities.events มีอยู่ และ $.result.supportedVersions มี 2026-07-28 ส่ง events/list และยืนยันว่า delivery ของแต่ละเหตุการณ์มี webhook
  2. **การสมัครสมาชิกแบบ Idempotent (Idempotent subscribe)** ส่งเนื้อหาการสมัครสมาชิกข้างต้นโดยใช้ {{CALLBACK_URL}} และ {{WEBHOOK_SECRET}} พร้อมตัวประมวลผลหลังการดึงตัวแปรที่บันทึก $.result.id เป็น SUB_ID ส่งซ้ำโดยไม่เปลี่ยนแปลง จากนั้นเปลี่ยนลำดับคีย์ของ arguments และยืนยันว่า $.result.id เท่ากับ {{SUB_ID}} ทั้งสองครั้ง
  3. **การตรวจสอบ (Validation)** ส่งรหัสลับที่ถอดรหัสได้น้อยกว่า 24 ไบต์ จากนั้นส่ง callback ที่เป็น http:// ร่างเอกสารระบุว่าทั้งสองกรณีจะคืนค่าเป็น -32602 (InvalidParams); ยืนยันที่ $.error.code Callback ที่เป็น IP ส่วนตัวก็ต้องล้มเหลวด้วย
  4. **ความท้าทายในการยืนยัน (Verification challenge)** สร้าง mock endpoint ของ Apidog ที่ตอบกลับ {"challenge": "wrong"} และสมัครสมาชิกโดยใช้ URL mock cloud เป็น callback ยืนยันว่า $.error.code คือ -32015 และ $.error.data.reason คือ challenge_failed จากนั้นให้ชี้ CALLBACK_URL ไปยังตัวรับของคุณและยืนยันว่าการสมัครสมาชิกสำเร็จ
  5. **เพย์โหลดเกินขนาด (Oversize payload)** ทริกเกอร์เหตุการณ์ที่มีเนื้อหาเกิน 262,144 ไบต์ ผู้ส่งของคุณควรกำหนดปฏิเสธ หากมีข้อมูลหลุดรอดไป ตัวรับจะตอบกลับ 413 และบันทึกของเซิร์ฟเวอร์ของคุณควรแสดงการพยายามเพียงครั้งเดียว ไม่ใช่การลองใหม่
  6. **การเล่นซ้ำและการดัดแปลง (Replay and tampering)** คัดลอกส่วนหัวและเนื้อหาของการส่งข้อมูลที่ลงนามแล้วจากบันทึกการส่งออกของเซิร์ฟเวอร์ของคุณไปยังคำขอ Apidog ใหม่ ส่งซ้ำทันที: 200 และไม่มีบรรทัดบันทึกที่สอง เปลี่ยนไบต์หนึ่งไบต์ในเนื้อหา: 401 ส่งซ้ำหลังจาก 5 นาที: 401

บันทึกขั้นตอนเหล่านี้เป็นสถานการณ์ (scenario) และรันใน CI ด้วย Apidog CLI คู่มือการทดสอบเซิร์ฟเวอร์ MCP ครอบคลุมการเรียกใช้เครื่องมือ และ วิธีการทดสอบ webhooks จะลงรายละเอียดเกี่ยวกับตัวรับสัญญาณ จากนั้นเชื่อมต่อเซิร์ฟเวอร์กับ ChatGPT ผ่านปลั๊กอินและรันรายการตรวจสอบวงจรชีวิตของ OpenAI

คำถามที่พบบ่อย

MCP Events คืออะไร? เป็นส่วนขยาย MCP แบบทดลองที่ช่วยให้เซิร์ฟเวอร์สามารถส่งการแจ้งเตือนเหตุการณ์ไปยังไคลเอนต์ได้ แทนที่จะให้ไคลเอนต์คอยตรวจสอบ (polling) ChatGPT รองรับโหมด webhook ของส่วนขยายนี้ที่ 2026-07-28

ChatGPT รองรับการตรวจสอบ (polling) หรือการสตรีมมิ่งสำหรับ MCP Events หรือไม่? ไม่รองรับ ChatGPT รองรับการส่งผ่าน webhook และการตรวจสอบ callback เท่านั้น การตรวจสอบ (polling), การสตรีมมิ่ง และการแจ้งเตือน gap และ terminated ไม่ได้รับการสนับสนุน

แผน ChatGPT ใดบ้างที่ได้รับ MCP Events? สรุป DevDay ของ OpenAI ระบุว่าฟีเจอร์นี้พร้อมใช้งานสำหรับทุกแผน

ใครเป็นผู้สร้างรหัสลับการลงนาม? ผู้สมัครสมาชิก ChatGPT ส่งรหัสลับ whsec_ ใน delivery.secret; เซิร์ฟเวอร์ของคุณจะตรวจสอบ, จัดเก็บ และลงนามด้วยรหัสลับนั้น และไม่เคยสร้างรหัสลับของตัวเอง

สิ่งนี้แตกต่างจาก Agents API อย่างไร? MCP Events ผลักข้อมูลจากเซิร์ฟเวอร์ของคุณเข้าสู่ ChatGPT OpenAI Agents API จะรันเอเจนต์ที่คุณสร้างขึ้นและรายงานความคืบหน้าโดยการสตรีมมิ่งหรือ webhooks

ขั้นตอนต่อไป

เพิ่ม "events": {} ไปยังเซิร์ฟเวอร์หนึ่งแห่ง, เปิดเผยหนึ่งเหตุการณ์พร้อมหนึ่งตัวกรอง, และรันการตรวจสอบทั้งหกรายการกับตัวรับในเครื่องก่อนที่จะเชื่อมต่อ ChatGPT ดาวน์โหลด Apidog เพื่อเก็บการตรวจสอบเหล่านั้นไว้เป็นสถานการณ์ที่รันทุกครั้งที่ commit

ฝึกการออกแบบ API แบบ Design-first ใน Apidog

ค้นพบวิธีที่ง่ายขึ้นในการสร้างและใช้ API