MCP 이벤트는 MCP 서버가 업데이트 발생 즉시 ChatGPT에 푸시하도록 하여, 에이전트가 이를 폴링할 필요가 없도록 합니다. 2026년 9월 29일 OpenAI DevDay 이후, ChatGPT는 모든 플랜에서 프로토콜 버전 `2026-07-28` (MCP 2.0)의 제안된 MCP 이벤트 사양을 지원합니다. 귀하의 서버는 세 가지 메서드(`events/list`, `events/subscribe`, `events/unsubscribe`)를 추가하고, `server/discover`에서 `events`를 광고하며, 콜백 확인 챌린지를 통과하고, 모든 전송을 Standard Webhooks HMAC으로 서명합니다. ChatGPT는 웹훅 전송만 허용합니다.
이 가이드에서는 OpenAI가 문서화한 각 메시지 형식, 서버가 적용하는 보안 규칙, 그리고 전체 루프에 대한 테스트 과정을 다룹니다. 프로토콜이 처음이신가요? MCP란 무엇인가부터 시작하십시오. Apidog에서 JSON-RPC 호출을 전송하고 콜백을 모의 테스트할 수 있습니다.
MCP 이벤트를 한눈에
| 항목 | ChatGPT가 기대하는 것 |
|---|---|
| 프로토콜 | MCP 2.0, 버전 2026-07-28 |
| 기능 | server/discover 기능의 "events": {} |
| 메서드 | events/list, events/subscribe, events/unsubscribe, 도구와 동일한 인증된 엔드포인트에서 |
| 전송 | 웹훅 전용: 폴링, 스트리밍, gap 또는 terminated 알림 없음 |
| 서명 | Standard Webhooks HMAC-SHA256 |
| 헤더 | webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id |
| 비밀 | whsec_에 ChatGPT가 제공하는 24-64바이트로 디코딩되는 base64 추가 |
| 페이로드 제한 | 256 KiB (262,144 바이트), 요청당 하나의 이벤트 |
| 구독 ID | 결정론적: 주체, 콜백 URL, 이벤트 이름, 인수 |
| 콜백 | HTTPS, 챌린지 확인, 사설 주소 없음, 리디렉션 없음 |
출처: OpenAI의 MCP 이벤트 가이드 및 MCP 이벤트 디자인 스케치 초안.
폴링 대신 이벤트를 사용하는 이유
이벤트가 없으면, 검토 댓글을 모니터링하는 에이전트는 타이머에 따라 도구를 호출하고 결과를 비교하여, 변경 사항이 없을 때는 요청을 낭비하고 변경 사항이 있을 때는 지연됩니다. MCP 이벤트는 방향을 바꿉니다. 서버는 레코드가 변경될 때를 알기 때문에 업데이트를 보냅니다. 절충점은 이제 MCP 내부에서 고전적인 웹훅 대 폴링의 절충점과 같습니다.
OpenAI의 DevDay 요약은 프로젝트 보드를 사용합니다. 사용자가 ChatGPT에 새 작업을 감시하도록 요청하면, 작업이 도착할 때 ChatGPT는 연결된 문서를 읽고 계획 초안을 작성합니다. 문서는 두 가지 사례를 더 추가합니다: 채널의 버그 보고서를 초안 풀 리퀘스트로 전환(`message.created`, `channel_id`로 필터링), 그리고 문서에 검토 댓글 적용(`comment.created`, `document_id`로 필터링).
이 사양은 MCP의 트리거 및 이벤트 워킹 그룹(저장소)에서 나왔으며 실험적(experimental)으로 분류되어 있으므로, 코드를 `2026-07-28`에 고정하십시오. 나머지 출시 정보는 DevDay 2026 허브를 참조하십시오.
이벤트를 광고하고 정의하기
server/discover 응답의 기능에 events를 추가하십시오:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "events": {} }
}
}
그런 다음 events/list에서 정의를 반환합니다. 각 정의에는 name, description, delivery: ["webhook"], 구독 인수에 대한 inputSchema(예: document_id와 같은 필터), 그리고 각 전송의 data 객체에 대한 payloadSchema가 포함됩니다. 안정적인 이름과 구체적인 설명을 사용하고, 서버에 필터를 적용하며, 연결된 계정이 볼 수 있는 이벤트만 나열하십시오.
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
}
}
수락하기 전에 해당 이벤트 및 인수에 대해 사용자를 승인하고, 둘 다 정의에 대해 유효성을 검사하며, base64가 24-64바이트로 디코딩되는 whsec_ 비밀을 요구하고, 콜백을 확인(다음 섹션)하고, 소유자, 필터, URL, 비밀 및 만료와 함께 구독을 저장합니다. 그런 다음 응답합니다:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
세 가지 규칙이 구독을 올바르게 유지합니다:
- 결정론적 ID. 인증된 주체, 콜백 URL, 이벤트 이름 및 인수에서
id를 파생합니다. 디자인 스케치에서는 해당 키의 잘린 SHA-256을 제안합니다. - 멱등성 업데이트/삽입. 동일한 ID로 반복 구독하면 기존 레코드가 업데이트됩니다. 키 순서가 중복을 생성하지 않도록 인수를 정식 JSON으로 비교하십시오.
refreshBefore전에 갱신. ChatGPT는 해당 시간 전에 동일한 ID와 마지막으로 저장된 커서로events/subscribe를 다시 호출합니다. 그러면 새로운 만료일을 반환합니다. 갱신에 새 비밀이 포함된 경우, 이를 교체하고 짧은 시간 동안 두 키로 서명합니다. 다시 재생할 수 없는 이벤트에 대해서는cursor: null을 반환합니다.
데이터 전송 전 콜백 확인
애플리케이션 데이터를 전송하기 전에, 신선하고 일회성이며 짧은 수명의 챌린지가 포함된 서명된 확인 본문을 POST하십시오:
{ "type": "verification", "challenge": "a-single-use-random-value" }
고유한 webhook-id(예: msg_verification_123)와 전송과 동일한 서명 헤더를 부여하십시오. ChatGPT는 2xx 응답과 함께 {"challenge": "a-single-use-random-value"}를 에코합니다. 일정한 시간 안에 비교한 다음 전송을 활성화합니다. 실패 시, challenge_failed 또는 timeout과 같은 data.reason과 함께 JSON-RPC 오류 -32015 (CallbackEndpointError)를 반환합니다. 갱신 시 다시 챌린지를 보내지 않도록 제한된 기간 동안 주체 및 URL당 통과를 캐시합니다.
챌린지는 구독자가 비밀을 제공하기 때문에 존재합니다. 챌린지가 없으면 누구나 서버를 피해자의 URL로 겨냥하여 플러딩할 수 있습니다. 모든 외부 요청에 동일한 보안 강화를 적용하십시오: HTTPS만 사용, 연결 시 주소 확인 및 유효성 검사, 사설, 로컬 및 기타 비공개 주소 차단, 리디렉션은 절대 따르지 마십시오.
이벤트 전송 및 서명
일치하는 이벤트가 발생하면, 하나의 이벤트 객체를 콜백 URL로 POST하십시오:
{
"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를 전송하십시오. 본문을 한 번 직렬화하고 해당 바이트를 그대로 전송하십시오. 서명이 이를 포함하기 때문입니다.
256 KiB 미만으로 유지하십시오. 대규모 레코드의 경우 요약을 보내고 읽기 도구를 노출하십시오. 사용자 작성 텍스트를 데이터로 취급하고 페이로드에 모델 지침을 절대 넣지 마십시오. 이벤트 ID를 유지하고 각 시도를 새로 서명하면서 제한된 지수 백오프를 사용하여 일시적인 실패를 재시도하십시오. 410 또는 413은 재시도하지 마십시오. 이벤트는 순서가 뒤바뀌어 도착할 수 있으므로, 쓰기 도구를 멱등(idempotent)하게 만드십시오. 신뢰할 수 있는 웹훅 디자인 가이드에서 재시도 일정을 다룹니다.
Standard Webhooks 서명 확인
Standard Webhooks 사양에 따라, 서명된 콘텐츠는 ${webhook-id}.${webhook-timestamp}.${body}이며, HMAC-SHA256 키는 whsec_를 제거한 후 base64 디코딩된 비밀이고, 헤더에는 하나 이상의 공백으로 구분된 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을 반환하며, 챌린지를 에코하고 각 webhook-id를 한 번씩 로깅합니다. 우리는 Standard Webhooks JavaScript 라이브러리의 서명 테스트 벡터에 대해 verify()를 확인했습니다. 이론에 대해서는 웹훅 서명 확인을 읽어보십시오.
서버는 사설 주소를 차단하므로 기본적으로 localhost로 전송하지 않습니다. 초안은 명시적으로 구성된 경우에만 비공개 대상을 허용합니다. 개발 전용 허용 목록 항목을 추가하거나 터널을 통해 수신자를 노출하십시오.
ChatGPT가 하기 전에 테스트하기
다음은 OpenAI가 나열한 실패 모드에 대한 Apidog 테스트 과정입니다. MCP_URL, MCP_TOKEN, CALLBACK_URL, WEBHOOK_SECRET로 환경을 생성하고, 각 JSON-RPC 호출을 {{MCP_URL}}로 POST 전송하며, Authorization: Bearer {{MCP_TOKEN}} 및 스트리밍 가능 HTTP 바인딩이 요구하는 헤더(`Accept: application/json, text/event-stream`, `MCP-Protocol-Version: 2026-07-28`, 메서드로 설정된 `Mcp-Method`)를 포함하십시오. 각 본문에는 `io.modelcontextprotocol/protocolVersion` 및 `io.modelcontextprotocol/clientCapabilities`가 있는 `params._meta`도 필요합니다. OpenAI 샘플은 이를 생략하며, `_meta`가 없으면 `-32602`를 반환하므로, 3단계가 잘못된 이유로 통과될 수 있습니다.
- 검색.
server/discover를 전송하고$.result.capabilities.events가 존재하며$.result.supportedVersions에2026-07-28이 포함되어 있는지 확인합니다.events/list를 전송하고 각 이벤트의delivery에webhook이 포함되어 있는지 확인합니다. - 멱등성 구독. 위 구독 본문을
{{CALLBACK_URL}}및{{WEBHOOK_SECRET}}를 사용하여 전송하고,$.result.id를SUB_ID로 저장하는 변수 추출 후처리기를 사용합니다. 변경 없이 다시 전송한 다음,arguments키를 재정렬하여 전송하고, 두 번 모두$.result.id가{{SUB_ID}}와 같은지 확인합니다. - 유효성 검사. 24바이트 미만으로 디코딩되는 비밀을 전송한 다음,
http://콜백을 전송합니다. 초안은 둘 다-32602(InvalidParams)에 매핑합니다.$.error.code를 확인하십시오. 사설 IP 콜백도 실패해야 합니다. - 확인 챌린지.
{"challenge": "wrong"}이라고 응답하는 Apidog 모의 엔드포인트를 생성하고, 해당 클라우드 모의 URL을 콜백으로 사용하여 구독합니다.$.error.code가-32015이고$.error.data.reason이challenge_failed인지 확인합니다. 그런 다음CALLBACK_URL을 수신자로 지정하고 구독이 성공하는지 확인합니다. - 초과 페이로드. 본문이 262,144바이트를 초과하는 이벤트를 트리거합니다. 발신자는 이를 거부해야 합니다. 만약 하나가 통과되면 수신자는
413으로 응답하며, 서버 로그에는 재시도가 아닌 한 번의 시도만 표시되어야 합니다. - 재생 및 변조. 서명된 전송의 헤더와 본문을 서버의 외부 로그에서 새 Apidog 요청으로 복사합니다. 즉시 다시 전송합니다:
200이 응답되고 두 번째 로그 라인은 없습니다. 본문 바이트 하나를 변경합니다:401이 응답됩니다. 5분 후에 다시 전송합니다:401이 응답됩니다.
이 단계를 시나리오로 저장하고 Apidog CLI를 사용하여 CI에서 실행하십시오. MCP 서버 테스트 플레이북은 도구 호출을 다루고, 웹훅 테스트 방법은 수신자에 대해 더 깊이 다룹니다. 그런 다음 플러그인을 통해 서버를 ChatGPT에 연결하고 OpenAI의 수명 주기 체크리스트를 실행하십시오.
자주 묻는 질문
MCP 이벤트란 무엇인가요? 클라이언트가 폴링하는 대신 서버가 클라이언트에게 이벤트 알림을 푸시할 수 있도록 하는 실험적인 MCP 확장 기능입니다. ChatGPT는 2026-07-28 버전의 웹훅 모드를 지원합니다.
ChatGPT는 MCP 이벤트에 대한 폴링 또는 스트리밍을 지원하나요? 아니요. ChatGPT는 웹훅 전송 및 콜백 확인만 지원합니다. 폴링, 스트리밍, 그리고 gap 및 terminated 알림은 지원되지 않습니다.
어떤 ChatGPT 플랜이 MCP 이벤트를 이용할 수 있나요? OpenAI의 DevDay 요약에 따르면 이 기능은 모든 플랜에서 사용할 수 있습니다.
서명 비밀은 누가 생성하나요? 구독자가 생성합니다. ChatGPT는 delivery.secret에 whsec_ 비밀을 보냅니다. 서버는 이를 사용하여 유효성을 검사하고, 저장하고, 서명하며, 자체적으로 생성하지 않습니다.
Agents API와는 어떻게 다른가요? MCP 이벤트는 서버에서 ChatGPT로 데이터를 푸시합니다. OpenAI Agents API는 사용자가 구축한 에이전트를 실행하고 스트리밍 또는 웹훅으로 진행 상황을 보고합니다.
다음 단계
하나의 서버에 "events": {}를 추가하고, 하나의 필터로 하나의 이벤트를 노출한 다음, ChatGPT에 연결하기 전에 로컬 수신자에 대해 6가지 검사를 실행하십시오. 모든 커밋에서 실행되는 시나리오로 해당 검사들을 유지하려면 Apidog를 다운로드하십시오.
